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:
parent
50e3008ebb
commit
3da1e738d8
@ -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
|
||||
|
@ -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(
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -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',
|
||||
];
|
||||
|
@ -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(),
|
||||
);
|
||||
|
||||
|
@ -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);
|
||||
|
260
app/Http/Controllers/TaskController.php
Normal file
260
app/Http/Controllers/TaskController.php
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
*/
|
||||
*/
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
36
app/Models/Task.php
Normal 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);
|
||||
});
|
@ -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');
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
102
app/Ninja/Repositories/TaskRepository.php
Normal file
102
app/Ninja/Repositories/TaskRepository.php
Normal 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);
|
||||
}
|
||||
}
|
@ -19,7 +19,8 @@
|
||||
"spectrum": "~1.3.4",
|
||||
"d3": "~3.4.11",
|
||||
"handsontable": "*",
|
||||
"pdfmake": "*"
|
||||
"pdfmake": "*",
|
||||
"moment": "*"
|
||||
},
|
||||
"resolutions": {
|
||||
"jquery": "~1.11"
|
||||
|
@ -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
228
composer.lock
generated
@ -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,
|
||||
|
@ -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',
|
||||
|
@ -14,6 +14,8 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'postmark' => env('POSTMARK_API_TOKEN', ''),
|
||||
|
||||
'mailgun' => [
|
||||
'domain' => '',
|
||||
'secret' => '',
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ class AddFontSize extends Migration {
|
||||
Schema::table('accounts', function($table)
|
||||
{
|
||||
$table->smallInteger('font_size')->default(DEFAULT_FONT_SIZE);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
55
database/migrations/2015_05_27_121828_add_tasks.php
Normal file
55
database/migrations/2015_05_27_121828_add_tasks.php
Normal 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');
|
||||
}
|
||||
|
||||
}
|
6
public/css/built.css
vendored
6
public/css/built.css
vendored
@ -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;
|
||||
|
6
public/css/style.css
vendored
6
public/css/style.css
vendored
@ -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
@ -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;
|
||||
}
|
@ -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',
|
||||
|
||||
|
||||
|
||||
);
|
||||
|
@ -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()
|
||||
|
@ -65,8 +65,4 @@
|
||||
|
||||
</script>
|
||||
|
||||
@stop
|
||||
|
||||
@section('onReady')
|
||||
//$('.client-select input.form-control').focus();
|
||||
@stop
|
@ -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>
|
||||
|
@ -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
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
273
resources/views/tasks/edit.blade.php
Normal file
273
resources/views/tasks/edit.blade.php
Normal 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(' ') !!}
|
||||
<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
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user