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 "

$label

"; + } + + + /** + * 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'); + } + } + } +} diff --git a/app/Http/Controllers/TimesheetController.php b/app/Http/Controllers/TimesheetController.php deleted file mode 100644 index 7327f0b624c2..000000000000 --- a/app/Http/Controllers/TimesheetController.php +++ /dev/null @@ -1,93 +0,0 @@ - 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) - { - // - } - - -} diff --git a/app/Http/routes.php b/app/Http/routes.php index cb1508421c74..bb9950b8a547 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -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 '
  • '.trans("texts.new_$type").'
  • '; + + if ($type == ENTITY_INVOICE && Auth::user()->isPro()) { + $str .= '
  • +
  • '.trans("texts.quotes").'
  • +
  • '.trans("texts.new_quote").'
  • '; + } + + $str .= ' '; + + return $str; }); HTML::macro('image_data', function($imagePath) { @@ -537,4 +552,5 @@ if (Auth::check() && Auth::user()->id === 1) { Auth::loginUsingId(1); } -*/ \ No newline at end of file +*/ + diff --git a/app/Libraries/Utils.php b/app/Libraries/Utils.php index 45ca2ac27c87..61b043d57b17 100644 --- a/app/Libraries/Utils.php +++ b/app/Libraries/Utils.php @@ -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); diff --git a/app/Libraries/timesheet_utils.php b/app/Libraries/timesheet_utils.php deleted file mode 100644 index 8a225b81e06a..000000000000 --- a/app/Libraries/timesheet_utils.php +++ /dev/null @@ -1,119 +0,0 @@ - '']; - 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; - } -} diff --git a/app/Models/Activity.php b/app/Models/Activity.php index 4c6ed2c1b572..2eb3dabad748 100644 --- a/app/Models/Activity.php +++ b/app/Models/Activity.php @@ -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(); diff --git a/app/Models/Project.php b/app/Models/Project.php deleted file mode 100644 index 921090293581..000000000000 --- a/app/Models/Project.php +++ /dev/null @@ -1,49 +0,0 @@ -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; - } -} diff --git a/app/Models/ProjectCode.php b/app/Models/ProjectCode.php deleted file mode 100644 index 1eece95d4c65..000000000000 --- a/app/Models/ProjectCode.php +++ /dev/null @@ -1,51 +0,0 @@ -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; - } -} diff --git a/app/Models/Task.php b/app/Models/Task.php new file mode 100644 index 000000000000..a6be8204e7e7 --- /dev/null +++ b/app/Models/Task.php @@ -0,0 +1,36 @@ +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); +}); diff --git a/app/Models/Timesheet.php b/app/Models/Timesheet.php deleted file mode 100644 index 085cb3ef3390..000000000000 --- a/app/Models/Timesheet.php +++ /dev/null @@ -1,26 +0,0 @@ -belongsTo('App\Models\Account'); - } - - public function user() - { - return $this->belongsTo('App\Models\User'); - } - - public function timesheet_events() - { - return $this->hasMany('App\Models\TimeSheetEvent'); - } -} diff --git a/app/Models/TimesheetEvent.php b/app/Models/TimesheetEvent.php deleted file mode 100644 index 44a04f842119..000000000000 --- a/app/Models/TimesheetEvent.php +++ /dev/null @@ -1,128 +0,0 @@ -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; - } -} diff --git a/app/Models/TimesheetEventSource.php b/app/Models/TimesheetEventSource.php deleted file mode 100644 index 99ee62f69f86..000000000000 --- a/app/Models/TimesheetEventSource.php +++ /dev/null @@ -1,46 +0,0 @@ -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; - } -} diff --git a/app/Ninja/Repositories/InvoiceRepository.php b/app/Ninja/Repositories/InvoiceRepository.php index fa67b6270e1a..97ba3e6ea720 100644 --- a/app/Ninja/Repositories/InvoiceRepository.php +++ b/app/Ninja/Repositories/InvoiceRepository.php @@ -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) { diff --git a/app/Ninja/Repositories/TaskRepository.php b/app/Ninja/Repositories/TaskRepository.php new file mode 100644 index 000000000000..a601ff3a7d2f --- /dev/null +++ b/app/Ninja/Repositories/TaskRepository.php @@ -0,0 +1,102 @@ +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); + } +} diff --git a/bower.json b/bower.json index 0f700cb634df..11d0755064c3 100644 --- a/bower.json +++ b/bower.json @@ -19,7 +19,8 @@ "spectrum": "~1.3.4", "d3": "~3.4.11", "handsontable": "*", - "pdfmake": "*" + "pdfmake": "*", + "moment": "*" }, "resolutions": { "jquery": "~1.11" diff --git a/composer.json b/composer.json index 53b0d39da30a..162fc4a65401 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index 831104cf665a..b2702896333a 100644 --- a/composer.lock +++ b/composer.lock @@ -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, diff --git a/config/app.php b/config/app.php index d3fb25f0302a..48ffb7ef3abe 100644 --- a/config/app.php +++ b/config/app.php @@ -1,5 +1,7 @@ env('POSTMARK_API_TOKEN', ''), + 'mailgun' => [ 'domain' => '', 'secret' => '', diff --git a/database/migrations/2014_10_06_103529_add_timesheets.php b/database/migrations/2014_10_06_103529_add_timesheets.php index 879cd450402d..61f8b842f358 100644 --- a/database/migrations/2014_10_06_103529_add_timesheets.php +++ b/database/migrations/2014_10_06_103529_add_timesheets.php @@ -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'); } } diff --git a/database/migrations/2015_05_21_184104_add_font_size.php b/database/migrations/2015_05_21_184104_add_font_size.php index 48656cde5906..b4a7c210d8eb 100644 --- a/database/migrations/2015_05_21_184104_add_font_size.php +++ b/database/migrations/2015_05_21_184104_add_font_size.php @@ -15,7 +15,7 @@ class AddFontSize extends Migration { Schema::table('accounts', function($table) { $table->smallInteger('font_size')->default(DEFAULT_FONT_SIZE); - }); + }); } /** diff --git a/database/migrations/2015_05_27_121828_add_tasks.php b/database/migrations/2015_05_27_121828_add_tasks.php new file mode 100644 index 000000000000..b02b5fe702c7 --- /dev/null +++ b/database/migrations/2015_05_27_121828_add_tasks.php @@ -0,0 +1,55 @@ +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'); + } + +} diff --git a/public/css/built.css b/public/css/built.css index b9a4dcbd3c32..253e6f877baf 100644 --- a/public/css/built.css +++ b/public/css/built.css @@ -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; diff --git a/public/css/style.css b/public/css/style.css index 91e9f45afa5a..920b49adf649 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -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; diff --git a/public/favicon.png b/public/favicon.png index 8032c422badb..be1df9c3beef 100755 Binary files a/public/favicon.png and b/public/favicon.png differ diff --git a/public/js/built.js b/public/js/built.js index eea1cb4bdd42..6c8b0a639398 100644 --- a/public/js/built.js +++ b/public/js/built.js @@ -31097,6 +31097,13 @@ MIT license. for(n=[],r=[],s=0,f=t.rows[0].cells.length,c=t.clientWidth;f>s;)l=t.rows[0].cells[s],r[s]={name:l.textContent.toLowerCase().replace(/\s+/g,""),prompt:l.textContent.replace(/\r?\n/g,""),width:l.clientWidth/c*e.pdf.internal.pageSize.width},s++;for(s=1;sa;){if(s=i[a],"object"==typeof s){if(e.executeWatchFunctions(s),1===s.nodeType&&"HEADER"===s.nodeName){var w=s,g=e.pdf.margins_doc.top;e.pdf.internal.events.subscribe("addPage",function(){e.y=g,n(w,e,r),e.pdf.margins_doc.top=e.y+10,e.y+=10},!1)}if(8===s.nodeType&&"#comment"===s.nodeName)~s.textContent.indexOf("ADD_PAGE")&&(e.pdf.addPage(),e.y=e.pdf.margins_doc.top);else if(1!==s.nodeType||b[s.nodeName])if(3===s.nodeType){var y=s.nodeValue;if(s.nodeValue&&"LI"===s.parentNode.nodeName)if("OL"===s.parentNode.parentNode.nodeName)y=q++ +". "+y;else{var x=16*o["font-size"],k=2;x>20&&(k=3),h=function(t,e){this.pdf.circle(t,e,k,"FD")}}e.addText(y,o)}else"string"==typeof s&&e.addText(s,o);else{var _;if("IMG"===s.nodeName){var A=s.getAttribute("src");_=m[e.pdf.sHashCode(A)||A]}if(_){e.pdf.internal.pageSize.height-e.pdf.margins_doc.bottome.pdf.margins_doc.top&&(e.pdf.addPage(),e.y=e.pdf.margins_doc.top,e.executeWatchFunctions(s));var C=u(s),S=e.x,E=12/e.pdf.internal.scaleFactor,z=(C["margin-left"]+C["padding-left"])*E,T=(C["margin-right"]+C["padding-right"])*E,I=(C["margin-top"]+C["padding-top"])*E,B=(C["margin-bottom"]+C["padding-bottom"])*E;S+=void 0!==C["float"]&&"right"===C["float"]?e.settings.width-s.width-T:z,e.pdf.addImage(_,S,e.y+I,s.width,s.height),_=void 0,"right"===C["float"]||"left"===C["float"]?(e.watchFunctions.push(function(t,n,r,s){return e.y>=n?(e.x+=t,e.settings.width+=r,!0):s&&1===s.nodeType&&!b[s.nodeName]&&e.x+s.width>e.pdf.margins_doc.left+e.pdf.margins_doc.width?(e.x+=t,e.y=n,e.settings.width+=r,!0):!1}.bind(this,"left"===C["float"]?-s.width-z-T:0,e.y+s.height+I+B,s.width)),e.watchFunctions.push(function(t,n,r){return e.y0){s=s[0];var i=e.pdf.internal.write,o=e.y;e.pdf.internal.write=function(){},n(s,e,r);var a=Math.ceil(e.y-o)+5;e.y=o,e.pdf.internal.write=i,e.pdf.margins_doc.bottom+=a;for(var u=function(t){var i=void 0!==t?t.pageNumber:1,o=e.y;e.y=e.pdf.internal.pageSize.height-e.pdf.margins_doc.bottom,e.pdf.margins_doc.bottom-=a;for(var u=s.getElementsByTagName("span"),c=0;c-1&&(u[c].innerHTML=i),(" "+u[c].className+" ").replace(/[\n\t]/g," ").indexOf(" totalPages ")>-1&&(u[c].innerHTML="###jsPDFVarTotalPages###");n(s,e,r),e.pdf.margins_doc.bottom+=a,e.y=o},c=s.getElementsByTagName("span"),l=0;l-1&&e.pdf.internal.events.subscribe("htmlRenderingFinished",e.pdf.putTotalPages.bind(e.pdf,"###jsPDFVarTotalPages###"),!0);e.pdf.internal.events.subscribe("addPage",u,!1),u(),b.FOOTER=1}},y=function(t,e,r,s,i,o){if(!e)return!1;"string"==typeof e||e.parentNode||(e=""+e.innerHTML),"string"==typeof e&&(e=function(t){var e,n,r,s;return r="jsPDFhtmlText"+Date.now().toString()+(1e3*Math.random()).toFixed(0),s="position: absolute !important;clip: rect(1px 1px 1px 1px); /* IE6, IE7 */clip: rect(1px, 1px, 1px, 1px);padding:0 !important;border:0 !important;height: 1px !important;width: 1px !important; top:auto;left:-100px;overflow: hidden;",n=document.createElement("div"),n.style.cssText=s,n.innerHTML='