diff --git a/Gruntfile.js b/Gruntfile.js index 005c733a9d5e..4b01db377ffd 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -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 diff --git a/app/Console/Commands/ImportTimesheetData.php b/app/Console/Commands/ImportTimesheetData.php deleted file mode 100644 index 450b02b1f4c8..000000000000 --- a/app/Console/Commands/ImportTimesheetData.php +++ /dev/null @@ -1,335 +0,0 @@ -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( - ); - } - -} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 64d68d6f4642..9235cbf87b58 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -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', ]; diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php index 7a58ad08bb8c..52a078b21a66 100644 --- a/app/Http/Controllers/ClientController.php +++ b/app/Http/Controllers/ClientController.php @@ -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(), ); diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index 1a18941bb6e9..c1e9afda49a9 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -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); diff --git a/app/Http/Controllers/TaskController.php b/app/Http/Controllers/TaskController.php new file mode 100644 index 000000000000..19e48224a026 --- /dev/null +++ b/app/Http/Controllers/TaskController.php @@ -0,0 +1,260 @@ +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 ''; }) + ->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 = '
'; + }) + ->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 " '; + } + + return $str . '