diff --git a/app/Http/Controllers/AccountApiController.php b/app/Http/Controllers/AccountApiController.php index e925401478..1920978c0e 100644 --- a/app/Http/Controllers/AccountApiController.php +++ b/app/Http/Controllers/AccountApiController.php @@ -75,7 +75,7 @@ class AccountApiController extends BaseAPIController $updatedAt = $request->updated_at ? date('Y-m-d H:i:s', $request->updated_at) : false; $transformer = new AccountTransformer(null, $request->serializer); - $account->load($transformer->getDefaultIncludes()); + $account->load(array_merge($transformer->getDefaultIncludes(), ['projects.client'])); $account = $this->createItem($account, $transformer, 'account'); return $this->response($account); diff --git a/app/Http/Controllers/ExpenseApiController.php b/app/Http/Controllers/ExpenseApiController.php index 465edcc852..2f38a53362 100644 --- a/app/Http/Controllers/ExpenseApiController.php +++ b/app/Http/Controllers/ExpenseApiController.php @@ -44,7 +44,7 @@ class ExpenseApiController extends BaseAPIController { $expenses = Expense::scope() ->withTrashed() - ->with('client', 'invoice', 'vendor') + ->with('client', 'invoice', 'vendor', 'expense_category') ->orderBy('created_at','desc'); return $this->listResponse($expenses); diff --git a/app/Http/Controllers/ExpenseCategoryController.php b/app/Http/Controllers/ExpenseCategoryController.php index 63e1851677..fdea57cbe8 100644 --- a/app/Http/Controllers/ExpenseCategoryController.php +++ b/app/Http/Controllers/ExpenseCategoryController.php @@ -6,6 +6,7 @@ use Input; use Session; use App\Services\ExpenseCategoryService; use App\Ninja\Repositories\ExpenseCategoryRepository; +use App\Ninja\Datatables\ExpenseCategoryDatatable; use App\Http\Requests\ExpenseCategoryRequest; use App\Http\Requests\CreateExpenseCategoryRequest; use App\Http\Requests\UpdateExpenseCategoryRequest; @@ -29,14 +30,10 @@ class ExpenseCategoryController extends BaseController */ public function index() { - return View::make('list', [ + return View::make('list_wrapper', [ 'entityType' => ENTITY_EXPENSE_CATEGORY, + 'datatable' => new ExpenseCategoryDatatable(), 'title' => trans('texts.expense_categories'), - 'columns' => Utils::trans([ - 'checkbox', - 'name', - '' - ]), ]); } @@ -77,7 +74,7 @@ class ExpenseCategoryController extends BaseController Session::flash('message', trans('texts.created_expense_category')); - return redirect()->to($category->getRoute()); + return redirect()->to('/expense_categories'); } public function update(UpdateExpenseCategoryRequest $request) diff --git a/app/Http/Controllers/ExpenseController.php b/app/Http/Controllers/ExpenseController.php index 38b7023877..cc850cbf32 100644 --- a/app/Http/Controllers/ExpenseController.php +++ b/app/Http/Controllers/ExpenseController.php @@ -252,7 +252,7 @@ class ExpenseController extends BaseController 'countries' => Cache::get('countries'), 'customLabel1' => Auth::user()->account->custom_vendor_label1, 'customLabel2' => Auth::user()->account->custom_vendor_label2, - 'categories' => ExpenseCategory::whereAccountId(Auth::user()->account_id)->orderBy('name')->get(), + 'categories' => ExpenseCategory::whereAccountId(Auth::user()->account_id)->withArchived()->orderBy('name')->get(), 'taxRates' => TaxRate::scope()->orderBy('name')->get(), ]; } diff --git a/app/Http/Controllers/ProductController.php b/app/Http/Controllers/ProductController.php index d3fe5e9c0d..e9ab8dd005 100644 --- a/app/Http/Controllers/ProductController.php +++ b/app/Http/Controllers/ProductController.php @@ -155,7 +155,8 @@ class ProductController extends BaseController $ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids'); $count = $this->productService->bulk($ids, $action); - Session::flash('message', trans('texts.archived_product')); + $message = Utils::pluralize($action.'d_product', $count); + Session::flash('message', $message); return $this->returnBulk(ENTITY_PRODUCT, $action, $ids); } diff --git a/app/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php new file mode 100644 index 0000000000..40d1a8846b --- /dev/null +++ b/app/Http/Controllers/ProjectController.php @@ -0,0 +1,113 @@ +projectRepo = $projectRepo; + $this->projectService = $projectService; + } + + /** + * Display a listing of the resource. + * + * @return Response + */ + public function index() + { + return View::make('list_wrapper', [ + 'entityType' => ENTITY_PROJECT, + 'datatable' => new ProjectDatatable(), + 'title' => trans('texts.projects'), + ]); + } + + public function getDatatable($expensePublicId = null) + { + $search = Input::get('sSearch'); + $userId = Auth::user()->filterId(); + + return $this->projectService->getDatatable($search, $userId); + } + + public function create(ProjectRequest $request) + { + $data = [ + 'project' => null, + 'method' => 'POST', + 'url' => 'projects', + 'title' => trans('texts.new_project'), + 'clients' => Client::scope()->with('contacts')->orderBy('name')->get(), + 'clientPublicId' => $request->client_id, + ]; + + return View::make('projects.edit', $data); + } + + public function edit(ProjectRequest $request) + { + $project = $request->entity(); + + $data = [ + 'project' => $project, + 'method' => 'PUT', + 'url' => 'projects/' . $project->public_id, + 'title' => trans('texts.edit_project'), + 'clients' => Client::scope()->with('contacts')->orderBy('name')->get(), + 'clientPublicId' => $project->client ? $project->client->public_id : null, + ]; + + return View::make('projects.edit', $data); + } + + public function store(CreateProjectRequest $request) + { + $project = $this->projectRepo->save($request->input()); + + Session::flash('message', trans('texts.created_project')); + + return redirect()->to($project->getRoute()); + } + + public function update(UpdateProjectRequest $request) + { + $project = $this->projectRepo->save($request->input(), $request->entity()); + + Session::flash('message', trans('texts.updated_project')); + + return redirect()->to($project->getRoute()); + } + + public function bulk() + { + $action = Input::get('action'); + $ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids'); + $count = $this->projectService->bulk($ids, $action); + + if ($count > 0) { + $field = $count == 1 ? "{$action}d_project" : "{$action}d_projects"; + $message = trans("texts.$field", ['count' => $count]); + Session::flash('message', $message); + } + + return redirect()->to('/projects'); + } + +} diff --git a/app/Http/Controllers/TaskApiController.php b/app/Http/Controllers/TaskApiController.php index 0671217dbc..632a8b034d 100644 --- a/app/Http/Controllers/TaskApiController.php +++ b/app/Http/Controllers/TaskApiController.php @@ -41,7 +41,7 @@ class TaskApiController extends BaseAPIController { $tasks = Task::scope() ->withTrashed() - ->with('client', 'invoice') + ->with('client', 'invoice', 'project') ->orderBy('created_at', 'desc'); return $this->listResponse($tasks); diff --git a/app/Http/Controllers/TaskController.php b/app/Http/Controllers/TaskController.php index 5dacd8ebdb..e7693b07d2 100644 --- a/app/Http/Controllers/TaskController.php +++ b/app/Http/Controllers/TaskController.php @@ -10,6 +10,7 @@ use Session; use DropdownButton; use App\Models\Client; use App\Models\Task; +use App\Models\Project; use App\Ninja\Repositories\TaskRepository; use App\Ninja\Repositories\InvoiceRepository; use App\Services\TaskService; @@ -120,6 +121,7 @@ class TaskController extends BaseController $data = [ 'task' => null, 'clientPublicId' => Input::old('client') ? Input::old('client') : ($request->client_id ?: 0), + 'projectPublicId' => Input::old('project_id') ? Input::old('project_id') : ($request->project_id ?: 0), 'method' => 'POST', 'url' => 'tasks', 'title' => trans('texts.new_task'), @@ -171,6 +173,7 @@ class TaskController extends BaseController 'task' => $task, 'entity' => $task, 'clientPublicId' => $task->client ? $task->client->public_id : 0, + 'projectPublicId' => $task->project ? $task->project->public_id : 0, 'method' => 'PUT', 'url' => 'tasks/'.$task->public_id, 'title' => trans('texts.edit_task'), @@ -206,6 +209,7 @@ class TaskController extends BaseController return [ 'clients' => Client::scope()->with('contacts')->orderBy('name')->get(), 'account' => Auth::user()->account, + 'projects' => Project::scope()->with('client.contacts')->withArchived()->orderBy('name')->get(), ]; } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index b2d587c1a8..e18c8148bb 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -215,7 +215,7 @@ class UserController extends BaseController Session::flash('message', $message); } - return Redirect::to('settings/' . ACCOUNT_USER_MANAGEMENT); + return Redirect::to('users/' . $user->public_id . '/edit'); } public function sendConfirmation($userPublicId) diff --git a/app/Http/Requests/CreateProjectRequest.php b/app/Http/Requests/CreateProjectRequest.php new file mode 100644 index 0000000000..2c55f9a7cb --- /dev/null +++ b/app/Http/Requests/CreateProjectRequest.php @@ -0,0 +1,26 @@ +user()->can('create', ENTITY_PROJECT); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + 'name' => sprintf('required|unique:projects,name,,id,account_id,%s', $this->user()->account_id), + ]; + } +} diff --git a/app/Http/Requests/ProjectRequest.php b/app/Http/Requests/ProjectRequest.php new file mode 100644 index 0000000000..42820c5e6b --- /dev/null +++ b/app/Http/Requests/ProjectRequest.php @@ -0,0 +1,7 @@ +user()->can('edit', $this->entity()); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + 'name' => 'required', + 'name' => sprintf('required|unique:projects,name,%s,id,account_id,%s', $this->entity()->id, $this->user()->account_id), + ]; + } +} diff --git a/app/Http/routes.php b/app/Http/routes.php index db3a539f5d..36dd9963e9 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -146,6 +146,13 @@ Route::group(['middleware' => 'auth:user'], function() { Route::get('api/tasks/{client_id?}', 'TaskController@getDatatable'); Route::get('tasks/create/{client_id?}', 'TaskController@create'); Route::post('tasks/bulk', 'TaskController@bulk'); + Route::get('projects', 'ProjectController@index'); + Route::get('api/projects', 'ProjectController@getDatatable'); + Route::get('projects/create/{client_id?}', 'ProjectController@create'); + Route::post('projects', 'ProjectController@store'); + Route::put('projects/{projects}', 'ProjectController@update'); + Route::get('projects/{projects}/edit', 'ProjectController@edit'); + Route::post('projects/bulk', 'ProjectController@bulk'); Route::get('api/recurring_invoices/{client_id?}', 'InvoiceController@getRecurringDatatable'); @@ -388,6 +395,7 @@ if (!defined('CONTACT_EMAIL')) { define('ENTITY_BANK_ACCOUNT', 'bank_account'); define('ENTITY_BANK_SUBACCOUNT', 'bank_subaccount'); define('ENTITY_EXPENSE_CATEGORY', 'expense_category'); + define('ENTITY_PROJECT', 'project'); define('INVOICE_TYPE_STANDARD', 1); define('INVOICE_TYPE_QUOTE', 2); diff --git a/app/Models/Account.php b/app/Models/Account.php index bf7e2e3968..a6251081c7 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -303,6 +303,14 @@ class Account extends Eloquent return $this->hasMany('App\Models\ExpenseCategory','account_id','id')->withTrashed(); } + /** + * @return mixed + */ + public function projects() + { + return $this->hasMany('App\Models\Project','account_id','id')->withTrashed(); + } + /** * @param $value */ diff --git a/app/Models/ExpenseCategory.php b/app/Models/ExpenseCategory.php index d6c0bdf357..77782e5b85 100644 --- a/app/Models/ExpenseCategory.php +++ b/app/Models/ExpenseCategory.php @@ -47,13 +47,4 @@ class ExpenseCategory extends EntityModel { return "/expense_categories/{$this->public_id}/edit"; } - - public static function getStates($entityType = false) - { - $statuses = parent::getStates($entityType); - - unset($statuses[STATUS_DELETED]); - - return $statuses; - } } diff --git a/app/Models/Product.php b/app/Models/Product.php index 13062923f8..abb93f80b2 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -87,13 +87,4 @@ class Product extends EntityModel { return $this->belongsTo('App\Models\TaxRate'); } - - public static function getStates($entityType = false) - { - $statuses = parent::getStates($entityType); - - unset($statuses[STATUS_DELETED]); - - return $statuses; - } } diff --git a/app/Models/Project.php b/app/Models/Project.php new file mode 100644 index 0000000000..4ef9e16401 --- /dev/null +++ b/app/Models/Project.php @@ -0,0 +1,65 @@ +public_id}/edit"; + } + + /** + * @return mixed + */ + public function client() + { + return $this->belongsTo('App\Models\Client')->withTrashed(); + } + +} + +Project::creating(function ($project) { + $project->setNullValues(); +}); + +Project::updating(function ($project) { + $project->setNullValues(); +}); diff --git a/app/Models/Task.php b/app/Models/Task.php index 8576b14a83..5a114a245a 100644 --- a/app/Models/Task.php +++ b/app/Models/Task.php @@ -69,6 +69,14 @@ class Task extends EntityModel return $this->belongsTo('App\Models\Client')->withTrashed(); } + /** + * @return mixed + */ + public function project() + { + return $this->belongsTo('App\Models\Project')->withTrashed(); + } + /** * @param $task * @return string diff --git a/app/Ninja/Datatables/ProjectDatatable.php b/app/Ninja/Datatables/ProjectDatatable.php new file mode 100644 index 0000000000..15b8fc6178 --- /dev/null +++ b/app/Ninja/Datatables/ProjectDatatable.php @@ -0,0 +1,59 @@ +can('editByOwner', [ENTITY_PROJECT, $model->user_id])) { + return $model->project; + } + + return link_to("projects/{$model->public_id}/edit", $model->project)->toHtml(); + } + ], + [ + 'client_name', + function ($model) + { + if ($model->client_public_id) { + if(!Auth::user()->can('viewByOwner', [ENTITY_CLIENT, $model->client_user_id])){ + return Utils::getClientDisplayName($model); + } + + return link_to("clients/{$model->client_public_id}", Utils::getClientDisplayName($model))->toHtml(); + } else { + return ''; + } + } + ] + ]; + } + + public function actions() + { + return [ + [ + trans('texts.edit_project'), + function ($model) { + return URL::to("projects/{$model->public_id}/edit") ; + }, + function ($model) { + return Auth::user()->can('editByOwner', [ENTITY_PROJECT, $model->user_id]); + } + ], + ]; + } + +} diff --git a/app/Ninja/Datatables/TaskDatatable.php b/app/Ninja/Datatables/TaskDatatable.php index a65e28b8de..4d3956f260 100644 --- a/app/Ninja/Datatables/TaskDatatable.php +++ b/app/Ninja/Datatables/TaskDatatable.php @@ -8,7 +8,7 @@ use App\Models\Task; class TaskDatatable extends EntityDatatable { public $entityType = ENTITY_TASK; - public $sortCol = 2; + public $sortCol = 3; public function columns() { @@ -24,6 +24,16 @@ class TaskDatatable extends EntityDatatable }, ! $this->hideClient ], + [ + 'project', + function ($model) { + if(!Auth::user()->can('editByOwner', [ENTITY_PROJECT, $model->project_user_id])){ + return $model->project; + } + + return $model->project_public_id ? link_to("projects/{$model->project_public_id}/edit", $model->project)->toHtml() : ''; + } + ], [ 'date', function ($model) { diff --git a/app/Ninja/Repositories/AccountRepository.php b/app/Ninja/Repositories/AccountRepository.php index b80bd1321e..04342ddca2 100644 --- a/app/Ninja/Repositories/AccountRepository.php +++ b/app/Ninja/Repositories/AccountRepository.php @@ -187,7 +187,8 @@ class AccountRepository ENTITY_VENDOR, ENTITY_RECURRING_INVOICE, ENTITY_PAYMENT, - ENTITY_CREDIT + ENTITY_CREDIT, + ENTITY_PROJECT, ]; foreach ($entityTypes as $entityType) { diff --git a/app/Ninja/Repositories/ExpenseCategoryRepository.php b/app/Ninja/Repositories/ExpenseCategoryRepository.php index bdf82a10d7..a9224b4266 100644 --- a/app/Ninja/Repositories/ExpenseCategoryRepository.php +++ b/app/Ninja/Repositories/ExpenseCategoryRepository.php @@ -25,7 +25,8 @@ class ExpenseCategoryRepository extends BaseRepository 'expense_categories.name as category', 'expense_categories.public_id', 'expense_categories.user_id', - 'expense_categories.deleted_at' + 'expense_categories.deleted_at', + 'expense_categories.is_deleted' ); $this->applyFilters($query, ENTITY_EXPENSE_CATEGORY); diff --git a/app/Ninja/Repositories/ExpenseRepository.php b/app/Ninja/Repositories/ExpenseRepository.php index aa97915e8e..85bae1501a 100644 --- a/app/Ninja/Repositories/ExpenseRepository.php +++ b/app/Ninja/Repositories/ExpenseRepository.php @@ -54,7 +54,7 @@ class ExpenseRepository extends BaseRepository ->where('contacts.deleted_at', '=', null) ->where('vendors.deleted_at', '=', null) ->where('clients.deleted_at', '=', null) - ->where(function ($query) { + ->where(function ($query) { // handle when client isn't set $query->where('contacts.is_primary', '=', true) ->orWhere('contacts.is_primary', '=', null); }) diff --git a/app/Ninja/Repositories/ProductRepository.php b/app/Ninja/Repositories/ProductRepository.php index 3cc191c86e..6500124c51 100644 --- a/app/Ninja/Repositories/ProductRepository.php +++ b/app/Ninja/Repositories/ProductRepository.php @@ -32,7 +32,8 @@ class ProductRepository extends BaseRepository 'products.cost', 'tax_rates.name as tax_name', 'tax_rates.rate as tax_rate', - 'products.deleted_at' + 'products.deleted_at', + 'products.is_deleted' ); if ($filter) { diff --git a/app/Ninja/Repositories/ProjectRepository.php b/app/Ninja/Repositories/ProjectRepository.php new file mode 100644 index 0000000000..aa802a069b --- /dev/null +++ b/app/Ninja/Repositories/ProjectRepository.php @@ -0,0 +1,75 @@ +get(); + } + + public function find($filter = null, $userId = false) + { + $query = DB::table('projects') + ->where('projects.account_id', '=', Auth::user()->account_id) + ->leftjoin('clients', 'clients.id', '=', 'projects.client_id') + ->leftJoin('contacts', 'contacts.client_id', '=', 'clients.id') + ->where('contacts.deleted_at', '=', null) + ->where('clients.deleted_at', '=', null) + ->where(function ($query) { // handle when client isn't set + $query->where('contacts.is_primary', '=', true) + ->orWhere('contacts.is_primary', '=', null); + }) + ->select( + 'projects.name as project', + 'projects.public_id', + 'projects.user_id', + 'projects.deleted_at', + 'projects.is_deleted', + DB::raw("COALESCE(NULLIF(clients.name,''), NULLIF(CONCAT(contacts.first_name, ' ', contacts.last_name),''), NULLIF(contacts.email,'')) client_name"), + 'clients.user_id as client_user_id', + 'clients.public_id as client_public_id' + ); + + $this->applyFilters($query, ENTITY_PROJECT); + + 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('contacts.email', 'like', '%'.$filter.'%') + ->orWhere('projects.name', 'like', '%'.$filter.'%'); + }); + } + + if ($userId) { + $query->where('projects.user_id', '=', $userId); + } + + return $query; + } + + public function save($input, $project = false) + { + $publicId = isset($data['public_id']) ? $data['public_id'] : false; + + if ( ! $project) { + $project = Project::createNew(); + } + + $project->fill($input); + $project->save(); + + return $project; + } +} diff --git a/app/Ninja/Repositories/TaskRepository.php b/app/Ninja/Repositories/TaskRepository.php index c16bcae9e1..579ef71e68 100644 --- a/app/Ninja/Repositories/TaskRepository.php +++ b/app/Ninja/Repositories/TaskRepository.php @@ -3,6 +3,7 @@ use Auth; use Session; use App\Models\Client; +use App\Models\Project; use App\Models\Task; class TaskRepository extends BaseRepository @@ -18,8 +19,9 @@ class TaskRepository extends BaseRepository ->leftJoin('clients', 'tasks.client_id', '=', 'clients.id') ->leftJoin('contacts', 'contacts.client_id', '=', 'clients.id') ->leftJoin('invoices', 'invoices.id', '=', 'tasks.invoice_id') + ->leftJoin('projects', 'projects.id', '=', 'tasks.project_id') ->where('tasks.account_id', '=', Auth::user()->account_id) - ->where(function ($query) { + ->where(function ($query) { // handle when client isn't set $query->where('contacts.is_primary', '=', true) ->orWhere('contacts.is_primary', '=', null); }) @@ -46,7 +48,10 @@ class TaskRepository extends BaseRepository 'tasks.time_log as duration', 'tasks.created_at', 'tasks.created_at as date', - 'tasks.user_id' + 'tasks.user_id', + 'projects.name as project', + 'projects.public_id as project_public_id', + 'projects.user_id as project_user_id' ); if ($clientPublicId) { @@ -84,7 +89,9 @@ class TaskRepository extends BaseRepository $query->where('clients.name', 'like', '%'.$filter.'%') ->orWhere('contacts.first_name', 'like', '%'.$filter.'%') ->orWhere('contacts.last_name', 'like', '%'.$filter.'%') - ->orWhere('tasks.description', 'like', '%'.$filter.'%'); + ->orWhere('tasks.description', 'like', '%'.$filter.'%') + ->orWhere('contacts.email', 'like', '%'.$filter.'%') + ->orWhere('projects.name', 'like', '%'.$filter.'%'); }); } @@ -105,9 +112,13 @@ class TaskRepository extends BaseRepository return $task; } - if (isset($data['client']) && $data['client']) { - $task->client_id = Client::getPrivateId($data['client']); + if (isset($data['client'])) { + $task->client_id = $data['client'] ? Client::getPrivateId($data['client']) : null; } + if (isset($data['project_id'])) { + $task->project_id = $data['project_id'] ? Project::getPrivateId($data['project_id']) : null; + } + if (isset($data['description'])) { $task->description = trim($data['description']); } diff --git a/app/Ninja/Transformers/AccountTransformer.php b/app/Ninja/Transformers/AccountTransformer.php index fce83760ae..4ddbf8f583 100644 --- a/app/Ninja/Transformers/AccountTransformer.php +++ b/app/Ninja/Transformers/AccountTransformer.php @@ -14,7 +14,8 @@ class AccountTransformer extends EntityTransformer 'users', 'products', 'tax_rates', - 'expense_categories' + 'expense_categories', + 'projects', ]; /** @@ -36,6 +37,16 @@ class AccountTransformer extends EntityTransformer return $this->includeCollection($account->expense_categories, $transformer, 'expense_categories'); } + /** + * @param Account $account + * @return \League\Fractal\Resource\Collection + */ + public function includeProjects(Account $account) + { + $transformer = new ProjectTransformer($account, $this->serializer); + return $this->includeCollection($account->projects, $transformer, 'projects'); + } + /** * @param Account $account * @return \League\Fractal\Resource\Collection diff --git a/app/Ninja/Transformers/ExpenseTransformer.php b/app/Ninja/Transformers/ExpenseTransformer.php index 31248deaa1..953eac338a 100644 --- a/app/Ninja/Transformers/ExpenseTransformer.php +++ b/app/Ninja/Transformers/ExpenseTransformer.php @@ -7,7 +7,7 @@ class ExpenseTransformer extends EntityTransformer public function __construct($account = null, $serializer = null, $client = null) { parent::__construct($account, $serializer); - + $this->client = $client; } @@ -23,7 +23,7 @@ class ExpenseTransformer extends EntityTransformer 'transaction_id' => $expense->transaction_id, 'bank_id' => $expense->bank_id, 'expense_currency_id' => (int) $expense->expense_currency_id, - 'expense_category_id' => (int) $expense->expense_category_id, + 'expense_category_id' => $expense->expense_category ? (int) $expense->expense_category->public_id : null, 'amount' => (float) $expense->amount, 'expense_date' => $expense->expense_date, 'exchange_rate' => (float) $expense->exchange_rate, @@ -38,4 +38,4 @@ class ExpenseTransformer extends EntityTransformer 'vendor_id' => isset($expense->vendor->public_id) ? (int) $expense->vendor->public_id : null, ]); } -} \ No newline at end of file +} diff --git a/app/Ninja/Transformers/ProjectTransformer.php b/app/Ninja/Transformers/ProjectTransformer.php new file mode 100644 index 0000000000..1de02e3b4e --- /dev/null +++ b/app/Ninja/Transformers/ProjectTransformer.php @@ -0,0 +1,18 @@ +getDefaults($project), [ + 'id' => (int) $project->public_id, + 'name' => $project->name, + 'client_id' => $project->client ? (int) $project->client->public_id : null, + 'updated_at' => $this->getTimestamp($project->updated_at), + 'archived_at' => $this->getTimestamp($project->deleted_at), + 'is_deleted' => (bool) $project->is_deleted, + ]); + } +} diff --git a/app/Ninja/Transformers/TaskTransformer.php b/app/Ninja/Transformers/TaskTransformer.php index 9615dd3550..24acd0d977 100644 --- a/app/Ninja/Transformers/TaskTransformer.php +++ b/app/Ninja/Transformers/TaskTransformer.php @@ -46,6 +46,7 @@ class TaskTransformer extends EntityTransformer 'archived_at' => (int) $this->getTimestamp($task->deleted_at), 'invoice_id' => $task->invoice ? (int) $task->invoice->public_id : false, 'client_id' => $task->client ? (int) $task->client->public_id : false, + 'project_id' => $task->project ? (int) $task->project->public_id : false, 'is_deleted' => (bool) $task->is_deleted, 'time_log' => $task->time_log, 'is_running' => (bool) $task->is_running, diff --git a/app/Policies/ProjectPolicy.php b/app/Policies/ProjectPolicy.php new file mode 100644 index 0000000000..a88d3a3790 --- /dev/null +++ b/app/Policies/ProjectPolicy.php @@ -0,0 +1,10 @@ + \App\Policies\TokenPolicy::class, \App\Models\BankAccount::class => \App\Policies\BankAccountPolicy::class, \App\Models\PaymentTerm::class => \App\Policies\PaymentTermPolicy::class, + \App\Models\Project::class => \App\Policies\ProjectPolicy::class, ]; /** diff --git a/app/Services/ProjectService.php b/app/Services/ProjectService.php new file mode 100644 index 0000000000..1b16ea52ea --- /dev/null +++ b/app/Services/ProjectService.php @@ -0,0 +1,71 @@ +projectRepo = $projectRepo; + $this->datatableService = $datatableService; + } + + /** + * @return CreditRepository + */ + protected function getRepo() + { + return $this->projectRepo; + } + + /** + * @param $data + * @return mixed|null + */ + public function save($data) + { + if (isset($data['client_id']) && $data['client_id']) { + $data['client_id'] = Client::getPrivateId($data['client_id']); + } + + return $this->projectRepo->save($data); + } + + /** + * @param $clientPublicId + * @param $search + * @return \Illuminate\Http\JsonResponse + */ + public function getDatatable($search, $userId) + { + // we don't support bulk edit and hide the client on the individual client page + $datatable = new ProjectDatatable(); + + $query = $this->projectRepo->find($search, $userId); + + return $this->datatableService->createDatatable($datatable, $query); + } +} diff --git a/database/migrations/2016_09_05_150625_create_gateway_types.php b/database/migrations/2016_09_05_150625_create_gateway_types.php index 518639da9a..1a0f5c25fd 100644 --- a/database/migrations/2016_09_05_150625_create_gateway_types.php +++ b/database/migrations/2016_09_05_150625_create_gateway_types.php @@ -54,7 +54,7 @@ class CreateGatewayTypes extends Migration }); DB::statement('SET FOREIGN_KEY_CHECKS=1;'); } - + /** * Reverse the migrations. * diff --git a/database/migrations/2016_11_28_092904_add_task_projects.php b/database/migrations/2016_11_28_092904_add_task_projects.php new file mode 100644 index 0000000000..f709d8d084 --- /dev/null +++ b/database/migrations/2016_11_28_092904_add_task_projects.php @@ -0,0 +1,106 @@ +increments('id'); + $table->unsignedInteger('user_id'); + $table->unsignedInteger('account_id')->index(); + $table->unsignedInteger('client_id')->index()->nullable(); + $table->timestamps(); + $table->softDeletes(); + + $table->string('name')->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('client_id')->references('id')->on('clients')->onDelete('cascade'); + + $table->unsignedInteger('public_id')->index(); + $table->unique( array('account_id','public_id') ); + }); + + Schema::table('tasks', function ($table) + { + $table->unsignedInteger('project_id')->nullable()->index(); + }); + + DB::statement('SET FOREIGN_KEY_CHECKS=0;'); + Schema::table('tasks', function ($table) + { + $table->foreign('project_id')->references('id')->on('projects')->onDelete('cascade'); + }); + DB::statement('SET FOREIGN_KEY_CHECKS=1;'); + + // is_deleted to standardize tables + Schema::table('expense_categories', function ($table) + { + $table->boolean('is_deleted')->default(false); + }); + + Schema::table('products', function ($table) + { + $table->boolean('is_deleted')->default(false); + }); + + // add 'delete cascase' to resolve error when deleting an account + Schema::table('account_gateway_tokens', function($table) + { + $table->dropForeign('account_gateway_tokens_default_payment_method_id_foreign'); + }); + + Schema::table('account_gateway_tokens', function($table) + { + $table->foreign('default_payment_method_id')->references('id')->on('payment_methods')->onDelete('cascade'); + }); + + Schema::table('invoices', function ($table) + { + $table->boolean('is_public')->default(false); + }); + DB::table('invoices')->update(['is_public' => true]); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('tasks', function ($table) + { + $table->dropForeign('tasks_project_id_foreign'); + $table->dropColumn('project_id'); + }); + + Schema::dropIfExists('projects'); + + Schema::table('expense_categories', function ($table) + { + $table->dropColumn('is_deleted'); + }); + + Schema::table('products', function ($table) + { + $table->dropColumn('is_deleted'); + }); + + Schema::table('invoices', function ($table) + { + $table->dropColumn('is_public'); + }); + } +} diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 9cc38a84f0..57970f7b96 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -2228,6 +2228,28 @@ $LANG = array( 'payment_status_name' => 'Status', 'client_created_at' => 'Date Created', 'postmark_error' => 'There was a problem sending the email through Postmark: :link', + 'project' => 'Project', + 'projects' => 'Projects', + 'new_project' => 'New Project', + 'edit_project' => 'Edit Project', + 'archive_project' => 'Archive Project', + 'list_projects' => 'List Projects', + 'updated_project' => 'Successfully updated project', + 'created_project' => 'Successfully created project', + 'archived_project' => 'Successfully archived project', + 'archived_projects' => 'Successfully archived :count projects', + 'restore_project' => 'Restore project', + 'restored_project' => 'Successfully restored project', + 'delete_project' => 'Delete project', + 'deleted_project' => 'Successfully deleted project', + 'deleted_projects' => 'Successfully deleted :count projects', + 'delete_expense_category' => 'Delete category', + 'deleted_expense_category' => 'Successfully deleted category', + 'delete_product' => 'Delete product', + 'deleted_product' => 'Successfully deleted product', + 'deleted_products' => 'Successfully deleted :count products', + 'restored_product' => 'Successfully restored product', + ); diff --git a/resources/views/header.blade.php b/resources/views/header.blade.php index 9fbaca07d6..9a8f2f6876 100644 --- a/resources/views/header.blade.php +++ b/resources/views/header.blade.php @@ -506,7 +506,9 @@ 'settings', //'self-update' ] as $option) - @if (in_array($option, ['dashboard', 'settings']) || Auth::user()->can('view', substr($option, 0, -1))) + @if (in_array($option, ['dashboard', 'settings']) + || Auth::user()->can('view', substr($option, 0, -1)) + || Auth::user()->can('create', substr($option, 0, -1)))
  • @if ($option == 'settings') asLinkTo('javascript:submitForm_'.$entityType.'("archive")')->appendIcon(Icon::create('trash')) !!} - @else - {!! DropdownButton::normal(trans('texts.archive'))->withContents([ - ['label' => trans('texts.archive_'.$entityType), 'url' => 'javascript:submitForm_'.$entityType.'("archive")'], - ['label' => trans('texts.delete_'.$entityType), 'url' => 'javascript:submitForm_'.$entityType.'("delete")'], - ])->withAttributes(['class'=>'archive'])->split() !!} - @endif + {!! DropdownButton::normal(trans('texts.archive'))->withContents([ + ['label' => trans('texts.archive_'.$entityType), 'url' => 'javascript:submitForm_'.$entityType.'("archive")'], + ['label' => trans('texts.delete_'.$entityType), 'url' => 'javascript:submitForm_'.$entityType.'("delete")'], + ])->withAttributes(['class'=>'archive'])->split() !!}