1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-09 20:52:56 +01:00

Added task projects

This commit is contained in:
Hillel Coren 2016-11-29 19:47:26 +02:00
parent 408aede80a
commit bbbf1237c5
41 changed files with 875 additions and 80 deletions

View File

@ -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);

View File

@ -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);

View File

@ -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)

View File

@ -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(),
];
}

View File

@ -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);
}

View File

@ -0,0 +1,113 @@
<?php namespace App\Http\Controllers;
use Auth;
use View;
use Utils;
use Input;
use Session;
use App\Models\Client;
use App\Services\ProjectService;
use App\Ninja\Repositories\ProjectRepository;
use App\Ninja\Datatables\ProjectDatatable;
use App\Http\Requests\ProjectRequest;
use App\Http\Requests\CreateProjectRequest;
use App\Http\Requests\UpdateProjectRequest;
class ProjectController extends BaseController
{
protected $projectRepo;
protected $projectService;
protected $entityType = ENTITY_PROJECT;
public function __construct(ProjectRepository $projectRepo, ProjectService $projectService)
{
$this->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');
}
}

View File

@ -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);

View File

@ -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(),
];
}

View File

@ -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)

View File

@ -0,0 +1,26 @@
<?php namespace App\Http\Requests;
class CreateProjectRequest extends ProjectRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return $this->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),
];
}
}

View File

@ -0,0 +1,7 @@
<?php namespace App\Http\Requests;
class ProjectRequest extends EntityRequest {
protected $entityType = ENTITY_PROJECT;
}

View File

@ -0,0 +1,27 @@
<?php namespace App\Http\Requests;
class UpdateProjectRequest extends ProjectRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return $this->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),
];
}
}

View File

@ -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);

View File

@ -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
*/

View File

@ -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;
}
}

View File

@ -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;
}
}

65
app/Models/Project.php Normal file
View File

@ -0,0 +1,65 @@
<?php namespace App\Models;
use Illuminate\Database\Eloquent\SoftDeletes;
use Laracasts\Presenter\PresentableTrait;
/**
* Class ExpenseCategory
*/
class Project extends EntityModel
{
// Expense Categories
use SoftDeletes;
use PresentableTrait;
/**
* @var array
*/
protected $dates = ['deleted_at'];
/**
* @var array
*/
protected $fillable = [
'name',
'client_id',
];
/**
* @var string
*/
protected $presenter = 'App\Ninja\Presenters\EntityPresenter';
/**
* @return mixed
*/
public function getEntityType()
{
return ENTITY_PROJECT;
}
/**
* @return string
*/
public function getRoute()
{
return "/projects/{$this->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();
});

View File

@ -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

View File

@ -0,0 +1,59 @@
<?php namespace App\Ninja\Datatables;
use Utils;
use URL;
use Auth;
class ProjectDatatable extends EntityDatatable
{
public $entityType = ENTITY_PROJECT;
public $sortCol = 1;
public function columns()
{
return [
[
'project',
function ($model)
{
if ( ! Auth::user()->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]);
}
],
];
}
}

View File

@ -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) {

View File

@ -187,7 +187,8 @@ class AccountRepository
ENTITY_VENDOR,
ENTITY_RECURRING_INVOICE,
ENTITY_PAYMENT,
ENTITY_CREDIT
ENTITY_CREDIT,
ENTITY_PROJECT,
];
foreach ($entityTypes as $entityType) {

View File

@ -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);

View File

@ -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);
})

View File

@ -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) {

View File

@ -0,0 +1,75 @@
<?php namespace App\Ninja\Repositories;
use DB;
use Utils;
use Auth;
use App\Models\Project;
class ProjectRepository extends BaseRepository
{
public function getClassName()
{
return 'App\Models\Project';
}
public function all()
{
return Project::scope()->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;
}
}

View File

@ -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']);
}

View File

@ -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

View File

@ -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,

View File

@ -0,0 +1,18 @@
<?php namespace App\Ninja\Transformers;
use App\Models\Project;
class ProjectTransformer extends EntityTransformer
{
public function transform(Project $project)
{
return array_merge($this->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,
]);
}
}

View File

@ -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,

View File

@ -0,0 +1,10 @@
<?php
namespace App\Policies;
use App\Models\User;
class ProjectPolicy extends EntityPolicy
{
}

View File

@ -28,6 +28,7 @@ class AuthServiceProvider extends ServiceProvider
\App\Models\AccountToken::class => \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,
];
/**

View File

@ -0,0 +1,71 @@
<?php namespace App\Services;
use Utils;
use Auth;
use App\Models\Client;
use App\Ninja\Repositories\ProjectRepository;
use App\Ninja\Datatables\ProjectDatatable;
/**
* Class ProjectService
*/
class ProjectService extends BaseService
{
/**
* @var ProjectRepository
*/
protected $projectRepo;
/**
* @var DatatableService
*/
protected $datatableService;
/**
* CreditService constructor.
*
* @param ProjectRepository $creditRepo
* @param DatatableService $datatableService
*/
public function __construct(ProjectRepository $projectRepo, DatatableService $datatableService)
{
$this->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);
}
}

View File

@ -0,0 +1,106 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddTaskProjects extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('projects', function($table)
{
$table->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');
});
}
}

View File

@ -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',
);

View File

@ -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)))
<li class="{{ Request::is("{$option}*") ? 'active' : '' }}">
@if ($option == 'settings')
<a type="button" class="btn btn-default btn-sm pull-right"

View File

@ -17,14 +17,10 @@
@endif
@endcan
@if (in_array($entityType, [ENTITY_EXPENSE_CATEGORY, ENTITY_PRODUCT]))
{!! Button::normal(trans('texts.archive'))->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
&nbsp;
<span id="statusWrapper_{{ $entityType }}" style="display:none">
@ -53,15 +49,15 @@
<input id="tableFilter_{{ $entityType }}" type="text" style="width:140px;margin-right:17px;background-color: white !important"
class="form-control pull-left" placeholder="{{ trans('texts.filter') }}" value="{{ Input::get('filter') }}"/>
@if (empty($clientId))
@if ($entityType == ENTITY_EXPENSE)
{!! Button::normal(trans('texts.categories'))->asLinkTo(URL::to('/expense_categories'))->appendIcon(Icon::create('list')) !!}
@elseif ($entityType == ENTITY_TASK)
{!! Button::normal(trans('texts.projects'))->asLinkTo(URL::to('/projects'))->appendIcon(Icon::create('list')) !!}
@endif
@if (empty($vendorId) && Auth::user()->can('create', $entityType))
@if (empty($clientId) && empty($vendorId) && Auth::user()->can('create', $entityType))
{!! Button::primary(trans("texts.new_{$entityType}"))->asLinkTo(url(Utils::pluralizeEntityType($entityType) . '/create'))->appendIcon(Icon::create('plus-sign')) !!}
@endif
@endif
</div>
@ -73,7 +69,7 @@
->setCustomValues('entityType', Utils::pluralizeEntityType($entityType))
->setCustomValues('clientId', isset($clientId) && $clientId)
->setOptions('sPaginationType', 'bootstrap')
->setOptions('aaSorting', [[$datatable->sortCol, 'desc']])
->setOptions('aaSorting', [[isset($clientId) ? ($datatable->sortCol-1) : $datatable->sortCol, 'desc']])
->render('datatable') !!}
@if ($entityType == ENTITY_PAYMENT)

View File

@ -57,7 +57,7 @@
}
if (account && ! decorator) {
decorator = account.show_currency_code ? 'code' : 'symbol';
decorator = parseInt(account.show_currency_code) ? 'code' : 'symbol';
}
return formatMoney(value, currencyId, countryId, decorator)

View File

@ -0,0 +1,73 @@
@extends('header')
@section('content')
{!! Former::open($url)
->addClass('col-md-10 col-md-offset-1 warn-on-exit')
->method($method)
->rules([
'name' => 'required',
]) !!}
@if ($project)
{!! Former::populate($project) !!}
@endif
<span style="display:none">
{!! Former::text('public_id') !!}
</span>
<div class="row">
<div class="col-md-10 col-md-offset-1">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.project') !!}</h3>
</div>
<div class="panel-body">
{!! Former::text('name') !!}
{!! Former::select('client_id')
->addOption('', '')
->label(trans('texts.client')) !!}
</div>
</div>
</div>
</div>
<center class="buttons">
{!! Button::normal(trans('texts.cancel'))->large()->asLinkTo(url('/expense_categories'))->appendIcon(Icon::create('remove-circle')) !!}
{!! Button::success(trans('texts.save'))->submit()->large()->appendIcon(Icon::create('floppy-disk')) !!}
</center>
{!! Former::close() !!}
<script>
var clients = {!! $clients !!};
$(function() {
$('#name').focus();
var $clientSelect = $('select#client_id');
for (var i=0; i<clients.length; i++) {
var client = clients[i];
var clientName = getClientDisplayName(client);
if (!clientName) {
continue;
}
$clientSelect.append(new Option(clientName, client.public_id));
}
@if ($clientPublicId)
$clientSelect.val({{ $clientPublicId }});
@endif
$clientSelect.combobox();
});
</script>
@stop

View File

@ -48,6 +48,8 @@
<div class="panel-body">
{!! Former::select('client')->addOption('', '')->addGroupClass('client-select') !!}
{!! Former::select('project_id')->addOption('', '')->addGroupClass('project-select')
->label(trans('texts.project')) !!}
{!! Former::textarea('description')->rows(3) !!}
@if ($task)
@ -208,6 +210,8 @@
}
var clients = {!! $clients !!};
var projects = {!! $projects !!};
var timeLabels = {};
@foreach (['hour', 'minute', 'second'] as $period)
timeLabels['{{ $period }}'] = '{{ trans("texts.{$period}") }}';
@ -425,22 +429,6 @@
ko.applyBindings(model);
$(function() {
var $clientSelect = $('select#client');
for (var i=0; i<clients.length; i++) {
var client = clients[i];
var clientName = getClientDisplayName(client);
if (!clientName) {
continue;
}
$clientSelect.append(new Option(clientName, client.public_id));
}
if ({{ $clientPublicId ? 'true' : 'false' }}) {
$clientSelect.val({{ $clientPublicId }});
}
$clientSelect.combobox();
@if (!$task && !$clientPublicId)
$('.client-select input.form-control').focus();
@else
@ -494,6 +482,98 @@
model.showTimeOverlaps();
showTimeDetails();
@endif
// setup clients and project comboboxes
var clientId = {{ $clientPublicId }};
var projectId = {{ $projectPublicId }};
var clientMap = {};
var projectMap = {};
var projectsForClientMap = {};
var projectsForAllClients = [];
var $clientSelect = $('select#client');
for (var i=0; i<projects.length; i++) {
var project = projects[i];
projectMap[project.public_id] = project;
var client = project.client;
if (!client) {
projectsForAllClients.push(project);
} else {
if (!projectsForClientMap.hasOwnProperty(client.public_id)) {
projectsForClientMap[client.public_id] = [];
}
projectsForClientMap[client.public_id].push(project);
}
}
for (var i=0; i<clients.length; i++) {
var client = clients[i];
clientMap[client.public_id] = client;
}
$clientSelect.append(new Option('', ''));
for (var i=0; i<clients.length; i++) {
var client = clients[i];
var clientName = getClientDisplayName(client);
if (!clientName) {
continue;
}
$clientSelect.append(new Option(clientName, client.public_id));
}
if (clientId) {
$clientSelect.val(clientId);
}
$clientSelect.combobox();
$clientSelect.on('change', function(e) {
var clientId = $('input[name=client]').val();
var projectId = $('input[name=project_id]').val();
var project = projectMap[projectId];
if (project && ((project.client && project.client.public_id == clientId) || !project.client)) {
e.preventDefault();
return;
}
setComboboxValue($('.project-select'), '', '');
$projectCombobox = $('select#project_id');
$projectCombobox.find('option').remove().end().combobox('refresh');
$projectCombobox.append(new Option('', ''));
var list = clientId ? (projectsForClientMap.hasOwnProperty(clientId) ? projectsForClientMap[clientId] : []).concat(projectsForAllClients) : projects;
for (var i=0; i<list.length; i++) {
var project = list[i];
$projectCombobox.append(new Option(project.name, project.public_id));
}
$('select#project_id').combobox('refresh');
});
var $projectSelect = $('select#project_id').on('change', function(e) {
$clientCombobox = $('select#client');
var projectId = $('input[name=project_id]').val();
if (projectId) {
var project = projectMap[projectId];
if (project.client) {
var client = clientMap[project.client.public_id];
if (client) {
project.client = client;
setComboboxValue($('.client-select'), client.public_id, getClientDisplayName(client));
}
}
} else {
$clientSelect.trigger('change');
}
});
$projectSelect.combobox();
if (projectId) {
var project = projectMap[projectId];
setComboboxValue($('.project-select'), project.public_id, project.name);
$projectSelect.trigger('change');
} else {
$clientSelect.trigger('change');
}
});
</script>