1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-08 12:12:48 +01:00

Working on tasks

This commit is contained in:
Hillel Coren 2015-05-27 19:52:10 +03:00
parent 50e3008ebb
commit 3da1e738d8
40 changed files with 1182 additions and 967 deletions

View File

@ -56,6 +56,8 @@ module.exports = function(grunt) {
'public/vendor/accounting/accounting.min.js',
'public/vendor/spectrum/spectrum.js',
'public/vendor/jspdf/dist/jspdf.min.js',
'public/vendor/moment/min/moment.min.js',
//'public/vendor/moment-duration-format/lib/moment-duration-format.js',
//'public/vendor/handsontable/dist/jquery.handsontable.full.min.js',
//'public/vendor/pdfmake/build/pdfmake.min.js',
//'public/vendor/pdfmake/build/vfs_fonts.js',
@ -63,8 +65,7 @@ module.exports = function(grunt) {
'public/js/lightbox.min.js',
'public/js/bootstrap-combobox.js',
'public/js/script.js',
'public/js/pdf.pdfmake.js',
'public/js/pdf.pdfmake.js'
],
dest: 'public/js/built.js',
nonull: true

View File

@ -1,335 +0,0 @@
<?php namespace App\Console\Commands;
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use PHPBenchTime\Timer;
class ImportTimesheetData extends Command {
protected $name = 'ninja:import-timesheet-data';
protected $description = 'Import timesheet data';
public function fire() {
$this->info(date('Y-m-d') . ' Running ImportTimesheetData...');
// Seems we are using the console timezone
DB::statement("SET SESSION time_zone = '+00:00'");
// Get the Unix epoch
$unix_epoch = new DateTime('1970-01-01T00:00:01', new DateTimeZone("UTC"));
// Create some initial sources we can test with
$user = User::first();
if (!$user) {
$this->error("Error: please create user account by logging in");
return;
}
// TODO: Populate with own test data until test data has been created
// Truncate the tables
/*$this->info("Truncate tables");
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
DB::table('projects')->truncate();
DB::table('project_codes')->truncate();
DB::table('timesheet_event_sources')->truncate();
DB::table('timesheet_events')->truncate();
DB::statement('SET FOREIGN_KEY_CHECKS=1;'); */
if (!Project::find(1)) {
$this->info("Import old project codes");
$oldcodes = json_decode(file_get_contents("/home/tlb/git/itktime/codes.json"), true);
foreach ($oldcodes as $name => $options) {
$project = Project::createNew($user);
$project->name = $options['description'];
$project->save();
$code = ProjectCode::createNew($user);
$code->name = $name;
$project->codes()->save($code);
}
}
if (!TimesheetEventSource::find(1)) {
$this->info("Import old event sources");
$oldevent_sources = json_decode(file_get_contents("/home/tlb/git/itktime/employes.json"), true);
foreach ($oldevent_sources as $source) {
$event_source = TimesheetEventSource::createNew($user);
$event_source->name = $source['name'];
$event_source->url = $source['url'];
$event_source->owner = $source['owner'];
$event_source->type = 'ical';
//$event_source->from_date = new DateTime("2009-01-01");
$event_source->save();
}
}
// Add all URL's to Curl
$this->info("Download ICAL feeds");
$T = new Timer;
$T->start();
$T->lap("Get Event Sources");
$event_sources = TimesheetEventSource::all(); // TODO: Filter based on ical feeds
$T->lap("Get ICAL responses");
$urls = [];
$event_sources->map(function($item) use(&$urls) {
$urls[] = $item->url;
});
$icalresponses = TimesheetUtils::curlGetUrls($urls);
$T->lap("Fetch all codes so we can do a quick lookup");
$codes = array();
ProjectCode::all()->map(function($item) use(&$codes) {
$codes[$item->name] = $item;
});
$this->info("Start parsing ICAL files");
foreach ($event_sources as $i => $event_source) {
if (!is_array($icalresponses[$i])) {
$this->info("Find events in " . $event_source->name);
file_put_contents("/tmp/" . $event_source->name . ".ical", $icalresponses[$i]); // FIXME: Remove
$T->lap("Split on events for ".$event_source->name);
// Check if the file is complete
if(!preg_match("/^\s*BEGIN:VCALENDAR/", $icalresponses[$i]) || !preg_match("/END:VCALENDAR\s*$/", $icalresponses[$i])) {
$this->error("Missing start or end of ical file");
continue;
}
// Extract all events from ical file
if (preg_match_all('/BEGIN:VEVENT\r?\n(.+?)\r?\nEND:VEVENT/s', $icalresponses[$i], $icalmatches)) {
$this->info("Found ".(count($icalmatches[1])-1)." events");
$T->lap("Fetch all uids and last updated at so we can do a quick lookup to find out if the event needs to be updated in the database".$event_source->name);
$uids = [];
$org_deleted = []; // Create list of events we know are deleted on the source, but still have in the db
$event_source->events()->withTrashed()->get(['uid', 'org_updated_at', 'updated_data_at', 'org_deleted_at'])->map(function($item) use(&$uids, &$org_deleted) {
if($item->org_updated_at > $item->updated_data_at) {
$uids[$item->uid] = $item->org_updated_at;
} else {
$uids[$item->uid] = $item->updated_data_at;
}
if($item->org_deleted_at > '0000-00-00 00:00:00') {
$org_deleted[$item->uid] = $item->updated_data_at;
}
});
$deleted = $uids;
// Loop over all the found events
$T->lap("Parse events for ".$event_source->name);
foreach ($icalmatches[1] as $eventstr) {
//print "---\n";
//print $eventstr."\n";
//print "---\n";
//$this->info("Match event");
# Fix lines broken by 76 char limit
$eventstr = preg_replace('/\r?\n\s/s', '', $eventstr);
//$this->info("Parse data");
$data = TimesheetUtils::parseICALEvent($eventstr);
if ($data) {
// Extract code for summary so we only import events we use
list($codename, $tags, $title) = TimesheetUtils::parseEventSummary($data['summary']);
if ($codename != null) {
$event = TimesheetEvent::createNew($user);
// Copy data to new object
$event->uid = $data['uid'];
$event->summary = $title;
$event->org_data = $eventstr;
$event->org_code = $codename;
if(isset($data['description'])) {
$event->description = $data['description'];
}
$event->owner = $event_source->owner;
$event->timesheet_event_source_id = $event_source->id;
if (isset($codes[$codename])) {
$event->project_id = $codes[$codename]->project_id;
$event->project_code_id = $codes[$codename]->id;
}
if (isset($data['location'])) {
$event->location = $data['location'];
}
# Add RECURRENCE-ID to the UID to make sure the event is unique
if (isset($data['recurrence-id'])) {
$event->uid .= "::".$data['recurrence-id'];
}
//TODO: Add support for recurring event, make limit on number of events created : https://github.com/tplaner/When
// Bail on RRULE as we don't support that
if(isset($event['rrule'])) {
die("Recurring event not supported: {$event['summary']} - {$event['dtstart']}");
}
// Convert to DateTime objects
foreach (['dtstart', 'dtend', 'created', 'last-modified'] as $key) {
// Parse and create DataTime object from ICAL format
list($dt, $timezone) = TimesheetUtils::parseICALDate($data[$key]);
// Handle bad dates in created and last-modified
if ($dt == null || $dt < $unix_epoch) {
if ($key == 'created' || $key == 'last-modified') {
$dt = $unix_epoch; // Default to UNIX epoch
$event->import_warning = "Could not parse date for $key: '" . $data[$key] . "' so default to UNIX Epoc\n";
} else {
$event->import_error = "Could not parse date for $key: '" . $data[$key] . "' so default to UNIX Epoc\n";
// TODO: Bail on this event or write to error table
die("Could not parse date for $key: '" . $data[$key] . "'\n");
}
}
// Assign DateTime object to
switch ($key) {
case 'dtstart':
$event->start_date = $dt;
if($timezone) {
$event->org_start_date_timezone = $timezone;
}
break;
case 'dtend':
$event->end_date = $dt;
if($timezone) {
$event->org_end_date_timezone = $timezone;
}
break;
case 'created':
$event->org_created_at = $dt;
break;
case 'last-modified':
$event->org_updated_at = $dt;
break;
}
}
// Check that we are witin the range
if ($event_source->from_date != null) {
$from_date = new DateTime($event_source->from_date, new DateTimeZone('UTC'));
if ($from_date > $event->end_date) {
// Skip this event
echo "Skiped: $codename: $title\n";
continue;
}
}
// Calculate number of hours
$di = $event->end_date->diff($event->start_date);
$event->hours = $di->h + $di->i / 60;
// Check for events we already have
if (isset($uids[$event->uid])) {
// Remove from deleted list
unset($deleted[$event->uid]);
// See if the event has been updated compared to the one in the database
$db_event_org_updated_at = new DateTime($uids[$event->uid], new DateTimeZone('UTC'));
// Check if same or older version of new event then skip
if($event->org_updated_at <= $db_event_org_updated_at) {
// SKIP
// Updated version of the event
} else {
// Get the old event from the database
/* @var $db_event TimesheetEvent */
$db_event = $event_source->events()->where('uid', $event->uid)->firstOrFail();
$changes = $db_event->toChangesArray($event);
// Make sure it's more than the org_updated_at that has been changed
if (count($changes) > 1) {
// Check if we have manually changed the event in the database or used it in a timesheet
if ($db_event->manualedit || $db_event->timesheet) {
$this->info("Updated Data");
$db_event->updated_data = $event->org_data;
$db_event->updated_data_at = $event->org_updated_at;
// Update the db_event with the changes
} else {
$this->info("Updated Event");
foreach ($changes as $key => $value) {
if($value == null) {
unset($db_event->$key);
} else {
$db_event->$key = $value;
}
}
}
} else {
$this->info("Nothing Changed");
// Nothing has been changed so update the org_updated_at
$db_event->org_updated_at = $changes['org_updated_at'];
}
$db_event->save();
}
} else {
try {
$this->info("New event: " . $event->summary);
$event->save();
} catch (Exception $ex) {
echo "'" . $event->summary . "'\n";
var_dump($data);
echo $ex->getMessage();
echo $ex->getTraceAsString();
exit(0);
}
}
// Add new uid to know uids
$uids[$event->uid] = $event->org_updated_at;
}
}
}
// Delete events in database that no longer exists in the source
foreach($deleted as $uid => $lastupdated_date) {
// Skip we already marked this a deleted
if(isset($org_deleted[$uid])) {
unset($deleted[$uid]);
continue;
}
// Delete or update event in db
$db_event = $event_source->events()->where('uid', $uid)->firstOrFail();
if($db_event->timesheet_id === null && !$db_event->manualedit) {
// Hard delete if this event has not been assigned to a timesheet or have been manually edited
$db_event->forceDelete();
} else {
// Mark as deleted in source
$db_event->org_deleted_at = new DateTime('now', new DateTimeZone('UTC'));
$db_event->save();
}
}
$this->info("Deleted ".count($deleted). " events");
} else {
// TODO: Parse error
}
} else {
// TODO: Curl Error
}
}
foreach($T->end()['laps'] as $lap) {
echo number_format($lap['total'], 3)." : {$lap['name']}\n";
}
$this->info('Done');
}
protected function getArguments() {
return array(
);
}
protected function getOptions() {
return array(
);
}
}

View File

@ -14,7 +14,6 @@ class Kernel extends ConsoleKernel {
'App\Console\Commands\SendRecurringInvoices',
'App\Console\Commands\CreateRandomData',
'App\Console\Commands\ResetData',
'App\Console\Commands\ImportTimesheetData',
'App\Console\Commands\CheckData',
'App\Console\Commands\SendRenewalInvoices',
];

View File

@ -20,6 +20,7 @@ use App\Models\PaymentTerm;
use App\Models\Industry;
use App\Models\Currency;
use App\Models\Country;
use App\Models\Task;
use App\Ninja\Repositories\ClientRepository;
@ -112,7 +113,7 @@ class ClientController extends BaseController
Utils::trackViewed($client->getDisplayName(), ENTITY_CLIENT);
$actionLinks = [
['label' => trans('texts.create_invoice'), 'url' => '/invoices/create/'.$client->public_id],
['label' => trans('texts.create_task'), 'url' => '/tasks/create/'.$client->public_id],
['label' => trans('texts.enter_payment'), 'url' => '/payments/create/'.$client->public_id],
['label' => trans('texts.enter_credit'), 'url' => '/credits/create/'.$client->public_id],
];
@ -128,6 +129,8 @@ class ClientController extends BaseController
'credit' => $client->getTotalCredit(),
'title' => trans('texts.view_client'),
'hasRecurringInvoices' => Invoice::scope()->where('is_recurring', '=', true)->whereClientId($client->id)->count() > 0,
'hasQuotes' => Invoice::scope()->where('is_quote', '=', true)->whereClientId($client->id)->count() > 0,
'hasTasks' => Task::scope()->whereClientId($client->id)->count() > 0,
'gatewayLink' => $client->getGatewayLink(),
);

View File

@ -327,7 +327,8 @@ class InvoiceController extends BaseController
'method' => 'POST',
'url' => 'invoices',
'title' => trans('texts.new_invoice'),
'client' => $client, );
'client' => $client,
'tasks' => Session::get('tasks') ? json_encode(Session::get('tasks')) : null);
$data = array_merge($data, self::getViewModel());
return View::make('invoices.edit', $data);

View File

@ -0,0 +1,260 @@
<?php namespace App\Http\Controllers;
use View;
use URL;
use Utils;
use Input;
use Datatable;
use Validator;
use Redirect;
use Session;
use App\Models\Client;
use App\Models\Task;
/*
use Auth;
use Cache;
use App\Models\Activity;
use App\Models\Contact;
use App\Models\Invoice;
use App\Models\Size;
use App\Models\PaymentTerm;
use App\Models\Industry;
use App\Models\Currency;
use App\Models\Country;
*/
use App\Ninja\Repositories\TaskRepository;
class TaskController extends BaseController
{
protected $taskRepo;
public function __construct(TaskRepository $taskRepo)
{
parent::__construct();
$this->taskRepo = $taskRepo;
}
/**
* Display a listing of the resource.
*
* @return Response
*/
public function index()
{
return View::make('list', array(
'entityType' => ENTITY_TASK,
'title' => trans('texts.tasks'),
'sortCol' => '2',
'columns' => Utils::trans(['checkbox', 'client', 'date', 'duration', 'description', 'status', 'action']),
));
}
public function getDatatable($clientPublicId = null)
{
$tasks = $this->taskRepo->find($clientPublicId, Input::get('sSearch'));
$table = Datatable::query($tasks);
if (!$clientPublicId) {
$table->addColumn('checkbox', function ($model) { return '<input type="checkbox" name="ids[]" value="'.$model->public_id.'" '.Utils::getEntityRowClass($model).'>'; })
->addColumn('client_name', function ($model) { return $model->client_public_id ? link_to('clients/'.$model->client_public_id, Utils::getClientDisplayName($model)) : ''; });
}
return $table->addColumn('start_time', function($model) { return Utils::fromSqlDateTime($model->start_time); })
->addColumn('duration', function($model) { return gmdate('H:i:s', $model->duration == -1 ? time() - strtotime($model->start_time) : $model->duration); })
->addColumn('description', function($model) { return $model->description; })
->addColumn('invoice_number', function($model) { return self::getStatusLabel($model); })
->addColumn('dropdown', function ($model) {
$str = '<div class="btn-group tr-action" style="visibility:hidden;">
<button type="button" class="btn btn-xs btn-default dropdown-toggle" data-toggle="dropdown">
'.trans('texts.select').' <span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">';
if (!$model->deleted_at || $model->deleted_at == '0000-00-00') {
$str .= '<li><a href="'.URL::to('tasks/'.$model->public_id.'/edit').'">'.trans('texts.edit_task').'</a></li>';
}
if ($model->invoice_number) {
$str .= '<li>' . link_to("/invoices/{$model->invoice_public_id}/edit", trans('texts.view_invoice')) . '</li>';
} elseif ($model->duration == -1) {
$str .= '<li><a href="javascript:stopTask('.$model->public_id.')">'.trans('texts.stop_task').'</a></li>';
} elseif (!$model->deleted_at || $model->deleted_at == '0000-00-00') {
$str .= '<li><a href="javascript:invoiceTask('.$model->public_id.')">'.trans('texts.invoice_task').'</a></li>';
}
if (!$model->deleted_at || $model->deleted_at == '0000-00-00') {
$str .= '<li class="divider"></li>
<li><a href="javascript:archiveEntity('.$model->public_id.')">'.trans('texts.archive_task').'</a></li>';
} else {
$str .= '<li><a href="javascript:restoreEntity('.$model->public_id.')">'.trans('texts.restore_task').'</a></li>';
}
if (!$model->is_deleted) {
$str .= '<li><a href="javascript:deleteEntity('.$model->public_id.')">'.trans('texts.delete_task').'</a></li></ul>';
}
return $str . '</div>';
})
->make();
}
private function getStatusLabel($model) {
if ($model->invoice_number) {
$class = 'success';
$label = trans('texts.invoiced');
} elseif ($model->duration == -1) {
$class = 'primary';
$label = trans('texts.running');
} else {
$class = 'default';
$label = trans('texts.logged');
}
return "<h4><div class=\"label label-{$class}\">$label</div></h4>";
}
/**
* Store a newly created resource in storage.
*
* @return Response
*/
public function store()
{
return $this->save();
}
/**
* Show the form for creating a new resource.
*
* @return Response
*/
public function create($clientPublicId = 0)
{
$data = [
'task' => null,
'clientPublicId' => Input::old('client') ? Input::old('client') : $clientPublicId,
'method' => 'POST',
'url' => 'tasks',
'title' => trans('texts.new_task'),
];
$data = array_merge($data, self::getViewModel());
return View::make('tasks.edit', $data);
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return Response
*/
public function edit($publicId)
{
$task = Task::scope($publicId)->with('client')->firstOrFail();
$data = [
'task' => $task,
'clientPublicId' => $task->client ? $task->client->public_id : 0,
'method' => 'PUT',
'url' => 'tasks/'.$publicId,
'title' => trans('texts.edit_task'),
];
$data = array_merge($data, self::getViewModel());
return View::make('tasks.edit', $data);
}
/**
* Update the specified resource in storage.
*
* @param int $id
* @return Response
*/
public function update($publicId)
{
return $this->save($publicId);
}
private static function getViewModel()
{
return [
'clients' => Client::scope()->with('contacts')->orderBy('name')->get()
];
}
private function save($publicId = null)
{
$task = $this->taskRepo->save($publicId, Input::all());
Session::flash('message', trans($publicId ? 'texts.updated_task' : 'texts.created_task'));
if (Input::get('action') == 'stop') {
return Redirect::to("tasks");
} else {
return Redirect::to("tasks/{$task->public_id}/edit");
}
}
public function bulk()
{
$action = Input::get('action');
$ids = Input::get('id') ? Input::get('id') : Input::get('ids');
if ($action == 'stop') {
$this->taskRepo->save($ids, ['action' => $action]);
Session::flash('message', trans('texts.stopped_task'));
return Redirect::to('tasks');
} else if ($action == 'invoice') {
$tasks = Task::scope($ids)->with('client')->get();
$clientPublicId = false;
$data = [];
foreach ($tasks as $task) {
if ($task->client) {
if (!$clientPublicId) {
$clientPublicId = $task->client->public_id;
} else if ($clientPublicId != $task->client->public_id) {
Session::flash('error', trans('texts.task_error_multiple_clients'));
return Redirect::to('tasks');
}
}
if ($task->duration == -1) {
Session::flash('error', trans('texts.task_error_running'));
return Redirect::to('tasks');
} else if ($task->invoice_id) {
Session::flash('error', trans('texts.task_error_invoiced'));
return Redirect::to('tasks');
}
$data[] = [
'publicId' => $task->public_id,
'description' => $task->description,
'startTime' => Utils::fromSqlDateTime($task->start_time),
'duration' => round($task->duration / (60 * 60), 2)
];
}
return Redirect::to("invoices/create/{$clientPublicId}")->with('tasks', $data);
} else {
$count = $this->taskRepo->bulk($ids, $action);
$message = Utils::pluralize($action.'d_task', $count);
Session::flash('message', $message);
if ($action == 'restore' && $count == 1) {
return Redirect::to('tasks/'.$ids[0].'/edit');
} else {
return Redirect::to('tasks');
}
}
}
}

View File

@ -1,93 +0,0 @@
<?php namespace App\Http\Controllers;
class TimesheetController extends BaseController {
/**
* Display a listing of the resource.
*
* @return Response
*/
public function index()
{
$data = [
'showBreadcrumbs' => false,
'timesheet' => [
'timesheet_number' => 1
]
];
return View::make('timesheets.edit', $data);
}
/**
* Show the form for creating a new resource.
*
* @return Response
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*
* @return Response
*/
public function store()
{
//
}
/**
* Display the specified resource.
*
* @param int $id
* @return Response
*/
public function show($id)
{
//
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return Response
*/
public function edit($id)
{
//
}
/**
* Update the specified resource in storage.
*
* @param int $id
* @return Response
*/
public function update($id)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return Response
*/
public function destroy($id)
{
//
}
}

View File

@ -78,6 +78,7 @@ if (Utils::isNinja()) {
Route::post('/signup/register', 'AccountController@doRegister');
Route::get('/news_feed/{user_type}/{version}/', 'HomeController@newsFeed');
Route::get('/demo', 'AccountController@demo');
Route::get('/keep_alive', 'HomeController@keepAlive');
}
Route::group(['middleware' => 'auth'], function() {
@ -85,8 +86,7 @@ Route::group(['middleware' => 'auth'], function() {
Route::get('view_archive/{entity_type}/{visible}', 'AccountController@setTrashVisible');
Route::get('hide_message', 'HomeController@hideMessage');
Route::get('force_inline_pdf', 'UserController@forcePDFJS');
Route::get('keep_alive', 'HomeController@keepAlive');
Route::get('api/users', array('as'=>'api.users', 'uses'=>'UserController@getDatatable'));
Route::resource('users', 'UserController');
Route::post('users/delete', 'UserController@delete');
@ -123,6 +123,11 @@ Route::group(['middleware' => 'auth'], function() {
Route::get('api/activities/{client_id?}', array('as'=>'api.activities', 'uses'=>'ActivityController@getDatatable'));
Route::post('clients/bulk', 'ClientController@bulk');
Route::resource('tasks', 'TaskController');
Route::get('api/tasks/{client_id?}', array('as'=>'api.tasks', 'uses'=>'TaskController@getDatatable'));
Route::get('tasks/create/{client_id?}', 'TaskController@create');
Route::post('tasks/bulk', 'TaskController@bulk');
Route::get('recurring_invoices', 'InvoiceController@recurringIndex');
Route::get('api/recurring_invoices/{client_id?}', array('as'=>'api.recurring_invoices', 'uses'=>'InvoiceController@getRecurringDatatable'));
@ -216,6 +221,7 @@ define('ENTITY_RECURRING_INVOICE', 'recurring_invoice');
define('ENTITY_PAYMENT', 'payment');
define('ENTITY_CREDIT', 'credit');
define('ENTITY_QUOTE', 'quote');
define('ENTITY_TASK', 'task');
define('PERSON_CONTACT', 'contact');
define('PERSON_USER', 'user');
@ -421,12 +427,21 @@ HTML::macro('menu_link', function($type) {
$Types = ucfirst($types);
$class = ( Request::is($types) || Request::is('*'.$type.'*')) && !Request::is('*advanced_settings*') ? ' active' : '';
return '<li class="dropdown '.$class.'">
$str = '<li class="dropdown '.$class.'">
<a href="'.URL::to($types).'" class="dropdown-toggle">'.trans("texts.$types").'</a>
<ul class="dropdown-menu" id="menu1">
<li><a href="'.URL::to($types.'/create').'">'.trans("texts.new_$type").'</a></li>
</ul>
<li><a href="'.URL::to($types.'/create').'">'.trans("texts.new_$type").'</a></li>';
if ($type == ENTITY_INVOICE && Auth::user()->isPro()) {
$str .= '<li class="divider"></li>
<li><a href="'.URL::to('quotes').'">'.trans("texts.quotes").'</a></li>
<li><a href="'.URL::to('quotes/create').'">'.trans("texts.new_quote").'</a></li>';
}
$str .= '</ul>
</li>';
return $str;
});
HTML::macro('image_data', function($imagePath) {
@ -537,4 +552,5 @@ if (Auth::check() && Auth::user()->id === 1)
{
Auth::loginUsingId(1);
}
*/
*/

View File

@ -333,7 +333,23 @@ class Utils
$timezone = Session::get(SESSION_TIMEZONE, DEFAULT_TIMEZONE);
$format = Session::get(SESSION_DATE_FORMAT, DEFAULT_DATE_FORMAT);
$dateTime = DateTime::createFromFormat('Y-m-d', $date, new DateTimeZone($timezone));
$dateTime = DateTime::createFromFormat('Y-m-d', $date);
$dateTime->setTimeZone(new DateTimeZone($timezone));
return $formatResult ? $dateTime->format($format) : $dateTime;
}
public static function fromSqlDateTime($date, $formatResult = true)
{
if (!$date || $date == '0000-00-00 00:00:00') {
return '';
}
$timezone = Session::get(SESSION_TIMEZONE, DEFAULT_TIMEZONE);
$format = Session::get(SESSION_DATETIME_FORMAT, DEFAULT_DATETIME_FORMAT);
$dateTime = DateTime::createFromFormat('Y-m-d H:i:s', $date);
$dateTime->setTimeZone(new DateTimeZone($timezone));
return $formatResult ? $dateTime->format($format) : $dateTime;
}
@ -404,6 +420,9 @@ class Utils
if (count($matches) == 0) {
continue;
}
usort($matches, function($a, $b) {
return strlen($b) - strlen($a);
});
foreach ($matches as $match) {
$offset = 0;
$addArray = explode('+', $match);

View File

@ -1,119 +0,0 @@
<?php
class TimesheetUtils
{
public static function parseEventSummary($summary) {
if (preg_match('/^\s*([^\s:\/]+)(?:\/([^:]+))?\s*:\s*([^)].*$|$)$/s', $summary, $matches)) {
return [strtoupper($matches[1]), strtolower($matches[2]), $matches[3]];
} else {
return false;
}
}
public static function parseICALEvent($eventstr) {
if (preg_match_all('/(?:^|\r?\n)([^;:]+)[;:]([^\r\n]+)/s', $eventstr, $matches)) {
// Build ICAL event array
$data = ['summary' => ''];
foreach ($matches[1] as $i => $key) {
# Convert escaped linebreakes to linebreak
$value = preg_replace("/\r?\n\s/", "", $matches[2][$i]);
# Unescape , and ;
$value = preg_replace('/\\\\([,;])/s', '$1', $value);
$data[strtolower($key)] = $value;
}
return $data;
} else {
return false;
}
}
public static function parseICALDate($datestr) {
$dt = null;
$timezone = null;
if (preg_match('/^TZID=(.+?):([12]\d\d\d)(\d\d)(\d\d)T(\d\d)(\d\d)(\d\d)$/', $datestr, $m)) {
$timezone = $m[1];
$dt = new DateTime("{$m[2]}-{$m[3]}-{$m[4]}T{$m[5]}:{$m[6]}:{$m[7]}", new DateTimeZone($m[1]));
} else if (preg_match('/^VALUE=DATE:([12]\d\d\d)(\d\d)(\d\d)$/', $datestr, $m)) {
$dt = new DateTime("{$m[1]}-{$m[2]}-{$m[3]}T00:00:00", new DateTimeZone("UTC"));
} else if (preg_match('/^([12]\d\d\d)(\d\d)(\d\d)T(\d\d)(\d\d)(\d\d)Z$/', $datestr, $m)) {
$dt = new DateTime("{$m[1]}-{$m[2]}-{$m[3]}T{$m[4]}:{$m[5]}:{$m[6]}", new DateTimeZone("UTC"));
} else {
return false;
}
// Convert all to UTC
if($dt->getTimezone()->getName() != 'UTC') {
$dt->setTimezone(new DateTimeZone('UTC'));
}
return [$dt, $timezone];
}
public static function curlGetUrls($urls = [], $timeout = 30) {
// Create muxer
$results = [];
$multi = curl_multi_init();
$handles = [];
$ch2idx = [];
try {
foreach ($urls as $i => $url) {
// Create new handle and add to muxer
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_ENCODING, "gzip");
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); //timeout in seconds
curl_multi_add_handle($multi, $ch);
$handles[(int) $ch] = $ch;
$ch2idx[(int) $ch] = $i;
}
// Do initial connect
$still_running = true;
while ($still_running) {
// Do curl stuff
while (($mrc = curl_multi_exec($multi, $still_running)) === CURLM_CALL_MULTI_PERFORM);
if ($mrc !== CURLM_OK) {
break;
}
// Try to read from handles that are ready
while ($info = curl_multi_info_read($multi)) {
if ($info["result"] == CURLE_OK) {
$results[$ch2idx[(int) $info["handle"]]] = curl_multi_getcontent($info["handle"]);
} else {
if (CURLE_UNSUPPORTED_PROTOCOL == $info["result"]) {
$results[$ch2idx[(int) $info["handle"]]] = [$info["result"], "Unsupported protocol"];
} else if (CURLE_URL_MALFORMAT == $info["result"]) {
$results[$ch2idx[(int) $info["handle"]]] = [$info["result"], "Malform url"];
} else if (CURLE_COULDNT_RESOLVE_HOST == $info["result"]) {
$results[$ch2idx[(int) $info["handle"]]] = [$info["result"], "Could not resolve host"];
} else if (CURLE_OPERATION_TIMEDOUT == $info["result"]) {
$results[$ch2idx[(int) $info["handle"]]] = [$info["result"], "Timed out waiting for operations to finish"];
} else {
$results[$ch2idx[(int) $info["handle"]]] = [$info["result"], "Unknown curl error code"];
}
}
}
// Sleep until
if (($rs = curl_multi_select($multi)) === -1) {
usleep(20); // select failed for some reason, so we sleep for 20ms and run some more curl stuff
}
}
} finally {
foreach ($handles as $chi => $ch) {
curl_multi_remove_handle($multi, $ch);
}
curl_multi_close($multi);
}
return $results;
}
}

View File

@ -317,6 +317,9 @@ class Activity extends Eloquent
$invoice = $payment->invoice;
$invoice->balance = $invoice->balance + $payment->amount;
if ($invoice->isPaid() && $invoice->balance > 0) {
$invoice->invoice_status_id = ($invoice->balance == $invoice->amount ? INVOICE_STATUS_DRAFT : INVOICE_STATUS_PARTIAL);
}
$invoice->save();
$activity = Activity::getBlank();

View File

@ -1,49 +0,0 @@
<?php namespace App\Models;
use Auth;
use Utils;
use Eloquent;
class Project extends Eloquent
{
public $timestamps = true;
protected $softDelete = true;
public function account()
{
return $this->belongsTo('App\Models\Account');
}
public function user()
{
return $this->belongsTo('App\Models\User');
}
public function client()
{
return $this->belongsTo('App\Models\Client');
}
public function codes()
{
return $this->hasMany('App\Models\ProjectCode');
}
public static function createNew($parent = false)
{
$className = get_called_class();
$entity = new $className();
if ($parent) {
$entity->user_id = $parent instanceof User ? $parent->id : $parent->user_id;
$entity->account_id = $parent->account_id;
} elseif (Auth::check()) {
$entity->user_id = Auth::user()->id;
$entity->account_id = Auth::user()->account_id;
} else {
Utils::fatalError();
}
return $entity;
}
}

View File

@ -1,51 +0,0 @@
<?php namespace App\Models;
use Auth;
use Utils;
use Eloquent;
use Illuminate\Database\Eloquent\SoftDeletes;
class ProjectCode extends Eloquent
{
public $timestamps = true;
use SoftDeletes;
protected $dates = ['deleted_at'];
public function account()
{
return $this->belongsTo('App\Models\Account');
}
public function user()
{
return $this->belongsTo('App\Models\User');
}
public function project()
{
return $this->belongsTo('App\Models\Project');
}
public function events()
{
return $this->hasMany('App\Models\TimesheetEvent');
}
public static function createNew($parent = false)
{
$className = get_called_class();
$entity = new $className();
if ($parent) {
$entity->user_id = $parent instanceof User ? $parent->id : $parent->user_id;
$entity->account_id = $parent->account_id;
} elseif (Auth::check()) {
$entity->user_id = Auth::user()->id;
$entity->account_id = Auth::user()->account_id;
} else {
Utils::fatalError();
}
return $entity;
}
}

36
app/Models/Task.php Normal file
View File

@ -0,0 +1,36 @@
<?php namespace App\Models;
use DB;
use Illuminate\Database\Eloquent\SoftDeletes;
class Task extends EntityModel
{
use SoftDeletes;
public function account()
{
return $this->belongsTo('App\Models\Account');
}
public function client()
{
return $this->belongsTo('App\Models\Client')->withTrashed();
}
}
Task::created(function ($task) {
//Activity::createTask($task);
});
Task::updating(function ($task) {
//Activity::updateTask($task);
});
Task::deleting(function ($task) {
//Activity::archiveTask($task);
});
Task::restoring(function ($task) {
//Activity::restoreTask($task);
});

View File

@ -1,26 +0,0 @@
<?php namespace App\Models;
use Eloquent;
use Illuminate\Database\Eloquent\SoftDeletes;
class Timesheet extends Eloquent
{
public $timestamps = true;
use SoftDeletes;
protected $dates = ['deleted_at'];
public function account()
{
return $this->belongsTo('App\Models\Account');
}
public function user()
{
return $this->belongsTo('App\Models\User');
}
public function timesheet_events()
{
return $this->hasMany('App\Models\TimeSheetEvent');
}
}

View File

@ -1,128 +0,0 @@
<?php namespace App\Models;
use Auth;
use Utils;
use Eloquent;
use Illuminate\Database\Eloquent\SoftDeletes;
class TimesheetEvent extends Eloquent
{
public $timestamps = true;
use SoftDeletes;
protected $dates = ['deleted_at'];
/* protected $dates = array('org_updated_at');
public function getDates() {
return array('created_at', 'updated_at', 'deleted_at');
} */
/* public function setOrgUpdatedAtAttribute($value)
{
var_dump($value);
$this->attributes['org_updated_at'] = $value->getTimestamp();
}*/
public function account()
{
return $this->belongsTo('App\Models\Account');
}
public function user()
{
return $this->belongsTo('App\Models\User');
}
public function source()
{
return $this->belongsTo('App\Models\TimesheetEventSource');
}
public function timesheet()
{
return $this->belongsTo('App\Models\Timesheet');
}
public function project()
{
return $this->belongsTo('App\Models\Project');
}
public function project_code()
{
return $this->belongsTo('App\Models\ProjectCode');
}
/**
* @return TimesheetEvent
*/
public static function createNew($parent = false)
{
$className = get_called_class();
$entity = new $className();
if ($parent) {
$entity->user_id = $parent instanceof User ? $parent->id : $parent->user_id;
$entity->account_id = $parent->account_id;
} elseif (Auth::check()) {
$entity->user_id = Auth::user()->id;
$entity->account_id = Auth::user()->account_id;
} else {
Utils::fatalError();
}
return $entity;
}
public function toChangesArray(TimesheetEvent $other)
{
$attributes_old = parent::toArray();
$attributes_new = $other->toArray();
$skip_keys = ['id' => 1, 'created_at' => 1, 'updated_at' => 1, 'deleted_at' => 1, 'org_data' => 1, 'update_data' => 1];
$zeroisempty_keys = ['discount' => 1];
$result = [];
// Find all the values that where changed or deleted
foreach ($attributes_old as $key => $value) {
// Skip null values, keys we don't care about and 0 value keys that means they are not used
if (empty($value) || isset($skip_keys[$key]) || (isset($zeroisempty_keys[$key]) && $value)) {
continue;
}
// Compare values if it exists in the new array
if (isset($attributes_new[$key]) || array_key_exists($key, $attributes_new)) {
if ($value instanceof \DateTime && $attributes_new[$key] instanceof \DateTime) {
if ($value != $attributes_new[$key]) {
$result[$key] = $attributes_new[$key]->format("Y-m-d H:i:s");
}
} elseif ($value instanceof \DateTime && is_string($attributes_new[$key])) {
if ($value->format("Y-m-d H:i:s") != $attributes_new[$key]) {
$result[$key] = $attributes_new[$key];
}
} elseif (is_string($value) && $attributes_new[$key] instanceof \DateTime) {
if ($attributes_new[$key]->format("Y-m-d H:i:s") != $value) {
$result[$key] = $attributes_new[$key]->format("Y-m-d H:i:s");
}
} elseif ($value != $attributes_new[$key]) {
$result[$key] = $attributes_new[$key];
}
} else {
$result[$key] = null;
}
}
// Find all the values that where deleted
foreach ($attributes_new as $key => $value) {
if (isset($skip_keys[$key])) {
continue;
}
if (!isset($attributes_old[$key])) {
$result[$key] = $value;
}
}
return $result;
}
}

View File

@ -1,46 +0,0 @@
<?php namespace App\Models;
use Auth;
use Utils;
use Eloquent;
use Illuminate\Database\Eloquent\SoftDeletes;
class TimesheetEventSource extends Eloquent
{
public $timestamps = true;
use SoftDeletes;
protected $dates = ['deleted_at'];
public function account()
{
return $this->belongsTo('App\Models\Account');
}
public function user()
{
return $this->belongsTo('App\Models\User');
}
public function events()
{
return $this->hasMany('App\Models\TimesheetEvent');
}
public static function createNew($parent = false)
{
$className = get_called_class();
$entity = new $className();
if ($parent) {
$entity->user_id = $parent instanceof User ? $parent->id : $parent->user_id;
$entity->account_id = $parent->account_id;
} elseif (Auth::check()) {
$entity->user_id = Auth::user()->id;
$entity->account_id = Auth::user()->account_id;
} else {
Utils::fatalError();
}
return $entity;
}
}

View File

@ -4,6 +4,7 @@ use App\Models\Invoice;
use App\Models\InvoiceItem;
use App\Models\Invitation;
use App\Models\Product;
use App\Models\Task;
use Utils;
class InvoiceRepository
@ -374,7 +375,11 @@ class InvoiceRepository
continue;
}
if ($item['product_key']) {
if (isset($item['task_public_id']) && $item['task_public_id']) {
$task = Task::scope($item['task_public_id'])->where('invoice_id', '=', null)->firstOrFail();
$task->invoice_id = $invoice->id;
$task->save();
} else if ($item['product_key']) {
$product = Product::findProductByKey(trim($item['product_key']));
if (!$product) {

View File

@ -0,0 +1,102 @@
<?php namespace App\Ninja\Repositories;
use Auth;
use Carbon;
use Session;
use App\Models\Client;
use App\Models\Contact;
use App\Models\Activity;
use App\Models\Task;
class TaskRepository
{
public function find($clientPublicId = null, $filter = null)
{
$query = \DB::table('tasks')
->leftJoin('clients', 'tasks.client_id', '=', 'clients.id')
->leftJoin('contacts', 'contacts.client_id', '=', 'clients.id')
->leftJoin('invoices', 'invoices.id', '=', 'tasks.invoice_id')
->where('tasks.account_id', '=', Auth::user()->account_id)
->where(function ($query) {
$query->where('contacts.is_primary', '=', true)
->orWhere('contacts.is_primary', '=', null);
})
->where('contacts.deleted_at', '=', null)
->where('clients.deleted_at', '=', null)
->select('tasks.public_id', 'clients.name as client_name', 'clients.public_id as client_public_id', 'contacts.first_name', 'contacts.email', 'contacts.last_name', 'invoices.invoice_status_id', 'tasks.start_time', 'tasks.description', 'tasks.duration', 'tasks.is_deleted', 'tasks.deleted_at', 'invoices.invoice_number', 'invoices.public_id as invoice_public_id');
if ($clientPublicId) {
$query->where('clients.public_id', '=', $clientPublicId);
}
if (!Session::get('show_trash:task')) {
$query->where('tasks.deleted_at', '=', null);
}
if ($filter) {
$query->where(function ($query) use ($filter) {
$query->where('clients.name', 'like', '%'.$filter.'%')
->orWhere('contacts.first_name', 'like', '%'.$filter.'%')
->orWhere('contacts.last_name', 'like', '%'.$filter.'%')
->orWhere('tasks.description', 'like', '%'.$filter.'%');
});
}
return $query;
}
public function save($publicId, $data)
{
if ($publicId) {
$task = Task::scope($publicId)->firstOrFail();
} else {
$task = Task::createNew();
}
if (isset($data['client']) && $data['client']) {
$task->client_id = Client::getPrivateId($data['client']);
}
if (isset($data['description'])) {
$task->description = trim($data['description']);
}
if ($data['action'] == 'start') {
$task->start_time = Carbon::now()->toDateTimeString();
$task->duration = -1;
} else if ($data['action'] == 'stop' && $task->duration == -1) {
$task->duration = strtotime('now') - strtotime($task->start_time);
} else if ($data['action'] == 'save' && $task->duration != -1) {
$task->start_time = $data['start_time'];
$task->duration = $data['duration'];
}
$task->duration = max($task->duration, -1);
$task->save();
return $task;
}
public function bulk($ids, $action)
{
$tasks = Task::withTrashed()->scope($ids)->get();
foreach ($tasks as $task) {
if ($action == 'restore') {
$task->restore();
$task->is_deleted = false;
$task->save();
} else {
if ($action == 'delete') {
$task->is_deleted = true;
$task->save();
}
$task->delete();
}
}
return count($tasks);
}
}

View File

@ -19,7 +19,8 @@
"spectrum": "~1.3.4",
"d3": "~3.4.11",
"handsontable": "*",
"pdfmake": "*"
"pdfmake": "*",
"moment": "*"
},
"resolutions": {
"jquery": "~1.11"

View File

@ -34,8 +34,9 @@
"fruitcakestudio/omnipay-sisow": "~2.0",
"alfaproject/omnipay-skrill": "dev-master",
"omnipay/bitpay": "dev-master",
"guzzlehttp/guzzle": "~4.0",
"laravelcollective/html": "~5.0"
"guzzlehttp/guzzle": "~5.0",
"laravelcollective/html": "~5.0",
"wildbit/laravel-postmark-provider": "dev-master"
},
"require-dev": {
"phpunit/phpunit": "~4.0",

228
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"hash": "a227afec5776c50509282b949ff6fd71",
"hash": "493811fbf580a8bbd5eb08b10f5bb9d1",
"packages": [
{
"name": "alfaproject/omnipay-neteller",
@ -1322,44 +1322,37 @@
},
{
"name": "guzzlehttp/guzzle",
"version": "4.2.3",
"version": "5.3.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "66fd916e9f9130bc22c51450476823391cb2f67c"
"reference": "f3c8c22471cb55475105c14769644a49c3262b93"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/66fd916e9f9130bc22c51450476823391cb2f67c",
"reference": "66fd916e9f9130bc22c51450476823391cb2f67c",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/f3c8c22471cb55475105c14769644a49c3262b93",
"reference": "f3c8c22471cb55475105c14769644a49c3262b93",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/streams": "~2.1",
"guzzlehttp/ringphp": "^1.1",
"php": ">=5.4.0"
},
"require-dev": {
"ext-curl": "*",
"phpunit/phpunit": "~4.0",
"psr/log": "~1.0"
},
"suggest": {
"ext-curl": "Guzzle will use specific adapters if cURL is present"
"phpunit/phpunit": "^4.0",
"psr/log": "^1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.2-dev"
"dev-master": "5.0-dev"
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\": "src/"
},
"files": [
"src/functions.php"
]
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
@ -1383,41 +1376,44 @@
"rest",
"web service"
],
"time": "2014-10-05 19:29:14"
"time": "2015-05-20 03:47:55"
},
{
"name": "guzzlehttp/streams",
"version": "2.1.0",
"name": "guzzlehttp/ringphp",
"version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/streams.git",
"reference": "f91b721d73f0e561410903b3b3c90a5d0e40b534"
"url": "https://github.com/guzzle/RingPHP.git",
"reference": "dbbb91d7f6c191e5e405e900e3102ac7f261bc0b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/streams/zipball/f91b721d73f0e561410903b3b3c90a5d0e40b534",
"reference": "f91b721d73f0e561410903b3b3c90a5d0e40b534",
"url": "https://api.github.com/repos/guzzle/RingPHP/zipball/dbbb91d7f6c191e5e405e900e3102ac7f261bc0b",
"reference": "dbbb91d7f6c191e5e405e900e3102ac7f261bc0b",
"shasum": ""
},
"require": {
"php": ">=5.4.0"
"guzzlehttp/streams": "~3.0",
"php": ">=5.4.0",
"react/promise": "~2.0"
},
"require-dev": {
"ext-curl": "*",
"phpunit/phpunit": "~4.0"
},
"suggest": {
"ext-curl": "Guzzle will use specific adapters if cURL is present"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0-dev"
"dev-master": "1.1-dev"
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Stream\\": "src/"
},
"files": [
"src/functions.php"
]
"GuzzleHttp\\Ring\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
@ -1430,13 +1426,58 @@
"homepage": "https://github.com/mtdowling"
}
],
"description": "Provides a simple abstraction over streams of data (Guzzle 4+)",
"description": "Provides a simple API and specification that abstracts away the details of HTTP into a single PHP function.",
"time": "2015-05-20 03:37:09"
},
{
"name": "guzzlehttp/streams",
"version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/streams.git",
"reference": "47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/streams/zipball/47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5",
"reference": "47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5",
"shasum": ""
},
"require": {
"php": ">=5.4.0"
},
"require-dev": {
"phpunit/phpunit": "~4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Stream\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Provides a simple abstraction over streams of data",
"homepage": "http://guzzlephp.org/",
"keywords": [
"Guzzle",
"stream"
],
"time": "2014-08-17 21:15:53"
"time": "2014-10-12 19:18:40"
},
{
"name": "illuminate/html",
@ -4340,6 +4381,50 @@
],
"time": "2015-03-26 18:43:54"
},
{
"name": "react/promise",
"version": "v2.2.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/promise.git",
"reference": "365fcee430dfa4ace1fbc75737ca60ceea7eeeef"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/promise/zipball/365fcee430dfa4ace1fbc75737ca60ceea7eeeef",
"reference": "365fcee430dfa4ace1fbc75737ca60ceea7eeeef",
"shasum": ""
},
"require": {
"php": ">=5.4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0-dev"
}
},
"autoload": {
"psr-4": {
"React\\Promise\\": "src/"
},
"files": [
"src/functions_include.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jan Sorgalla",
"email": "jsorgalla@googlemail.com"
}
],
"description": "A lightweight implementation of CommonJS Promises/A for PHP",
"time": "2014-12-30 13:32:42"
},
{
"name": "swiftmailer/swiftmailer",
"version": "v5.4.0",
@ -5308,6 +5393,80 @@
"laravel"
],
"time": "2015-05-21 06:56:40"
},
{
"name": "wildbit/laravel-postmark-provider",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/wildbit/laravel-postmark-provider.git",
"reference": "3cab780369d206e1c7eaae3f576ca7f0c4f5edc6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/wildbit/laravel-postmark-provider/zipball/3cab780369d206e1c7eaae3f576ca7f0c4f5edc6",
"reference": "3cab780369d206e1c7eaae3f576ca7f0c4f5edc6",
"shasum": ""
},
"require": {
"illuminate/mail": "~5.0",
"wildbit/swiftmailer-postmark": "~1.1"
},
"type": "library",
"autoload": {
"psr-0": {
"Postmark\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "An officially supported mail provider to send mail from Laravel through Postmark, see instructions for integrating it here: https://github.com/wildbit/laravel-postmark-provider/blob/master/README.md",
"time": "2015-03-19 13:32:47"
},
{
"name": "wildbit/swiftmailer-postmark",
"version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/wildbit/swiftmailer-postmark.git",
"reference": "2aff78a6cb2892e0c02e64edb753ad41d8f6496c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/wildbit/swiftmailer-postmark/zipball/2aff78a6cb2892e0c02e64edb753ad41d8f6496c",
"reference": "2aff78a6cb2892e0c02e64edb753ad41d8f6496c",
"shasum": ""
},
"require": {
"guzzlehttp/guzzle": "~5.2",
"swiftmailer/swiftmailer": "~5.1"
},
"require-dev": {
"phpunit/phpunit": "~4.5"
},
"suggest": {
"wildbit/laravel-postmark-provider": "~1.0"
},
"type": "library",
"autoload": {
"psr-0": {
"Postmark\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Postmark",
"email": "support@postmarkapp.com"
}
],
"description": "A Swiftmailer Transport for Postmark.",
"time": "2015-03-19 13:06:11"
}
],
"packages-dev": [
@ -6342,7 +6501,8 @@
"lokielse/omnipay-alipay": 20,
"alfaproject/omnipay-neteller": 20,
"alfaproject/omnipay-skrill": 20,
"omnipay/bitpay": 20
"omnipay/bitpay": 20,
"wildbit/laravel-postmark-provider": 20
},
"prefer-stable": false,
"prefer-lowest": false,

View File

@ -1,5 +1,7 @@
<?php
use App\Libraries\Utils;
return [
/*
@ -125,8 +127,8 @@ return [
'Illuminate\Filesystem\FilesystemServiceProvider',
'Illuminate\Foundation\Providers\FoundationServiceProvider',
'Illuminate\Hashing\HashServiceProvider',
'Illuminate\Mail\MailServiceProvider',
'Illuminate\Pagination\PaginationServiceProvider',
(isset($_ENV['POSTMARK_API_TOKEN']) ? 'Postmark\Adapters\LaravelMailProvider' : 'Illuminate\Mail\MailServiceProvider'),
'Illuminate\Pagination\PaginationServiceProvider',
'Illuminate\Pipeline\PipelineServiceProvider',
'Illuminate\Queue\QueueServiceProvider',
'Illuminate\Redis\RedisServiceProvider',

View File

@ -14,6 +14,8 @@ return [
|
*/
'postmark' => env('POSTMARK_API_TOKEN', ''),
'mailgun' => [
'domain' => '',
'secret' => '',

View File

@ -142,11 +142,11 @@ class AddTimesheets extends Migration {
*/
public function down()
{
Schema::drop('timesheet_events');
Schema::drop('timesheet_event_sources');
Schema::drop('timesheets');
Schema::drop('project_codes');
Schema::drop('projects');
Schema::dropIfExists('timesheet_events');
Schema::dropIfExists('timesheet_event_sources');
Schema::dropIfExists('timesheets');
Schema::dropIfExists('project_codes');
Schema::dropIfExists('projects');
}
}

View File

@ -15,7 +15,7 @@ class AddFontSize extends Migration {
Schema::table('accounts', function($table)
{
$table->smallInteger('font_size')->default(DEFAULT_FONT_SIZE);
});
});
}
/**

View File

@ -0,0 +1,55 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddTasks extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('tasks', function($table) {
$table->increments('id');
$table->unsignedInteger('user_id');
$table->unsignedInteger('account_id')->index();
$table->unsignedInteger('client_id')->nullable();
$table->unsignedInteger('invoice_id')->nullable();
$table->timestamps();
$table->softDeletes();
$table->timestamp('start_time');
$table->integer('duration')->nullable();
$table->string('description')->nullable();
$table->boolean('is_deleted')->default(false);
$table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade');
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade');
$table->foreign('client_id')->references('id')->on('clients')->onDelete('cascade');
$table->unsignedInteger('public_id')->index();
$table->unique( array('account_id','public_id') );
});
Schema::dropIfExists('timesheets');
Schema::dropIfExists('timesheet_events');
Schema::dropIfExists('timesheet_event_sources');
Schema::dropIfExists('project_codes');
Schema::dropIfExists('projects');
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('tasks');
}
}

View File

@ -3226,11 +3226,9 @@ div.checkbox > label {
background-color: #0b4d78 !important;
}
/*
.panel-default {
border-color: #e37329 !important;
div.alert {
z-index: 0;
}
*/
.alert-hide {
position: absolute;

View File

@ -842,11 +842,9 @@ div.checkbox > label {
background-color: #0b4d78 !important;
}
/*
.panel-default {
border-color: #e37329 !important;
div.alert {
z-index: 0;
}
*/
.alert-hide {
position: absolute;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

File diff suppressed because one or more lines are too long

View File

@ -1539,4 +1539,31 @@ function roundToTwo(num, toString) {
function truncate(str, length) {
return (str && str.length > length) ? (str.substr(0, length-1) + '...') : str;
}
// http://codeaid.net/javascript/convert-seconds-to-hours-minutes-and-seconds-%28javascript%29
function secondsToTime(secs)
{
secs = Math.round(secs);
var hours = Math.floor(secs / (60 * 60));
var divisor_for_minutes = secs % (60 * 60);
var minutes = Math.floor(divisor_for_minutes / 60);
var divisor_for_seconds = divisor_for_minutes % 60;
var seconds = Math.ceil(divisor_for_seconds);
var obj = {
"h": hours,
"m": minutes,
"s": seconds
};
return obj;
}
function twoDigits(value) {
if (value < 10) {
return '0' + value;
}
return value;
}

View File

@ -626,5 +626,48 @@ return array(
'last_invoice_sent' => 'Last invoice sent :date',
'processed_updates' => 'Successfully completed update',
'tasks' => 'Tasks',
'new_task' => 'New Task',
'start_time' => 'Start Time',
'created_task' => 'Successfully created task',
'updated_task' => 'Successfully updated task',
'edit_task' => 'Edit Task',
'archive_task' => 'Archive Task',
'restore_task' => 'Restore Task',
'delete_task' => 'Delete Task',
'stop_task' => 'Stop Task',
'time' => 'Time',
'start' => 'Start',
'stop' => 'Stop',
'now' => 'Now',
'timer' => 'Timer',
'manual' => 'Manual',
'date_and_time' => 'Date & Time',
'second' => 'second',
'seconds' => 'seconds',
'minute' => 'minute',
'minutes' => 'minutes',
'hour' => 'hour',
'hours' => 'hours',
'task_details' => 'Task Details',
'duration' => 'Duration',
'end_time' => 'End Time',
'end' => 'End',
'invoiced' => 'Invoiced',
'logged' => 'Logged',
'running' => 'Running',
'task_error_multiple_clients' => 'The tasks can\'t belong to different clients',
'task_error_running' => 'Please stop running tasks first',
'task_error_invoiced' => 'Tasks have already been invoiced',
'restored_task' => 'Successfully restored task',
'archived_task' => 'Successfully archived task',
'archived_tasks' => 'Successfully archived :count tasks',
'deleted_task' => 'Successfully deleted task',
'deleted_tasks' => 'Successfully deleted :count tasks',
'create_task' => 'Create Task',
'stopped_task' => 'Successfully stopped task',
'invoice_task' => 'Invoice Task',
);

View File

@ -20,8 +20,6 @@
{!! DropdownButton::normal(trans('texts.edit_client'))
->withAttributes(['class'=>'normalDropDown'])
->withContents([
['label' => trans('texts.edit_client'), 'url' => URL::to('clients/' . $client->public_id . '/edit')],
DropdownButton::DIVIDER,
['label' => trans('texts.archive_client'), 'url' => "javascript:onArchiveClick()"],
['label' => trans('texts.delete_client'), 'url' => "javascript:onDeleteClick()"],
]
@ -146,7 +144,10 @@
<ul class="nav nav-tabs nav-justified">
{!! HTML::tab_link('#activity', trans('texts.activity'), true) !!}
@if (Utils::isPro())
@if ($hasTasks)
{!! HTML::tab_link('#tasks', trans('texts.tasks')) !!}
@endif
@if ($hasQuotes && Utils::isPro())
{!! HTML::tab_link('#quotes', trans('texts.quotes')) !!}
@endif
{!! HTML::tab_link('#invoices', trans('texts.invoices')) !!}
@ -172,7 +173,26 @@
</div>
@if (Utils::isPro())
@if ($hasTasks)
<div class="tab-pane" id="tasks">
{!! Datatable::table()
->addColumn(
trans('texts.date'),
trans('texts.duration'),
trans('texts.description'),
trans('texts.status'))
->setUrl(url('api/tasks/'. $client->public_id))
->setOptions('sPaginationType', 'bootstrap')
->setOptions('bFilter', false)
->setOptions('aaSorting', [['0', 'desc']])
->render('datatable') !!}
</div>
@endif
@if (Utils::isPro() && $hasQuotes)
<div class="tab-pane" id="quotes">
{!! Datatable::table()

View File

@ -65,8 +65,4 @@
</script>
@stop
@section('onReady')
//$('.client-select input.form-control').focus();
@stop

View File

@ -310,9 +310,7 @@
<ul class="nav navbar-nav" style="font-weight: bold">
{!! HTML::nav_link('dashboard', 'dashboard') !!}
{!! HTML::menu_link('client') !!}
@if (Utils::isPro())
{!! HTML::menu_link('quote') !!}
@endif
{!! HTML::menu_link('task') !!}
{!! HTML::menu_link('invoice') !!}
{!! HTML::menu_link('payment') !!}
{!! HTML::menu_link('credit') !!}
@ -415,10 +413,6 @@
<br/>
<div class="container">
@if (!isset($showBreadcrumbs) || $showBreadcrumbs)
{!! HTML::breadcrumbs() !!}
@endif
@if (Session::has('warning'))
<div class="alert alert-warning">{{ Session::get('warning') }}</div>
@endif
@ -438,6 +432,10 @@
<div class="alert alert-danger">{{ Session::get('error') }}</div>
@endif
@if (!isset($showBreadcrumbs) || $showBreadcrumbs)
{!! HTML::breadcrumbs() !!}
@endif
@yield('content')
</div>

View File

@ -1179,6 +1179,7 @@
@endif
self.invoice_items.push(itemModel);
applyComboboxListeners();
return itemModel;
}
if (data) {
@ -1522,13 +1523,14 @@
function ItemModel(data) {
var self = this;
this.product_key = ko.observable('');
this.notes = ko.observable('');
this.cost = ko.observable(0);
this.qty = ko.observable(0);
self.product_key = ko.observable('');
self.notes = ko.observable('');
self.cost = ko.observable(0);
self.qty = ko.observable(0);
self.tax_name = ko.observable('');
self.tax_rate = ko.observable(0);
this.actionsVisible = ko.observable(false);
self.task_public_id = ko.observable('');
self.actionsVisible = ko.observable(false);
self._tax = ko.observable();
this.tax = ko.computed({
@ -1727,6 +1729,21 @@
//}
model.invoice().custom_taxes1({{ $account->custom_invoice_taxes1 ? 'true' : 'false' }});
model.invoice().custom_taxes2({{ $account->custom_invoice_taxes2 ? 'true' : 'false' }});
@if (isset($tasks) && $tasks)
// move the blank invoice line item to the end
var blank = model.invoice().invoice_items.pop();
var tasks = {!! $tasks !!};
for (var i=0; i<tasks.length; i++) {
var task = tasks[i];
var item = model.invoice().addItem();
item.notes(task.description);
item.product_key(task.startTime);
item.qty(task.duration);
item.task_public_id(task.publicId);
}
model.invoice().invoice_items.push(blank);
@endif
@endif
@endif

View File

@ -9,6 +9,10 @@
{!! Former::text('id') !!}
</div>
@if ($entityType == ENTITY_TASK)
{!! Button::primary(trans('texts.invoice'))->withAttributes(['class'=>'invoice', 'onclick' =>'submitForm("invoice")'])->appendIcon(Icon::create('check')) !!}
@endif
{!! DropdownButton::normal(trans('texts.archive'))->withContents([
['label' => trans('texts.archive_'.$entityType), 'url' => 'javascript:submitForm("archive")'],
['label' => trans('texts.delete_'.$entityType), 'url' => 'javascript:submitForm("delete")'],
@ -20,9 +24,11 @@
</label>
<div id="top_right_buttons" class="pull-right">
<input id="tableFilter" type="text" style="width:140px;margin-right:17px;background-color: white !important" class="form-control pull-left" placeholder="{{ trans('texts.filter') }}"/>
{!! Button::primary(trans("texts.new_$entityType"))->asLinkTo(URL::to("/{$entityType}s/create"))->withAttributes(array('class' => 'pull-right'))->appendIcon(Icon::create('plus-sign')) !!}
<input id="tableFilter" type="text" style="width:140px;margin-right:17px;background-color: white !important" class="form-control pull-left" placeholder="{{ trans('texts.filter') }}"/>
@if (Auth::user()->isPro() && $entityType == ENTITY_INVOICE)
{!! Button::normal(trans('texts.quotes'))->asLinkTo(URL::to('/quotes'))->appendIcon(Icon::create('list')) !!}
@endif
{!! Button::primary(trans("texts.new_$entityType"))->asLinkTo(URL::to("/{$entityType}s/create"))->appendIcon(Icon::create('plus-sign')) !!}
</div>
@if (isset($secEntityType))
@ -80,6 +86,16 @@
submitForm('mark');
}
function stopTask(id) {
$('#id').val(id);
submitForm('stop');
}
function invoiceTask(id) {
$('#id').val(id);
submitForm('invoice');
}
function setTrashVisible() {
var checked = $('#trashed').is(':checked');
window.location = '{{ URL::to('view_archive/' . $entityType) }}' + (checked ? '/true' : '/false');
@ -114,7 +130,7 @@
window.onDatatableReady = function() {
$(':checkbox').click(function() {
setArchiveEnabled();
setBulkActionsEnabled();
});
$('tbody tr').click(function(event) {
@ -122,7 +138,7 @@
$checkbox = $(this).closest('tr').find(':checkbox:not(:disabled)');
var checked = $checkbox.prop('checked');
$checkbox.prop('checked', !checked);
setArchiveEnabled();
setBulkActionsEnabled();
}
});
@ -137,7 +153,7 @@
}
$('.archive').prop('disabled', true);
$('.archive, .invoice').prop('disabled', true);
$('.archive:not(.dropdown-toggle)').click(function() {
submitForm('archive');
});
@ -146,9 +162,11 @@
$(this).closest('table').find(':checkbox:not(:disabled)').prop('checked', this.checked);
});
function setArchiveEnabled() {
function setBulkActionsEnabled() {
var checked = $('tbody :checkbox:checked').length > 0;
$('button.archive').prop('disabled', !checked);
$('button.archive, button.invoice').prop('disabled', !checked);
}
});

View File

@ -0,0 +1,273 @@
@extends('header')
@section('content')
<style type="text/css">
.date-group div.input-group {
width: 250px;
}
.time-input input,
.time-input select {
float: left;
width: 110px;
}
</style>
{!! Former::open($url)->addClass('col-md-10 col-md-offset-1 warn-on-exit task-form')->method($method)->rules(array(
)) !!}
@if ($task)
{!! Former::populate($task) !!}
@endif
<div style="display:none">
{!! Former::text('action') !!}
{!! Former::text('start_time') !!}
{!! Former::text('duration') !!}
</div>
<div class="row">
<div class="col-md-10 col-md-offset-1">
<div class="panel panel-default">
<div class="panel-body">
{!! Former::select('client')->addOption('', '')->addGroupClass('client-select') !!}
{!! Former::textarea('description')->rows(3) !!}
@if ($task && $task->duration == -1)
<center>
<div id="duration-text" style="font-size: 36px; font-weight: 300; padding: 30px 0 20px 0"/>
</center>
@else
@if (!$task)
{!! Former::radios('task_type')->radios([
trans('texts.timer') => array('name' => 'task_type', 'value' => 'timer'),
trans('texts.manual') => array('name' => 'task_type', 'value' => 'manual'),
])->inline()->check('timer')->label('&nbsp;') !!}
<div id="datetime-details" style="display: none">
<br>
@else
<div>
@endif
{!! Former::text('date')->data_date_format(Session::get(SESSION_DATE_PICKER_FORMAT, DEFAULT_DATE_PICKER_FORMAT))
->append('<i class="glyphicon glyphicon-calendar"></i>')->addGroupClass('date-group time-input') !!}
<div class="form-group">
<label for="time" class="control-label col-lg-4 col-sm-4">
{{ trans('texts.time') }}
</label>
<div class="col-lg-8 col-sm-8 time-input">
<input class="form-control" id="start_hours" placeholder="{{ uctrans('texts.hours') }}"
name="value" size="3" type="number" min="1" max="12" step="1"/>
<input class="form-control" id="start_minutes" placeholder="{{ uctrans('texts.minutes') }}"
name="value" size="2" type="number" min="0" max="59" step="1"/>
<input class="form-control" id="start_seconds" placeholder="{{ uctrans('texts.seconds') }}"
name="value" size="2" type="number" min="0" max="59" step="1"/>
<select class="form-control" id="start_ampm">
<option>AM</option>
<option>PM</option>
</select>
</div>
</div>
<div class="form-group">
<label class="control-label col-lg-4 col-sm-4">
{{ trans('texts.duration') }}
</label>
<div class="col-lg-8 col-sm-8 time-input">
<input class="form-control" id="duration_hours" placeholder="{{ uctrans('texts.hours') }}"
name="value" size="3" type="number" min="0" step="1"/>
<input class="form-control" id="duration_minutes" placeholder="{{ uctrans('texts.minutes') }}"
name="value" size="2" type="number" min="0" max="59" step="1"/>
<input class="form-control" id="duration_seconds" placeholder="{{ uctrans('texts.seconds') }}"
name="value" size="2" type="number" min="0" max="59" step="1"/>
</div>
</div>
<div class="form-group end-time">
<label for="end-time" class="control-label col-lg-4 col-sm-4">
{{ trans('texts.end') }}
</label>
<div class="col-lg-8 col-sm-8" style="padding-top: 10px">
</div>
</div>
</div>
@endif
</div>
</div>
</div>
</div>
<center class="buttons">
@if ($task && $task->duration == -1)
{!! Button::success(trans('texts.save'))->large()->appendIcon(Icon::create('floppy-disk'))->withAttributes(['id' => 'save-button']) !!}
{!! Button::primary(trans('texts.stop'))->large()->appendIcon(Icon::create('stop'))->withAttributes(['id' => 'stop-button']) !!}
@else
@if ($task)
{!! Button::success(trans('texts.save'))->large()->appendIcon(Icon::create('floppy-disk'))->withAttributes(['id' => 'save-button']) !!}
@else
{!! Button::success(trans('texts.start'))->large()->appendIcon(Icon::create('play'))->withAttributes(['id' => 'start-button']) !!}
{!! Button::success(trans('texts.save'))->large()->appendIcon(Icon::create('floppy-disk'))->withAttributes(['id' => 'save-button', 'style' => 'display:none']) !!}
@endif
{!! Button::normal(trans('texts.cancel'))->large()->asLinkTo(URL::to('/tasks'))->appendIcon(Icon::create('remove-circle')) !!}
@endif
</center>
{!! Former::close() !!}
<script type="text/javascript">
var clients = {!! $clients !!};
function tock() {
var timeLabels = {};
@foreach (['hour', 'minute', 'second'] as $period)
timeLabels['{{ $period }}'] = '{{ trans("texts.{$period}") }}';
timeLabels['{{ $period }}s'] = '{{ trans("texts.{$period}s") }}';
@endforeach
var now = Math.floor(Date.now() / 1000);
var duration = secondsToTime(now - NINJA.startTime);
var data = [];
var periods = ['hour', 'minute', 'second'];
for (var i=0; i<periods.length; i++) {
var period = periods[i];
var letter = period.charAt(0);
var value = duration[letter];
if (!value && !data.length) {
continue;
}
period = value == 1 ? timeLabels[period] : timeLabels[period + 's'];
data.push(value + ' ' + period);
}
$('#duration-text').html(data.length ? data.join(', ') : '0 ' + timeLabels['seconds']);
setTimeout(function() {
tock();
}, 1000);
}
function determineEndTime() {
var startDate = moment($('#date').datepicker('getDate'));
var parts = [$('#start_hours').val(), $('#start_minutes').val(), $('#start_seconds').val(), $('#start_ampm').val()];
var date = moment(startDate.format('YYYY-MM-DD') + ' ' + parts.join(':'), 'YYYY-MM-DD h:m:s:a', true);
var duration = (parseInt($('#duration_seconds').val(), 10) || 0)
+ (60 * (parseInt($('#duration_minutes').val(), 10) || 0))
+ (60 * 60 * (parseInt($('#duration_hours').val(), 10)) || 0);
$('#start_time').val(date.utc().format("YYYY-MM-DD HH:mm:ss"));
$('#duration').val(duration);
date.add(duration, 's')
$('div.end-time div').html(date.local().calendar());
}
function submitAction(action) {
$('#action').val(action);
$('.task-form').submit();
}
$(function() {
var $clientSelect = $('select#client');
for (var i=0; i<clients.length; i++) {
var client = clients[i];
$clientSelect.append(new Option(getClientDisplayName(client), client.public_id));
}
if ({{ $clientPublicId ? 'true' : 'false' }}) {
$clientSelect.val({{ $clientPublicId }});
}
$clientSelect.combobox();
@if ($task)
$('#date').datepicker('update', new Date('{{ Utils::fromSqlDateTime($task->start_time) }}'));
@else
var date = new Date();
$('#date').datepicker('update', date);
$('#start_hours').val((date.getHours() % 12) || 12);
$('#start_minutes').val(date.getMinutes());
$('#start_seconds').val(date.getSeconds());
$('#start_ampm').val(date.getHours() > 12 ? 'PM' : 'AM');
@endif
@if (!$task && !$clientPublicId)
$('.client-select input.form-control').focus();
@else
$('#amount').focus();
@endif
$('input[type=radio').change(function(event) {
var val = $(event.target).val();
if (val == 'now') {
$('#datetime-details').hide();
} else {
$('#datetime-details').fadeIn();
}
$('#start-button').toggle();
$('#save-button').toggle();
})
$('#start-button').click(function() {
submitAction('start');
});
$('#save-button').click(function() {
submitAction('save');
});
$('#stop-button').click(function() {
submitAction('stop');
});
$('.time-input').on('keyup change', (function() {
determineEndTime();
}));
@if ($task)
NINJA.startTime = {{ strtotime($task->start_time) }};
@if ($task->duration == -1)
tock();
@else
var date = new Date(NINJA.startTime * 1000);
var hours = date.getHours();
var pm = false;
if (hours >= 12) {
pm = true;
if (hours > 12) {
hours -= 12;
}
}
if (!hours) {
hours = 12;
}
$('#start_hours').val(hours);
$('#start_minutes').val(twoDigits(date.getMinutes()));
$('#start_seconds').val(twoDigits(date.getSeconds()));
$('#start_ampm').val(pm ? 'PM' : 'AM');
var parts = secondsToTime({{ $task->duration }});
$('#duration_hours').val(parts['h']);
$('#duration_minutes').val(parts['m']);
$('#duration_seconds').val(parts['s']);
@endif
@endif
determineEndTime();
});
</script>
@stop

View File

@ -1,26 +0,0 @@
<?php
class TimesheetUtilTest extends \PHPUnit_Framework_TestCase {
public function testParseEventSummary() {
list($code, $codes, $title) = TimesheetUtils::parseEventSummary('Riga :)');
$this->assertSame(null, $code);
list($code, $tags, $title) = TimesheetUtils::parseEventSummary('Test:');
$this->assertSame("TEST", $code);
list($code, $tags, $title) = TimesheetUtils::parseEventSummary('Test: ');
$this->assertSame("TEST", $code);
list($code, $tags, $title) = TimesheetUtils::parseEventSummary('Test::');
$this->assertSame("TEST", $code);
list($code, $tags, $title) = TimesheetUtils::parseEventSummary('TEST: Hello :)');
$this->assertSame("TEST", $code);
list($code, $tags, $title) = TimesheetUtils::parseEventSummary('Test/tags: ');
$this->assertSame('TEST', $code);
$this->assertSame('tags', $tags);
}
}