diff --git a/CHANGELOG.md b/CHANGELOG.md index 87a3bd5..1479687 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +### 31.05.2022 -> 1.2.0 +- Ajout de la fonctionnalitée d'estimations + +### 26.05.2022 -> 1.1.0 +- Correction de regression sur macro planning +- Correction de regression sur droits planning +- Ajout de fonctionnalité Label +- Ajout de fonctionalité assigner un ticket +- Rafactorisation du code ticket +- Refonte de tickets.index +- Ajout CSP report URI +- Remove external JS and CSS dependencies +- Use Vuejs runtime +- Add xss sanitizer +- Normalize Laravel mix + add cleaner +- Update npm deps +- Upgrade bootswatch theme +- ADD Secure HEADERS + ### 23.05.2022 -> 1.0.9 - Correction d'erreurs multiples - Fonctionnalité macro planning diff --git a/app/Casts/HtmlColor.php b/app/Casts/HtmlColor.php new file mode 100644 index 0000000..de6e071 --- /dev/null +++ b/app/Casts/HtmlColor.php @@ -0,0 +1,56 @@ +zeropad(dechex($value), 6)}"; + } + + /** + * Prepare the given value for storage. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @param string $key + * @param mixed $value + * @param array $attributes + * @return integer + * @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceAfterLastUsed + * @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceBeforeLastUsed + */ + public function set($model, $key, $value, $attributes) + { + return \intval(hexdec(ltrim($value, '#'))); + } + + /** + * Pad with zeros a numeric string + * + * @param string $num + * @param integer $lim + * @return string + */ + private function zeropad(string $num, int $lim): string + { + return (strlen($num) >= $lim) ? $num : $this->zeropad("0" . $num, $lim); + } +} diff --git a/app/Enums/Credit/AccountType.php b/app/Enums/Credit/AccountType.php new file mode 100644 index 0000000..9373f71 --- /dev/null +++ b/app/Enums/Credit/AccountType.php @@ -0,0 +1,38 @@ + 0, + 'seo' => 1 + ]; + } + + /** + * Enum labels + * + * @return array + */ + protected static function labels(): array + { + return [ + 'development' => trans('Jours de développement'), + 'seo' => trans('Jours de seo') + ]; + } +} diff --git a/app/Enums/Credit/BalanceOperation.php b/app/Enums/Credit/BalanceOperation.php new file mode 100644 index 0000000..5181f8a --- /dev/null +++ b/app/Enums/Credit/BalanceOperation.php @@ -0,0 +1,38 @@ + 0, + 'debit' => 1 + ]; + } + + /** + * Enum labels + * + * @return array + */ + protected static function labels(): array + { + return [ + 'credit' => trans('Créditer un compte'), + 'debit' => trans('Débiter un compte') + ]; + } +} diff --git a/app/Enums/Credit/EstimationStatus.php b/app/Enums/Credit/EstimationStatus.php new file mode 100644 index 0000000..d1769f3 --- /dev/null +++ b/app/Enums/Credit/EstimationStatus.php @@ -0,0 +1,41 @@ + 0, + 'accepted' => 1, + 'refused' => 2 + ]; + } + + /** + * Enum labels + * + * @return array + */ + protected static function labels(): array + { + return [ + 'pending' => trans('En attente'), + 'accepted' => trans('Acceptée'), + 'refused' => trans('Refusée'), + ]; + } +} diff --git a/app/Enums/Role.php b/app/Enums/Role.php index 193af5f..f7da12f 100644 --- a/app/Enums/Role.php +++ b/app/Enums/Role.php @@ -9,9 +9,13 @@ use Kwaadpepper\Enum\BaseEnumRoutable; * @method static self admin() * @method static self client() */ - class Role extends BaseEnumRoutable { + /** + * Enum values + * + * @return array + */ protected static function values(): array { return [ @@ -21,6 +25,11 @@ class Role extends BaseEnumRoutable ]; } + /** + * Enum labels + * + * @return array + */ protected static function labels(): array { return [ diff --git a/app/Enums/TicketStatus.php b/app/Enums/TicketStatus.php index 0e6b700..d764208 100644 --- a/app/Enums/TicketStatus.php +++ b/app/Enums/TicketStatus.php @@ -21,6 +21,11 @@ use Kwaadpepper\Enum\BaseEnumRoutable; */ class TicketStatus extends BaseEnumRoutable { + /** + * Enum values + * + * @return array + */ protected static function values(): array { return [ @@ -40,6 +45,11 @@ class TicketStatus extends BaseEnumRoutable ]; } + /** + * Enum labels + * + * @return array + */ protected static function labels(): array { return [ @@ -58,4 +68,40 @@ class TicketStatus extends BaseEnumRoutable 'archived' => trans('Archivé'), ]; } + + /** + * Enum classes + * + * @return array + */ + protected static function classes(): array + { + return [ + 'new' => 'text-danger', + 'estimateTodo' => 'text-info', + 'estimateValidated' => 'text-info', + 'estimateDone' => 'text-info', + 'toPlan' => 'text-info', + 'planned' => 'text-info', + 'inprogress' => 'text-dark', + 'waiting' => 'text-white bg-danger', + 'waitingValidation' => 'text-warning bg-danger', + 'processed' => trans('Traité'), + 'recette' => 'text-primary', + 'production' => 'text-success', + 'archived' => 'text-secondary', + ]; + } + + /** + * @param string $attribute + * @return integer|string + */ + public function __get(string $attribute) + { + if ($attribute === 'class') { + return $this->classes()[$this->getDefinition()]; + } + return parent::__get($attribute); + } } diff --git a/app/Enums/TicketType.php b/app/Enums/TicketType.php index b7fb171..3b8025c 100644 --- a/app/Enums/TicketType.php +++ b/app/Enums/TicketType.php @@ -12,6 +12,11 @@ use Kwaadpepper\Enum\BaseEnumRoutable; */ class TicketType extends BaseEnumRoutable { + /** + * Enum values + * + * @return array + */ protected static function values(): array { return [ @@ -22,6 +27,11 @@ class TicketType extends BaseEnumRoutable ]; } + /** + * Enum labels + * + * @return array + */ protected static function labels(): array { return [ @@ -31,4 +41,31 @@ class TicketType extends BaseEnumRoutable 'newsletter' => trans('Demande newsletter') ]; } + + /** + * Enum classes + * + * @return array + */ + protected static function classes(): array + { + return [ + 'bug' => 'bg-danger', + 'evolution' => 'bg-success', + 'br' => 'bg-primary', + 'newsletter' => 'bg-warning' + ]; + } + + /** + * @param string $attribute + * @return integer|string + */ + public function __get(string $attribute) + { + if ($attribute === 'class') { + return $this->classes()[$this->getDefinition()]; + } + return parent::__get($attribute); + } } diff --git a/app/Http/Controllers/BrandController.php b/app/Http/Controllers/BrandController.php new file mode 100644 index 0000000..39e36ea --- /dev/null +++ b/app/Http/Controllers/BrandController.php @@ -0,0 +1,51 @@ +__CrudConstructor(); + $this->authorizeResource(Brand::class, 'brand'); + } + + /** + * Crud index view + * + * @param \Kwaadpepper\CrudPolicies\Http\Requests\CrudIndexRequest $request + * @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory + */ + public function index(CrudIndexRequest $request) + { + /** @var \Illuminate\View\View */ + $view = $this->crudIndex($request); + if ($request->ajax()) { + return $view; + } + return view('brands.index', $view->getData()); + } +} diff --git a/app/Http/Controllers/Credit/AccountController.php b/app/Http/Controllers/Credit/AccountController.php new file mode 100644 index 0000000..3f537f1 --- /dev/null +++ b/app/Http/Controllers/Credit/AccountController.php @@ -0,0 +1,118 @@ +user(); + if (!$user->isAdmin()) { + // Show only users brand(s) accounts. + $brands = $user->sites()->with('brand')->get()->pluck('brand') + ->filter(function ($v) { + return !\is_null($v); + }); + return $query->whereHas('brand', function ($q) use ($brands) { + $q->whereIn('id', $brands->pluck('id')); + }); + } + return $query; + } + + /** + * Crud index view + * + * @param \Kwaadpepper\CrudPolicies\Http\Requests\CrudIndexRequest $request + * @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory + */ + public function index(CrudIndexRequest $request) + { + /** @var \Illuminate\View\View */ + $view = $this->crudIndex($request); + if ($request->ajax()) { + return $view; + } + return view('accounts.index', $view->getData()); + } + + + + /** + * Before store model middleware + * + * @param Account $model + * @return void + */ + public function storeModel(Account &$model) + { + $model->brand()->associate(\request()->brand); + } + + /** + * Credit + * + * @param \App\Models\Brand $brand + * @param \App\Models\Account $account + * @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory + * @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed + */ + public function credit(Brand $brand, Account $account) + { + return view('accounts.credit', compact('brand', 'account')); + } + + /** + * Credit Account + * + * @param \App\Http\Requests\Credit\AddCreditToAccountRequest $request + * @param \App\Models\Brand $brand + * @param \App\Models\Account $account + * @return \Illuminate\Http\RedirectResponse + * @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed + */ + public function creditSubmit(AddCreditToAccountRequest $request, Brand $brand, Account $account) + { + $log = new CreditLog(); + $log->account()->associate($account); + $log->type = BalanceOperation::credit(); + $log->amount = \strval($request->amount); + if ($log->saveOrFail()) { + return \response()->redirectToRoute('brands.credit_accounts.index', $brand) + ->with('info', trans('Le compte à bien été crédité')); + } + return back(); + } +} diff --git a/app/Http/Controllers/Credit/CreditLogController.php b/app/Http/Controllers/Credit/CreditLogController.php new file mode 100644 index 0000000..d8bedee --- /dev/null +++ b/app/Http/Controllers/Credit/CreditLogController.php @@ -0,0 +1,55 @@ +orderBy('created_at', 'DESC'); + return $query; + } + + /** + * Crud index view + * + * @param \Kwaadpepper\CrudPolicies\Http\Requests\CrudIndexRequest $request + * @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory + */ + public function index(CrudIndexRequest $request) + { + /** @var \Illuminate\View\View */ + $view = $this->crudIndex($request); + if ($request->ajax()) { + return $view; + } + return view('credit_logs.index', $view->getData()); + } +} diff --git a/app/Http/Controllers/Credit/EstimationController.php b/app/Http/Controllers/Credit/EstimationController.php new file mode 100644 index 0000000..73083cf --- /dev/null +++ b/app/Http/Controllers/Credit/EstimationController.php @@ -0,0 +1,102 @@ +authorize('estimate', $ticket); + return view('estimations.create', \compact('ticket')); + } + + /** + * Create Ticket estimation + * + * @param \App\Http\Requests\Credit\CreateEstimateRequest $request + * @param \App\Models\Ticket $ticket + * @return \Illuminate\Http\RedirectResponse + */ + public function estimateSubmit(CreateEstimateRequest $request, Ticket $ticket) + { + return DB::transaction(function () use ($request, $ticket) { + /** @var \App\Models\User */ + $user = auth()->user(); + $estimate = new Estimation($request->validated()); + $estimate->ticket()->associate($ticket); + $estimate->estimator()->associate($user); + $ticket->status = TicketStatus::waitingValidation(); + $ticket->saveQuietly(); + if ($estimate->saveOrFail()) { + $message = sprintf( + '%s a modifié le statut en %s, Une estimation de %s jours à été ajoutée', + $user->name, + $ticket->status->label, + \number_format(\floatval($request->amount), 1, ',', '') + ); + event(new TicketStatusChanged($ticket, $ticket->status, $user)); + event(new NotifySubscriptions($ticket, $message)); + event(new NotifyAdmin($ticket, $message)); + return response()->redirectToRoute('ticket.show', $ticket) + ->with('info', trans('L\'estimation à bien été ajouté')); + } + return \back(); + }); + } + + /** + * Answer ticket estimation + * + * @param \App\Http\Requests\Credit\AnswerEstimateRequest $request + * @param \App\Models\Ticket $ticket + * @return \Illuminate\Http\RedirectResponse + */ + public function estimatePendingSubmit(AnswerEstimateRequest $request, Ticket $ticket) + { + /** @var \App\Models\User */ + $user = auth()->user(); + /** @var \App\Models\Credit\Estimation */ + $estimation = $ticket->estimation; + if ($request->answer) { + $ticket->status = TicketStatus::toPlan(); + $estimation->status = EstimationStatus::accepted(); + } else { + $ticket->status = TicketStatus::archived(); + $estimation->status = EstimationStatus::refused(); + } + $estimation->validator()->associate($user); + if ($estimation->saveOrFail() and $ticket->saveOrFail()) { + $message = sprintf( + 'Une réponse à été fourni par %s pour l\'estimation du ticket n°%d : %s', + $user->name, + $ticket->id, + $estimation->status->label + ); + event(new TicketStatusChanged($ticket, $ticket->status, $user)); + event(new NotifySubscriptions($ticket, $message)); + event(new NotifyAdmin($ticket, $message)); + return response()->redirectToRoute('ticket.show', $ticket) + ->with('info', trans('Votre réponse à été enregistrée')); + } + return \back(); + } +} diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 3125a1d..b2b723f 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -7,6 +7,11 @@ use Parsedown; class HomeController extends Controller { + /** + * Show Home page + * + * @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory + */ public function home() { $Parsedown = new Parsedown(); diff --git a/app/Http/Controllers/LabelController.php b/app/Http/Controllers/LabelController.php new file mode 100644 index 0000000..490c3e7 --- /dev/null +++ b/app/Http/Controllers/LabelController.php @@ -0,0 +1,21 @@ +middleware('is-admin'); + } + public function index(Request $request) { $date = CarbonImmutable::now()->startOfWeek(); diff --git a/app/Http/Controllers/TicketController.php b/app/Http/Controllers/TicketController.php index 6a1ac4f..263cbb1 100644 --- a/app/Http/Controllers/TicketController.php +++ b/app/Http/Controllers/TicketController.php @@ -73,17 +73,23 @@ class TicketController extends Controller /** @var \App\Models\User */ $user = auth()->user(); - $tickets = Ticket::with('site')->has('site'); + $tickets = Ticket::with(['site', 'labels']); if (!$user->isAdmin()) { - $tickets = $tickets->whereHas('site', function ($q) use ($user) { + $tickets->whereHas('site', function ($q) use ($user) { $q->whereHas('users', function ($q) use ($user) { $q->whereId($user->id); }); }); - $tickets = $tickets->where('is_internal', false); + $tickets->where('is_internal', false); + } else { + $tickets->has('site'); } - $tickets = $tickets->where('status', ($request->st == TicketStatus::archived()) ? - '=' : '<>', TicketStatus::archived()); + + $tickets->where( + 'status', + ($request->st == TicketStatus::archived()) ? '=' : '<>', + TicketStatus::archived() + ); $tickets = $this->searchTicket($tickets, $request); $tickets = $this->filterTickets($tickets, $request); @@ -184,6 +190,9 @@ class TicketController extends Controller $ticket->file2 = ToolboxHelper::storeImage($ticket->getTable(), $fileTicket2); } + if ($request->labels) { + $ticket->labels()->sync($request->labels); + } $ticket->user()->associate(auth()->user()); $ticket->site()->associate($site); $ticket->saveOrFail(); @@ -254,6 +263,10 @@ class TicketController extends Controller $ticket->file2 = ToolboxHelper::storeImage($ticket->getTable(), $fileTicket2); } + if ($request->labels) { + $ticket->labels()->sync($request->labels); + } + $ticket->site()->associate($site); $ticket->saveOrFail(); diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 60ad4c2..70e4bd1 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -38,12 +38,7 @@ class Kernel extends HttpKernel \Illuminate\Routing\Middleware\SubstituteBindings::class, \App\Http\Middleware\SecureHeaders::class, 'xss' - ], - - 'api' => [ - 'throttle:api', - \Illuminate\Routing\Middleware\SubstituteBindings::class, - ], + ] ]; /** diff --git a/app/Http/Middleware/SecureHeaders.php b/app/Http/Middleware/SecureHeaders.php index 1a04dc5..15047e0 100644 --- a/app/Http/Middleware/SecureHeaders.php +++ b/app/Http/Middleware/SecureHeaders.php @@ -46,7 +46,7 @@ class SecureHeaders // phpcs:ignore Generic.Files.LineLength.TooLong "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;", // phpcs:ignore Generic.Files.LineLength.TooLong - "script-src 'self' $jsDev;" + "script-src 'self' $jsDev https://cdnjs.cloudflare.com;" ]; $response->headers->set( 'Content-Security-Policy', diff --git a/app/Http/Requests/Credit/AddCreditToAccountRequest.php b/app/Http/Requests/Credit/AddCreditToAccountRequest.php new file mode 100644 index 0000000..9b3c410 --- /dev/null +++ b/app/Http/Requests/Credit/AddCreditToAccountRequest.php @@ -0,0 +1,41 @@ +check()) { + return false; + } + /** @var \App\Models\User */ + $user = auth()->user(); + return $user->isAdmin() and $user->can('credit', $this->account); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + 'amount' => [ + 'required', 'numeric', 'gt:0', function ($attribute, $value, $fail) { + if ((\floatval($value) * 10) % 5 !== 0) { + $fail("$attribute doit être un multiple de 5 (comptez par demi journées, soit 0.5)"); + } + } + ], + ]; + } +} diff --git a/app/Http/Requests/Credit/AnswerEstimateRequest.php b/app/Http/Requests/Credit/AnswerEstimateRequest.php new file mode 100644 index 0000000..4af0210 --- /dev/null +++ b/app/Http/Requests/Credit/AnswerEstimateRequest.php @@ -0,0 +1,48 @@ +check()) { + return false; + } + /** @var \App\Models\User */ + $user = auth()->user(); + return !$user->isAdmin() and $user->can('validateEstimate', $this->ticket); + } + + /** + * Prepare the data for validation. + * + * @return void + */ + protected function prepareForValidation() + { + $this->merge([ + 'answer' => $this->status === 'accept' + ]); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + 'status' => 'required|in:accept,refuse', + 'answer' => 'required|boolean' + ]; + } +} diff --git a/app/Http/Requests/Credit/CreateEstimateRequest.php b/app/Http/Requests/Credit/CreateEstimateRequest.php new file mode 100644 index 0000000..453c293 --- /dev/null +++ b/app/Http/Requests/Credit/CreateEstimateRequest.php @@ -0,0 +1,67 @@ +check()) { + return false; + } + /** @var \App\Models\User */ + $user = auth()->user(); + return $user->isAdmin() and $user->can('estimate', $this->ticket); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + /** @var \App\Models\Ticket */ + $ticket = $this->ticket; + return [ + 'amount' => [ + 'required', 'numeric', 'gt:0', function ($attribute, $value, $fail) { + if ((\floatval($value) * 10) % 5 !== 0) { + $fail("$attribute doit être un multiple de 5 (comptez par demi journées, soit 0.5)"); + } + } + ], + 'credit_account_id' => [ + 'required', 'exists:credit_accounts,id', + function ($attribute, $value, $fail) use ($ticket) { + /** @var \App\Models\Credit\Account|null */ + $account = Account::whereId($value)->first(); + if ($account and !$ticket->accountIsValidForThisTicket($account)) { + $fail("$attribute does not belong to the ticket brand"); + } + } + ] + ]; + } + + /** + * Get custom messages for validator errors. + * + * @return array + */ + public function messages(): array + { + return [ + 'amount' => 'Montant', + 'credit_account_id' => 'Compte' + ]; + } +} diff --git a/app/Http/Requests/MacroSchedule/PostActivityRequest.php b/app/Http/Requests/MacroSchedule/PostActivityRequest.php index ec3d8de..7962cc1 100644 --- a/app/Http/Requests/MacroSchedule/PostActivityRequest.php +++ b/app/Http/Requests/MacroSchedule/PostActivityRequest.php @@ -19,7 +19,12 @@ class PostActivityRequest extends FormRequest */ public function authorize() { - return auth()->check() and $this->ajax(); + if (!auth()->check() or !$this->ajax()) { + return false; + } + /** @var \App\Models\User */ + $user = auth()->user(); + return $user->isAdmin(); } /** diff --git a/app/Http/Requests/StoreTicket.php b/app/Http/Requests/StoreTicket.php index a5bbb8b..96b2ead 100644 --- a/app/Http/Requests/StoreTicket.php +++ b/app/Http/Requests/StoreTicket.php @@ -4,6 +4,7 @@ namespace App\Http\Requests; use App\Enums\TicketStatus; use App\Enums\TicketType; +use App\Models\Ticket; use Illuminate\Foundation\Http\FormRequest; class StoreTicket extends FormRequest @@ -30,7 +31,7 @@ class StoreTicket extends FormRequest if ($user) { if (!$user->isAdmin()) { $this->merge([ - 'status' => $this->site->status->value, + 'status' => TicketStatus::new()->value, 'is_internal' => false, ]); } else { @@ -50,6 +51,8 @@ class StoreTicket extends FormRequest */ public function rules() { + /** @var \App\Models\User */ + $user = auth()->user(); return [ 'site_id' => [ 'required', @@ -93,7 +96,16 @@ class StoreTicket extends FormRequest 'text' => 'required|string|min:40', 'file_ticket' => 'mimes:jpeg,jpg,png,pdf|max:2048', 'file_ticket2' => 'mimes:jpeg,jpg,png,pdf|max:2048', - 'is_internal' => 'required|boolean' + 'is_internal' => 'required|boolean', + 'labels' => [ + // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed + 'sometimes', 'array', function ($attribute, $value, $fail) use ($user) { + if (!$user->can('assignLabels', Ticket::class)) { + $fail(trans('Vous n\'êtes pas autorisé à modifier cette valeur')); + } + } + ], + 'labels.*' => 'required|exists:labels,id' ]; } } diff --git a/app/Lib/ToolboxHelper.php b/app/Lib/ToolboxHelper.php index ab9ca09..32b48ca 100644 --- a/app/Lib/ToolboxHelper.php +++ b/app/Lib/ToolboxHelper.php @@ -3,12 +3,15 @@ namespace App\Lib; use App\Enums\LoadLevel; +use Illuminate\Database\Eloquent\Model; use Illuminate\Http\Request; use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Route; +use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\View; use Illuminate\Support\Str; +use Illuminate\Validation\Rule; use Illuminate\Validation\ValidationException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -252,4 +255,35 @@ class ToolboxHelper } return $fullPath; } + + /** + * Assert Is Unique + * + * @param \Illuminate\Database\Eloquent\Model $model + * @param array $columns + * @return void + * @throws \Illuminate\Validation\ValidationException If a model like this already exists. + */ + public static function assertIsUnique(Model $model, array $columns): void + { + $columns = collect($columns); + /** @var string|null */ + $firstColumn = $columns->pull($columns->keys()->first()); + if (!$firstColumn) { + return; + } + $modelClass = \get_class($model); + $uniqueRule = Rule::unique((new $modelClass())->getTable()); + if ($model->id) { + $uniqueRule->ignore($model->id); + } + $uniqueRule->where(function ($q) use ($model, $columns) { + $columns->map(function (string $column) use ($model, $q) { + $q->where($column, $model->{$column}); + }); + }); + Validator::make([$firstColumn => $model->{$firstColumn}], [ + $firstColumn => ['required', $uniqueRule] + ])->validate(); + } } diff --git a/app/Models/Brand.php b/app/Models/Brand.php new file mode 100644 index 0000000..8add9ef --- /dev/null +++ b/app/Models/Brand.php @@ -0,0 +1,117 @@ + [ + 'label' => trans('Nom'), + 'type' => CrudType::string(), + 'validate' => [ + 'string', + 'max:255' + ], + 'required' => true, + 'nullable' => false, + 'actions' => [ + CrudAction::viewAny(), + CrudAction::view(), + CrudAction::create(), + CrudAction::update(), + ] + ], + 'sites' => [ + 'label' => trans('models.classes.sites'), + 'type' => CrudType::hasMany(), + 'hasMany' => Site::class, + 'hasManyLabel' => 'name', + 'nullable' => true, + 'readonly' => false, + 'actions' => [ + CrudAction::viewAny(), + CrudAction::view(), + CrudAction::create(), + CrudAction::update() + ] + ], + 'accounts' => [ + 'label' => trans('models.classes.accounts'), + 'type' => CrudType::hasMany(), + 'hasMany' => Account::class, + 'hasManyLabel' => 'label', + 'nullable' => true, + 'readonly' => false, + 'actions' => [ + CrudAction::viewAny(), + CrudAction::view(), + CrudAction::create(), + CrudAction::update() + ] + ], + ]; + parent::__construct($attributes); + } + + // * RELATIONSHIPS + + /** + * HasMany Site relation + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function sites() + { + return $this->hasMany(Site::class); + } + + /** + * HasMany Account relation + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function accounts() + { + return $this->hasMany(Account::class); + } +} diff --git a/app/Models/Credit/Account.php b/app/Models/Credit/Account.php new file mode 100644 index 0000000..e5b58b1 --- /dev/null +++ b/app/Models/Credit/Account.php @@ -0,0 +1,144 @@ + AccountType::class + ]; + + /** + * Account + * + * @param array $attributes + */ + public function __construct(array $attributes = []) + { + self::$editableProperties = [ + 'label' => [ + 'label' => trans('Label'), + 'placeholder' => trans('Nom du compte'), + 'type' => CrudType::string(), + 'validate' => [ + 'string', + 'max:255' + ], + 'required' => true, + 'nullable' => false, + 'actions' => [ + CrudAction::viewAny(), + CrudAction::view(), + CrudAction::create(), + CrudAction::update(), + ] + ], + 'type' => [ + 'label' => trans('Type'), + 'type' => CrudType::enum(), + 'enum' => AccountType::class, + 'required' => true, + 'nullable' => false, + 'actions' => [ + CrudAction::viewAny(), + CrudAction::view(), + CrudAction::create(), + CrudAction::update(), + ] + ], + 'balance' => [ + 'label' => trans('Solde'), + 'type' => CrudType::float(), + 'required' => false, + 'nullable' => false, + 'readonly' => true, + 'step' => 0.5, + 'validate' => [ + 'numeric', + function ($attribute, $value, $fail) { + if (($value * 10) % 5 !== 0) { + $fail("$attribute doit être divisible par 0.5 jour/homme"); + } + }, + ], + 'actions' => [ + CrudAction::viewAny(), + CrudAction::view(), + CrudAction::create(), + CrudAction::update(), + ] + ], + 'brand' => [ + 'label' => Str::singular(trans('models.classes.brands')), + 'type' => CrudType::belongsTo(), + 'belongsTo' => Brand::class, + 'required' => false, + 'nullable' => false, + 'actions' => [ + CrudAction::viewAny() + ] + ] + ]; + parent::__construct($attributes); + } + + // * RELATIONSHIPS + + /** + * BelongsTo Brand relation + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function brand() + { + return $this->belongsTo(Brand::class); + } +} diff --git a/app/Models/Credit/CreditLog.php b/app/Models/Credit/CreditLog.php new file mode 100644 index 0000000..3f654bd --- /dev/null +++ b/app/Models/Credit/CreditLog.php @@ -0,0 +1,122 @@ + BalanceOperation::class + ]; + + /** + * Account + * + * @param array $attributes + */ + public function __construct(array $attributes = []) + { + self::$editableProperties = [ + 'amount' => [ + 'label' => trans('Montant'), + 'type' => CrudType::float(), + 'actions' => [ + CrudAction::viewAny() + ] + ], + 'type' => [ + 'label' => trans('Opération'), + 'type' => CrudType::enum(), + 'enum' => BalanceOperation::class, + 'actions' => [ + CrudAction::viewAny() + ] + ], + 'created_at' => [ + 'label' => trans('Date'), + 'type' => CrudType::datetime(), + 'actions' => [ + CrudAction::viewAny() + ] + ] + ]; + parent::__construct($attributes); + } + + /** + * Perform any actions required after the model boots. + * + * @return void + */ + protected static function booted() + { + static::creating(function (self $model) { + $model->created_at = now(); + if ($model->type->equals(BalanceOperation::debit())) { + $model->account->balance = $model->account->balance - $model->amount; + } + if ($model->type->equals(BalanceOperation::credit())) { + $model->account->balance = $model->account->balance + $model->amount; + } + $model->account->saveOrFail(); + }); + } + + // * RELATIONSHIPS + + /** + * BelongsToMany estimation relation (1 nullable) + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + public function estimation() + { + return $this->belongsToMany(Estimation::class, EstimationLog::class, 'credit_log_id', 'credit_estimation_id') + ->withTimestamps(); + } + + /** + * BelongsTo account relation + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class, 'credit_account_id'); + } +} diff --git a/app/Models/Credit/Estimation.php b/app/Models/Credit/Estimation.php new file mode 100644 index 0000000..e855e87 --- /dev/null +++ b/app/Models/Credit/Estimation.php @@ -0,0 +1,108 @@ + EstimationStatus::class + ]; + + /** + * The table associated with the model. + * + * @var string + */ + protected $table = 'credit_estimations'; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'amount', 'status', 'credit_account_id' + ]; + + /** + * Perform any actions required after the model boots. + * + * @return void + */ + protected static function booted() + { + static::updating(function (self $model) { + if (!$model->original['validator_id'] !== $model->validator_id) { + $log = new CreditLog(); + $log->account()->associate($model->account); + $log->type = BalanceOperation::debit(); + $log->amount = $model->amount; + $log->saveOrFail(); + $log->estimation()->sync([$model->id]); + } + if ($model->status->equals(EstimationStatus::pending())) { + throw ValidationException::withMessages([trans('Impossible de modifier une estimation')]); + } + }); + } + + + // * RELATIONSHIPS + + /** + * BelongsTo Ticket relation + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function ticket() + { + return $this->belongsTo(Ticket::class); + } + + /** + * BelongsTo Credit\Account relation + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class, 'credit_account_id'); + } + + /** + * BelongsTo User relation + * The use who created the estimate + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function estimator() + { + return $this->belongsTo(User::class, 'estimator_id'); + } + + /** + * BelongsTo User relation (nullable) + * The use who validated the estimate + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function validator() + { + return $this->belongsTo(User::class, 'validator_id'); + } +} diff --git a/app/Models/Credit/EstimationLog.php b/app/Models/Credit/EstimationLog.php new file mode 100644 index 0000000..d2d6c57 --- /dev/null +++ b/app/Models/Credit/EstimationLog.php @@ -0,0 +1,15 @@ + HtmlColor::class, + ]; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'name', 'color' + ]; + + /** + * Site + * + * @param array $attributes + */ + public function __construct(array $attributes = []) + { + self::$editableProperties = [ + 'name' => [ + 'name' => trans('Libellé'), + 'type' => CrudType::string(), + 'validate' => [ + 'required', + 'min:2', + 'max:255', + (new Unique('labels', 'name'))->ignore(request()->label) + ], + 'required' => true, + 'nullable' => false, + 'actions' => [ + CrudAction::viewAny(), + CrudAction::create(), + CrudAction::update(), + ] + ], + 'color' => [ + 'label' => trans('Couleur'), + 'type' => CrudType::color(), + 'validate' => ['required', new Hex()], + 'required' => true, + 'nullable' => false, + 'actions' => [ + CrudAction::viewAny(), + CrudAction::create(), + CrudAction::update(), + ] + ] + ]; + parent::__construct($attributes); + } + + /** + * Perform any actions required after the model boots. + * + * @return void + */ + protected static function booted() + { + static::creating(function (self $model) { + ToolboxHelper::assertIsUnique($model, ['name']); + }); + static::updating(function (self $model) { + ToolboxHelper::assertIsUnique($model, ['name']); + }); + } + + // * RELATIONSHIPS + + /** + * BelongsToMany Ticket relation + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + public function tickets() + { + return $this->belongsToMany(Ticket::class); + } + + /** + * BelongsTo Site relation + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function site() + { + return $this->belongsTo(Site::class); + } +} diff --git a/app/Models/Site.php b/app/Models/Site.php index dba17f9..20bbe1a 100644 --- a/app/Models/Site.php +++ b/app/Models/Site.php @@ -146,7 +146,7 @@ class Site extends Model ] ], 'projects' => [ - 'label' => trans('Projets'), + 'label' => trans('models.classes.projects'), 'type' => CrudType::belongsToMany(), 'belongsToMany' => Project::class, 'belongsToManyLabel' => 'name', @@ -174,31 +174,12 @@ class Site extends Model */ protected static function booted() { - static::created(function (Site $site) { - $path = Site::ticketAssetsFullPath($site->slug_name); - if (!File::exists($path)) { - File::makeDirectory($path, 0755, true); - } + static::created(function (self $site) { + Label::createDefaultLabelsFor($site); }); - static::updating(function (Site $site) { - $oldName = Site::where('id', $site->id)->first()->name; - $oldPath = Site::ticketAssetsFullPath($oldName); - $newPath = Site::ticketAssetsFullPath($site->slug_name); - if (File::exists($oldPath)) { - File::move($oldPath, $newPath); - } elseif (!File::exists($newPath)) { - File::makeDirectory($newPath, 0755, true); - } - }); - static::updated(function (Site $site) { + static::updated(function (self $site) { event(new SiteUpdated($site, \auth()->user())); }); - static::deleted(function (Site $site) { - $path = Site::ticketAssetsFullPath($site->slug_name); - if (File::exists($path)) { - File::deleteDirectory($path); - } - }); } // * RELATIONSHIPS @@ -224,6 +205,16 @@ class Site extends Model return $this->hasMany(Ticket::class, 'site_id'); } + /** + * BelongsTo Brand relation + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function brand() + { + return $this->belongsTo(Brand::class); + } + /** * BelongsToMany Project relation * @@ -265,14 +256,4 @@ class Site extends Model { return Str::slug($this->name); } - - public static function ticketAssetsFullPath(string $str) - { - return public_path(self::ticketAssetsPath($str)); - } - - public static function ticketAssetsPath(string $str) - { - return sprintf('fichiers/%s', $str); - } } diff --git a/app/Models/Ticket.php b/app/Models/Ticket.php index 372b5a4..d703d0f 100644 --- a/app/Models/Ticket.php +++ b/app/Models/Ticket.php @@ -10,6 +10,8 @@ use App\Events\NotifySubscriptions; use App\Events\TicketStatusChanged; use App\Jobs\SendTicketMentionMail; use App\Mail\TicketMention; +use App\Models\Credit\Account; +use App\Models\Credit\Estimation; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\ModelNotFoundException; @@ -89,6 +91,16 @@ class Ticket extends Model // * RELATIONSHIPS + /** + * HasOne Credit\Estimation relation + * + * @return \Illuminate\Database\Eloquent\Relations\HasOne + */ + public function estimation() + { + return $this->hasOne(Estimation::class); + } + /** * BelongsTo Site relation * @@ -142,6 +154,16 @@ class Ticket extends Model return $this->belongsToMany(User::class, 'subscriptions'); } + /** + * BelongsToMany Label relation + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + public function labels() + { + return $this->belongsToMany(Label::class, 'label_ticket'); + } + // * METHODS /** @@ -193,4 +215,37 @@ class Ticket extends Model }//end foreach return $usersNotified; } + + /** + * full_name getter + * + * @return string + */ + public function getFullNameAttribute(): string + { + return trans('Ticket n° :ticketId : :ticketSiteName - :ticketTypeLabel', [ + 'ticketId' => $this->id, + 'ticketSiteName' => $this->site->name, + 'ticketTypeLabel' => $this->type->label + ]); + } + + /** + * Check if an account has the same brand as this ticket. + * + * @param Account $account + * @return boolean + */ + public function accountIsValidForThisTicket(Account $account): bool + { + /** @var \App\Models\Brand|null */ + if (!($siteBrand = optional($this->site)->brand)) { + return false; + } + + /** @var \App\Models\Brand */ + $accountBrand = $account->brand; + + return !\is_null($accountBrand->id) and $accountBrand->id === $siteBrand->id; + } } diff --git a/app/Policies/AccountPolicy.php b/app/Policies/AccountPolicy.php new file mode 100644 index 0000000..882c03a --- /dev/null +++ b/app/Policies/AccountPolicy.php @@ -0,0 +1,39 @@ +policies = [ + 'account' => [ + 'viewAny' => URR::make('allowEveryOne'), + 'view' => URR::make('denyEveryOne'), + 'update' => URR::make('denyEveryOne'), + 'create' => URR::make('userIsAdmin', $user), + 'delete' => URR::make('userIsAdmin', $user), + 'credit' => URR::make('userIsAdmin', $user) + ] + ]; + } +} diff --git a/app/Policies/BrandPolicy.php b/app/Policies/BrandPolicy.php new file mode 100644 index 0000000..0c6d0e9 --- /dev/null +++ b/app/Policies/BrandPolicy.php @@ -0,0 +1,38 @@ +policies = [ + 'brand' => [ + 'viewAny' => URR::make('userIsAdmin', $user), + 'view' => URR::make('userIsAdmin', $user), + 'update' => URR::make('userIsAdmin', $user), + 'create' => URR::make('userIsAdmin', $user), + 'delete' => URR::make('userIsAdmin', $user) + ] + ]; + } +} diff --git a/app/Policies/CreditLogPolicy.php b/app/Policies/CreditLogPolicy.php new file mode 100644 index 0000000..99c90bf --- /dev/null +++ b/app/Policies/CreditLogPolicy.php @@ -0,0 +1,34 @@ +policies = [ + 'creditlog' => [ + 'viewAny' => URR::make('allowEveryOne') + ] + ]; + } +} diff --git a/app/Policies/EstimationPolicy.php b/app/Policies/EstimationPolicy.php new file mode 100644 index 0000000..05ca7c1 --- /dev/null +++ b/app/Policies/EstimationPolicy.php @@ -0,0 +1,34 @@ +policies = [ + 'estimation' => [ + 'answer' => EstimationStaticRules::make('estimateIsPending', $model) + ] + ]; + } +} diff --git a/app/Policies/LabelPolicy.php b/app/Policies/LabelPolicy.php new file mode 100644 index 0000000..7e57ccf --- /dev/null +++ b/app/Policies/LabelPolicy.php @@ -0,0 +1,37 @@ +policies = [ + 'label' => [ + 'viewAny' => URR::make('userIsAdmin', $user), + 'create' => URR::make('userIsAdmin', $user), + 'update' => URR::make('userIsAdmin', $user), + 'delete' => URR::make('userIsAdmin', $user) + ] + ]; + } +} diff --git a/app/Policies/LogPolicy.php b/app/Policies/LogPolicy.php index becb137..b2a7c67 100644 --- a/app/Policies/LogPolicy.php +++ b/app/Policies/LogPolicy.php @@ -17,11 +17,11 @@ class LogPolicy extends RootPolicy /** * Init policies on call * - * @param User $user + * @param User $user * @param Model $model * @return void + * @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed */ - // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed public function setPolicies(User $user, Model $model = null) { /** diff --git a/app/Policies/Rules/EstimationStaticRules.php b/app/Policies/Rules/EstimationStaticRules.php new file mode 100644 index 0000000..75069fc --- /dev/null +++ b/app/Policies/Rules/EstimationStaticRules.php @@ -0,0 +1,25 @@ +status->equals(EstimationStatus::pending()); + } +} diff --git a/app/Policies/Rules/LogStaticRules.php b/app/Policies/Rules/LogStaticRules.php index eafeadc..042dbbd 100644 --- a/app/Policies/Rules/LogStaticRules.php +++ b/app/Policies/Rules/LogStaticRules.php @@ -7,12 +7,19 @@ use App\Models\User; use Kwaadpepper\CrudPolicies\Policies\Rules\StaticRules; /** - * Userlevel Policiy Static Rules + * Log Policiy Static Rules * ! Rules has to return boolean value */ abstract class LogStaticRules extends StaticRules { - protected static function logIsOwnedByUser(User $user, Log $log) + /** + * Log Is owned by user. + * + * @param User $user + * @param Log $log + * @return boolean + */ + protected static function logIsOwnedByUser(User $user, Log $log): bool { return $log->user->id === $user->id; } diff --git a/app/Policies/Rules/TicketStaticRules.php b/app/Policies/Rules/TicketStaticRules.php index f9cf42a..d7269b6 100644 --- a/app/Policies/Rules/TicketStaticRules.php +++ b/app/Policies/Rules/TicketStaticRules.php @@ -7,7 +7,7 @@ use App\Models\Ticket; use Kwaadpepper\CrudPolicies\Policies\Rules\StaticRules; /** - * Userlevel Policiy Static Rules + * Ticket Policiy Static Rules * ! Rules has to return boolean value */ abstract class TicketStaticRules extends StaticRules @@ -33,4 +33,52 @@ abstract class TicketStaticRules extends StaticRules { return !$ticket->status->equals(TicketStatus::archived()); } + + /** + * Ticket has brand which has at least one account + * + * @param Ticket $ticket + * @return boolean + */ + protected static function ticketsBrandHasAnAccount(Ticket $ticket): bool + { + /** @var \App\Models\Brand|null */ + if (!($brand = $ticket->site->brand)) { + return false; + } + return (bool)$brand->accounts()->count(); + } + + /** + * Ticket does already have an estimation + * + * @param Ticket $ticket + * @return boolean + */ + protected static function ticketDoesHaveEstimation(Ticket $ticket): bool + { + return $ticket->estimation()->exists(); + } + + /** + * Ticket does not already have an estimation + * + * @param Ticket $ticket + * @return boolean + */ + protected static function ticketDoesntHaveEstimation(Ticket $ticket): bool + { + return !$ticket->estimation()->exists(); + } + + /** + * Ticket is not internal + * + * @param Ticket $ticket + * @return boolean + */ + protected static function ticketIsNotInternal(Ticket $ticket): bool + { + return !(bool)$ticket->is_internal; + } } diff --git a/app/Policies/Rules/UserRoleRules.php b/app/Policies/Rules/UserRoleRules.php index 034aca1..1258368 100644 --- a/app/Policies/Rules/UserRoleRules.php +++ b/app/Policies/Rules/UserRoleRules.php @@ -7,32 +7,60 @@ use App\Models\User; use Kwaadpepper\CrudPolicies\Policies\Rules\StaticRules; /** - * Userlevel Policiy Static Rules + * User Policiy Static Rules * ! Rules has to return boolean value */ abstract class UserRoleRules extends StaticRules { - protected static function userIsAdmin(User $user) + /** + * User is admin + * + * @param User $user + * @return boolean + */ + protected static function userIsAdmin(User $user): bool { return $user->isAdmin(); } - protected static function userIsDev(User $user) + /** + * User Is Dev + * + * @param User $user + * @return boolean + */ + protected static function userIsDev(User $user): bool { return $user->role->equals(Role::developper()); } - protected static function userIsClient(User $user) + /** + * User Is Client + * + * @param User $user + * @return boolean + */ + protected static function userIsClient(User $user): bool { return $user->role->equals(Role::client()); } - protected static function allowEveryOne() + /** + * Allow every one + * + * @return boolean + */ + protected static function allowEveryOne(): bool { return true; } - protected static function denyEveryOne() + /** + * Deny everyone + * + * @return boolean + */ + protected static function denyEveryOne(): bool { return false; } diff --git a/app/Policies/SitePolicy.php b/app/Policies/SitePolicy.php index 8e9523b..ee6a055 100644 --- a/app/Policies/SitePolicy.php +++ b/app/Policies/SitePolicy.php @@ -15,11 +15,11 @@ class SitePolicy extends RootPolicy /** * Init policies on call * - * @param User $user + * @param User $user * @param Model $model * @return void + * @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed */ - // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed public function setPolicies(User $user, Model $model = null) { /** diff --git a/app/Policies/TicketPolicy.php b/app/Policies/TicketPolicy.php index 5dbdb0c..04659d1 100644 --- a/app/Policies/TicketPolicy.php +++ b/app/Policies/TicketPolicy.php @@ -55,6 +55,22 @@ class TicketPolicy extends RootPolicy URR::make('userIsAdmin', $user), TicketStaticRules::make('ticketIsNotInProduction', $model), TicketStaticRules::make('ticketIsNotArchived', $model) + ]), + 'assignTickets' => URR::make('userIsAdmin', $user), + 'assignLabels' => URR::make('userIsAdmin', $user), + 'estimate' => StaticRules::and([ + URR::make('userIsAdmin', $user), + TicketStaticRules::make('ticketsBrandHasAnAccount', $model), + TicketStaticRules::make('ticketDoesntHaveEstimation', $model), + TicketStaticRules::make('ticketIsNotInternal', $model), + TicketStaticRules::make('ticketIsNotArchived', $model) + ]), + 'validateEstimate' => StaticRules::and([ + URR::make('userIsClient', $user), + TicketStaticRules::make('ticketsBrandHasAnAccount', $model), + TicketStaticRules::make('ticketDoesHaveEstimation', $model), + TicketStaticRules::make('ticketIsNotInternal', $model), + TicketStaticRules::make('ticketIsNotArchived', $model) ]) ] ]; diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index be41a7d..52a25c4 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -2,7 +2,10 @@ namespace App\Providers; +use App\Models\Brand; +use App\Models\Credit\Account; use App\Models\File; +use App\Models\Label; use App\Models\Log; use App\Models\MacroSchedule\Marker; use App\Models\Project; @@ -38,15 +41,13 @@ class RouteServiceProvider extends ServiceProvider $this->cspReportRoute(); $this->routes(function () { - Route::prefix('api') - ->middleware('api') - ->namespace($this->namespace) - ->group(base_path('routes/api.php')); - Route::middleware('web') ->namespace($this->namespace) ->group(base_path('routes/web.php')); }); + Route::model('account', Account::class); + Route::model('brand', Brand::class); + Route::model('label', Label::class); Route::model('marker', Marker::class); Route::model('file', File::class); Route::model('log', Log::class); diff --git a/composer.json b/composer.json index 89ce10a..bb0831b 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "ldcweb/supportweb", - "version": "1.0.9", + "version": "1.2.0", "type": "project", "description": "The Laravel Framework.", "keywords": [ @@ -15,10 +15,11 @@ "fideloper/proxy": "^4.4", "fruitcake/laravel-cors": "^2.0", "guzzlehttp/guzzle": "^7.0.1", - "kwaadpepper/crud-policies": "^0.3.6", + "kwaadpepper/crud-policies": "^0.4.0", "kwaadpepper/enum": "^2.1", "kwaadpepper/exception-handler": "^0.0.5", "laravel-lang/lang": "~8.0", + "laravel-validation-rules/colour": "^1.6", "laravel/framework": "^8.12", "laravel/tinker": "^2.5", "laravel/ui": "^3.3", diff --git a/composer.lock b/composer.lock index d3cab31..3062eb0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2a618153177d1ef96c1f71551d669f8c", + "content-hash": "22dfaf5696ddcb98ef8764d0f03a6ba0", "packages": [ { "name": "asm89/stack-cors", @@ -1505,16 +1505,16 @@ }, { "name": "kwaadpepper/crud-policies", - "version": "0.3.6", + "version": "0.4.0", "source": { "type": "git", "url": "https://github.com/Kwaadpepper/crud-policies.git", - "reference": "205b8a16c99c362fc78422eeb31c0ba618a3d351" + "reference": "91616b1788fb581a15473f435fad2c0dbdd07f4c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Kwaadpepper/crud-policies/zipball/205b8a16c99c362fc78422eeb31c0ba618a3d351", - "reference": "205b8a16c99c362fc78422eeb31c0ba618a3d351", + "url": "https://api.github.com/repos/Kwaadpepper/crud-policies/zipball/91616b1788fb581a15473f435fad2c0dbdd07f4c", + "reference": "91616b1788fb581a15473f435fad2c0dbdd07f4c", "shasum": "" }, "require": { @@ -1565,9 +1565,9 @@ ], "support": { "issues": "https://github.com/Kwaadpepper/crud-policies/issues", - "source": "https://github.com/Kwaadpepper/crud-policies/tree/0.3.6" + "source": "https://github.com/Kwaadpepper/crud-policies/tree/0.4.0" }, - "time": "2022-05-04T13:19:25+00:00" + "time": "2022-05-31T15:35:05+00:00" }, { "name": "kwaadpepper/enum", @@ -1743,6 +1743,59 @@ }, "time": "2021-04-14T17:41:45+00:00" }, + { + "name": "laravel-validation-rules/colour", + "version": "1.6.0", + "source": { + "type": "git", + "url": "https://github.com/laravel-validation-rules/colour.git", + "reference": "1e7ab2420879738f45adcf0320f8c571789e9fb4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel-validation-rules/colour/zipball/1e7ab2420879738f45adcf0320f8c571789e9fb4", + "reference": "1e7ab2420879738f45adcf0320f8c571789e9fb4", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^5.5|^6.0|^7.0|^8.0|^9.0" + }, + "require-dev": { + "orchestra/testbench": "^5.0|^7.0", + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "LVR\\Colour\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Ashley Clarke", + "email": "me@ashleyclarke.me" + }, + { + "name": "Scott Robinson", + "email": "scott@dor.ky" + } + ], + "description": "Validate colours", + "keywords": [ + "colour", + "laravel", + "validation" + ], + "support": { + "issues": "https://github.com/laravel-validation-rules/colour/issues", + "source": "https://github.com/laravel-validation-rules/colour/tree/1.6.0" + }, + "time": "2022-02-08T08:13:39+00:00" + }, { "name": "laravel/framework", "version": "v8.83.13", diff --git a/config/auth.php b/config/auth.php index 6f77186..37b0aa1 100644 --- a/config/auth.php +++ b/config/auth.php @@ -39,13 +39,7 @@ return [ 'web' => [ 'driver' => 'session', 'provider' => 'users', - ], - - 'api' => [ - 'driver' => 'token', - 'provider' => 'users', - 'hash' => false, - ], + ] ], /* diff --git a/config/crud-policies.php b/config/crud-policies.php new file mode 100644 index 0000000..b536acf --- /dev/null +++ b/config/crud-policies.php @@ -0,0 +1,50 @@ + 'crud-policies', + + /** + * You shall protect routes + * These are applied to crud routes directly as middlewares + */ + 'middlewares' => [ + 'web', + 'auth' + ], + + /** + * The public path where are stored + * crud-policies asset files + * + * Change this if you want to modify + * published assets folder under /public + ** default is 'vendor/crud-policies' + */ + 'assetPath' => 'vendor/crud-policies', + + /** + * This is the package layout that will be used + * to display crud elements + * + * You can publish views to know what sections it + * has to display + */ + 'viewLayout' => 'crud-policies::crud.layout', + + /** + * Controls whether to display flash messages + * on actions such as create, update or delete. + */ + 'displayAlerts' => true, + + /** + * Controls whether to show delete button on index routes + */ + 'deleteOnIndex' => true +]; diff --git a/database/migrations/2022_05_25_120339_create_labels_table.php b/database/migrations/2022_05_25_120339_create_labels_table.php new file mode 100644 index 0000000..94e75bb --- /dev/null +++ b/database/migrations/2022_05_25_120339_create_labels_table.php @@ -0,0 +1,44 @@ +id(); + $table->string('name')->unique()->comment('Un ou deux mot désignant le label'); + $table->unsignedInteger('color')->comment('Cast HTML color'); + }); + + $defaultLabels = [ + ['name' => 'urgent', 'color' => '#ebb734'], + ['name' => 'bloquant', 'color' => '#851400'] + ]; + collect($defaultLabels)->map(function (array $defaultLabel) { + $label = new Label(); + $label->name = $defaultLabel['name']; + $label->color = $defaultLabel['color']; + $label->saveOrFail(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('labels'); + } +} diff --git a/database/migrations/2022_05_25_121407_create_label_ticket_table.php b/database/migrations/2022_05_25_121407_create_label_ticket_table.php new file mode 100644 index 0000000..29bd99d --- /dev/null +++ b/database/migrations/2022_05_25_121407_create_label_ticket_table.php @@ -0,0 +1,35 @@ +foreignIdFor(Ticket::class) + ->constrained()->cascadeOnDelete(); + $table->foreignIdFor(Label::class) + ->constrained()->cascadeOnDelete(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('label_ticket'); + } +} diff --git a/database/migrations/2022_05_30_091333_create_brands_table.php b/database/migrations/2022_05_30_091333_create_brands_table.php new file mode 100644 index 0000000..48fa439 --- /dev/null +++ b/database/migrations/2022_05_30_091333_create_brands_table.php @@ -0,0 +1,34 @@ +id()->comment('Doit correspondre aux ids marques de la CRM'); + + $table->string('name')->comment('Nom de la marque'); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('brands'); + } +} diff --git a/database/migrations/2022_05_30_091340_add_brand_id_to_sites.php b/database/migrations/2022_05_30_091340_add_brand_id_to_sites.php new file mode 100644 index 0000000..8dfd1f2 --- /dev/null +++ b/database/migrations/2022_05_30_091340_add_brand_id_to_sites.php @@ -0,0 +1,38 @@ +foreignIdFor(Brand::class) + ->nullable() + ->comment('Relation is nullable, since not all site would be affiliate with a brand.') + ->after('todo') + ->constrained(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('sites', function (Blueprint $table) { + $table->dropForeign(['brand_id']); + $table->dropColumn('brand_id'); + }); + } +} diff --git a/database/migrations/2022_05_30_091401_create_credit_accounts_table.php b/database/migrations/2022_05_30_091401_create_credit_accounts_table.php new file mode 100644 index 0000000..d199396 --- /dev/null +++ b/database/migrations/2022_05_30_091401_create_credit_accounts_table.php @@ -0,0 +1,41 @@ +id(); + + $table->unsignedTinyInteger('type')->comment('Enum Credit\\AccountType'); + + $table->string('label')->comment('Custom account label'); + $table->float('balance')->comment('Account balance'); + + $table->foreignIdFor(Brand::class) + ->constrained(); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('credit_accounts'); + } +} diff --git a/database/migrations/2022_05_30_091412_create_credit_logs_table.php b/database/migrations/2022_05_30_091412_create_credit_logs_table.php new file mode 100644 index 0000000..b28e4c0 --- /dev/null +++ b/database/migrations/2022_05_30_091412_create_credit_logs_table.php @@ -0,0 +1,39 @@ +id(); + + $table->unsignedTinyInteger('type')->comment('Enum Credit\\BalanceOperation'); + $table->unsignedFloat('amount')->comment('Montant de l\'opération'); + + $table->foreignId('credit_account_id') + ->constrained('credit_accounts') + ->cascadeOnDelete(); + + $table->timestamp('created_at'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('credit_logs'); + } +} diff --git a/database/migrations/2022_05_30_091438_create_credit_estimations_table.php b/database/migrations/2022_05_30_091438_create_credit_estimations_table.php new file mode 100644 index 0000000..d49a6db --- /dev/null +++ b/database/migrations/2022_05_30_091438_create_credit_estimations_table.php @@ -0,0 +1,53 @@ +id(); + + $table->unsignedTinyInteger('status')->default(EstimationStatus::pending()) + ->comment('Enum Credit\\EstimationStatus'); + + $table->unsignedFloat('amount')->comment('Estimation amount'); + + $table->foreignIdFor(Ticket::class) + ->constrained(); + + $table->foreignId('credit_account_id') + ->comment('The account to debit.') + ->constrained('credit_accounts'); + $table->foreignId('estimator_id') + ->comment('The person who has done the estimate.') + ->constrained('users'); + $table->foreignId('validator_id') + ->nullable() + ->comment('The person who has validate the estimate.') + ->constrained('users'); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('credit_estimations'); + } +} diff --git a/database/migrations/2022_05_30_091453_create_credit_estimation_log_table.php b/database/migrations/2022_05_30_091453_create_credit_estimation_log_table.php new file mode 100644 index 0000000..b9758b5 --- /dev/null +++ b/database/migrations/2022_05_30_091453_create_credit_estimation_log_table.php @@ -0,0 +1,40 @@ +id(); + + $table->foreignId('credit_log_id') + ->constrained('credit_logs') + ->cascadeOnDelete(); + + $table->foreignId('credit_estimation_id') + ->constrained('credit_estimations') + ->cascadeOnDelete(); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('credit_estimation_log'); + } +} diff --git a/package-lock.json b/package-lock.json index f641f34..b1949ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,6 +6,7 @@ "": { "devDependencies": { "@babel/plugin-transform-async-to-generator": "^7.16.8", + "@fortawesome/fontawesome-free": "^6.1.1", "autoprefixer": "10.4.5", "axios": "^0.21.1", "bootstrap": "^5.1.3", @@ -1656,6 +1657,16 @@ "node": ">=10.0.0" } }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.1.1.tgz", + "integrity": "sha512-J/3yg2AIXc9wznaVqpHVX3Wa5jwKovVF0AMYSnbmcXTiL3PpRPfF58pzWucCwEiCJBp+hCNRLWClTomD8SseKg==", + "dev": true, + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", @@ -11719,6 +11730,12 @@ "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", "dev": true }, + "@fortawesome/fontawesome-free": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.1.1.tgz", + "integrity": "sha512-J/3yg2AIXc9wznaVqpHVX3Wa5jwKovVF0AMYSnbmcXTiL3PpRPfF58pzWucCwEiCJBp+hCNRLWClTomD8SseKg==", + "dev": true + }, "@jridgewell/gen-mapping": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", diff --git a/package.json b/package.json index 0bd4333..8fdc024 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ }, "devDependencies": { "@babel/plugin-transform-async-to-generator": "^7.16.8", + "@fortawesome/fontawesome-free": "^6.1.1", "autoprefixer": "10.4.5", "axios": "^0.21.1", "bootstrap": "^5.1.3", diff --git a/resources/js/app.js b/resources/js/app.js index b165fd3..b484615 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -2,3 +2,4 @@ require('./bootstrap') require('./session_watch') require('./vue') require('./files_add') +require('./label_colorize') diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js index b4c3ce6..08b8e2a 100644 --- a/resources/js/bootstrap.js +++ b/resources/js/bootstrap.js @@ -20,8 +20,12 @@ window._ = require('lodash') window.axios = require('axios') -window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; -window.axios.defaults.headers.common['X-CSRF-TOKEN'] = document.querySelector('meta[name="csrf-token"]').getAttribute('content') +window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest' +let csrfToken = document.querySelector('meta[name="csrf-token"]') +if (csrfToken.getAttribute('content')) { + window.axios.defaults.headers.common['Authorization'] = `Bearer ${csrfToken.getAttribute('content')}` + window.axios.defaults.headers.common['X-CSRF-TOKEN'] = csrfToken.getAttribute('content') +} require('fslightbox') diff --git a/resources/js/components/LabelSelectComponent.vue b/resources/js/components/LabelSelectComponent.vue new file mode 100644 index 0000000..9be286e --- /dev/null +++ b/resources/js/components/LabelSelectComponent.vue @@ -0,0 +1,33 @@ + + + diff --git a/resources/js/label_colorize.js b/resources/js/label_colorize.js new file mode 100644 index 0000000..377a6cb --- /dev/null +++ b/resources/js/label_colorize.js @@ -0,0 +1,19 @@ +import colors from './modules/colors' + +document.addEventListener('DOMContentLoaded', () => { + + // Ticket index colorize labels + if (document.querySelector('.content.ticketsIndex') || document.querySelector('.content.ticketsShow')) { + let labels = document.querySelectorAll('.label.colorize') + + labels.forEach(label => { + if (label.dataset.color) { + if (colors.methods.isrgb(label.dataset.color)) { + label.dataset.color = colors.methods.rgb2hex(label.dataset.color) + } + label.style.color = colors.methods.invertColor(label.dataset.color, true) + label.style.backgroundColor = label.dataset.color + } + }) + } +}) diff --git a/resources/js/modules/colors.js b/resources/js/modules/colors.js new file mode 100644 index 0000000..4ab1bd7 --- /dev/null +++ b/resources/js/modules/colors.js @@ -0,0 +1,40 @@ +export default { + methods: { + isrgb(rgb) { + return (String(rgb).match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/) ?? []).length !== 0 + }, + rgb2hex(rgb) { + return `#${rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/).slice(1).map( + n => parseInt(n, 10).toString(16).padStart(2, '0') + ).join('')}` + }, + invertColor(hex, bw) { + if (hex.indexOf('#') === 0) { + hex = hex.slice(1); + } + // convert 3-digit hex to 6-digits. + if (hex.length === 3) { + hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; + } + if (hex.length !== 6) { + throw new Error('Invalid HEX color.'); + } + var r = parseInt(hex.slice(0, 2), 16), + g = parseInt(hex.slice(2, 4), 16), + b = parseInt(hex.slice(4, 6), 16); + if (bw) { + // https://stackoverflow.com/a/3943023/112731 + return (r * 0.299 + g * 0.587 + b * 0.114) > 186 + ? '#000000' + : '#FFFFFF'; + } + // invert color components + r = (255 - r).toString(16); + g = (255 - g).toString(16); + b = (255 - b).toString(16); + // pad each with zeros and return + return "#" + r.padStart(1, '0') + g.padStart(1, '0') + b.padStart(1, '0') + } + }, +} + diff --git a/resources/lang/en/models.php b/resources/lang/en/models.php index 7f0e46d..a90e8fc 100644 --- a/resources/lang/en/models.php +++ b/resources/lang/en/models.php @@ -2,10 +2,15 @@ return [ 'classes' => [ + 'users' => 'Users', + 'accounts' => 'Accounts', 'projects' => 'Projects', 'markers' => 'Markers', + 'tickets' => 'Tickets', 'logs' => 'Logs', + 'creditlogs' => 'Logs', 'files' => 'Files', 'sites' => 'Sites', + 'brands' => 'Brands', ] ]; diff --git a/resources/lang/fr/models.php b/resources/lang/fr/models.php index 0acf129..c3c3b0d 100644 --- a/resources/lang/fr/models.php +++ b/resources/lang/fr/models.php @@ -2,10 +2,15 @@ return [ 'classes' => [ + 'users' => 'Utilisateurs', + 'accounts' => 'Comptes', 'projects' => 'Projets', 'markers' => 'Marqueurs', + 'tickets' => 'Tickets', 'logs' => 'Journaux', + 'creditlogs' => 'Historique', 'files' => 'Fichiers', 'sites' => 'Sites', + 'brands' => 'Marques', ] ]; diff --git a/resources/sass/bootstrap.scss b/resources/sass/bootstrap.scss index 0b4a8f7..881dfb3 100644 --- a/resources/sass/bootstrap.scss +++ b/resources/sass/bootstrap.scss @@ -1,3 +1,20 @@ @import 'variables'; @import '~bootstrap/scss/bootstrap'; @import 'bootswatch'; + +// OVERLOAD BOOTSTRAP +.btn-xs, .btn-group-xs > .btn { + padding: 0.1rem 0.4rem; + font-size: 0.8rem; + border-radius: 0.3rem; +} + + +// Tickets Labels +.ticketIndexLabels { + display: -webkit-box; + max-width: 15rem; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} diff --git a/resources/views/accounts/credit.blade.php b/resources/views/accounts/credit.blade.php new file mode 100644 index 0000000..11d3646 --- /dev/null +++ b/resources/views/accounts/credit.blade.php @@ -0,0 +1,33 @@ +@extends('layout.app') + +@section('bodyClass', 'col-md-8') + +@section('content') +
+
+

+ + {{ $brand->name }} : + {{ $account->label }}  +  {{ __('Créditer un compte') }} +

+
+
+
+ @csrf + @method('PUT') +
+ + + @include('modules.input-error', [ + 'inputName' => 'amount', 'helperId' => 'amountHelper' , 'helper' => __('Le montant en homme jour à créditer') + ]) +
+ +
+ +
+
+
+
+@endsection diff --git a/resources/views/accounts/index.blade.php b/resources/views/accounts/index.blade.php new file mode 100644 index 0000000..36ee7e3 --- /dev/null +++ b/resources/views/accounts/index.blade.php @@ -0,0 +1,121 @@ +@extends($viewLayout) + +@section('title', sprintf('%s - %s', transFb(sprintf('models.classes.%s', strtolower(Str::plural(class_basename($modelClass)))), Str::plural(class_basename($modelClass))), __('crud-policies::crud.viewAny'))) +@section('description', sprintf('%s %s', __('crud-policies::crud.viewAny'), trans(Str::plural(class_basename($modelClass))))) +@section('metaIndex', 'noindex,nofollow') + +@section('content') +
+ @include('crud-policies::crud.modules.flashMessage') +
+ @include('crud-policies::crud.modules.breadcrumb', ['action' => CrudAction::viewAny()]) + @include('crud-policies::crud.modules.actionbar.bar', ['action' => CrudAction::viewAny()]) +
+
+
+
+ {{ $models->links() }} +
+
+
+
+ + + + {{ __('crud-policies::crud.reset') }} + +
+
+
+
+ + + @include('crud-policies::crud.modules.sorter') + + + @if(count($models)) + + {{-- INIT QUERIES for OPTIMIZING --}} + @foreach ($modelClass::getEditableProperties() as $fieldName => $prop) + @if($prop['type']->equals(CrudType::order())) + @php ${"$modelTable$fieldName"} = $modelClass::max($fieldName); @endphp + @endif + @if($prop['type']->equals(CrudType::belongsTo())) + @php try { $models->load(Str::singular((new $prop['belongsTo']())->getTable())); } catch(\Exception $e) {} @endphp + @endif + @if($prop['type']->equals(CrudType::belongsToMany())) + @php $models->load("$fieldName"); @endphp + @endif + @if($prop['type']->equals(CrudType::hasMany())) + @php $models->load("$fieldName"); @endphp + @endif + @endforeach + + {{-- DISPLAY MODELS --}} + @foreach($models as $model) + + @foreach ($modelClass::getEditableProperties() as $fieldName => $prop) + @if(in_array(CrudAction::viewAny(), $prop['actions'])) + @switch($prop['type']) + @case(CrudType::password()) + {{-- Ignore --}} + @break + @default + + @endswitch + @endif + @endforeach + + {{-- MODEL ACTIONS --}} + + + @endforeach + @else + + + + @endif + +
@include('crud-policies::crud.printfield', [ + 'action' => CrudAction::viewAny(), + 'field' => $model->{$fieldName}, + 'props' => $prop, + "$modelTable$fieldName" => ${"$modelTable$fieldName"} ?? null + ]) +
+ @can('viewAny', \App\Models\Credit\CreditLog::class) + @if(is_object(request()->brand) and get_class(request()->brand) === App\Models\Brand::class) + {{ __('Historique') }} + @else + {{ __('Historique') }} + @endif + @endcan + @can('credit', $model) + @if(is_object(request()->brand) and get_class(request()->brand) === App\Models\Brand::class) + {{ __('Créditer') }} + @endif + @endcan + @can('view', $model) + {{ __('crud-policies::crud.view') }} + @endcan + @can('update', $model) + {{ __('crud-policies::crud.edit') }} + @endcan + @can('delete', $model) +
+ @csrf + @method('DELETE') + +
+ @endcan +
+
+ {{ __('crud-policies::crud.nothingtoshow') }} +
+ {{ $models->links() }} +
+
+@include('crud-policies::crud.modules.scriptConstants') +@endsection diff --git a/resources/views/brands/index.blade.php b/resources/views/brands/index.blade.php new file mode 100644 index 0000000..9fb5b29 --- /dev/null +++ b/resources/views/brands/index.blade.php @@ -0,0 +1,112 @@ +@extends($viewLayout) + +@section('title', sprintf('%s - %s', transFb(sprintf('models.classes.%s', strtolower(Str::plural(class_basename($modelClass)))), Str::plural(class_basename($modelClass))), __('crud-policies::crud.viewAny'))) +@section('description', sprintf('%s %s', __('crud-policies::crud.viewAny'), trans(Str::plural(class_basename($modelClass))))) +@section('metaIndex', 'noindex,nofollow') + +@section('content') +
+ @include('crud-policies::crud.modules.flashMessage') +
+ @include('crud-policies::crud.modules.breadcrumb', ['action' => CrudAction::viewAny()]) + @include('crud-policies::crud.modules.actionbar.bar', ['action' => CrudAction::viewAny()]) +
+
+
+
+ {{ $models->links() }} +
+
+
+
+ + + + {{ __('crud-policies::crud.reset') }} + +
+
+
+
+ + + @include('crud-policies::crud.modules.sorter') + + + @if(count($models)) + + {{-- INIT QUERIES for OPTIMIZING --}} + @foreach ($modelClass::getEditableProperties() as $fieldName => $prop) + @if($prop['type']->equals(CrudType::order())) + @php ${"$modelTable$fieldName"} = $modelClass::max($fieldName); @endphp + @endif + @if($prop['type']->equals(CrudType::belongsTo())) + @php try { $models->load(Str::singular((new $prop['belongsTo']())->getTable())); } catch(\Exception $e) {} @endphp + @endif + @if($prop['type']->equals(CrudType::belongsToMany())) + @php $models->load("$fieldName"); @endphp + @endif + @if($prop['type']->equals(CrudType::hasMany())) + @php $models->load("$fieldName"); @endphp + @endif + @endforeach + + {{-- DISPLAY MODELS --}} + @foreach($models as $model) + + @foreach ($modelClass::getEditableProperties() as $fieldName => $prop) + @if(in_array(CrudAction::viewAny(), $prop['actions'])) + @switch($prop['type']) + @case(CrudType::password()) + {{-- Ignore --}} + @break + @default + + @endswitch + @endif + @endforeach + + {{-- MODEL ACTIONS --}} + + + @endforeach + @else + + + + @endif + +
@include('crud-policies::crud.printfield', [ + 'action' => CrudAction::viewAny(), + 'field' => $model->{$fieldName}, + 'props' => $prop, + "$modelTable$fieldName" => ${"$modelTable$fieldName"} ?? null + ]) +
+ @can('viewAny', \App\Models\Credit\Account::class) + {{ __('models.classes.accounts') }} + @endcan + @can('view', $model) + {{ __('crud-policies::crud.view') }} + @endcan + @can('update', $model) + {{ __('crud-policies::crud.edit') }} + @endcan + @can('delete', $model) +
+ @csrf + @method('DELETE') + +
+ @endcan +
+
+ {{ __('crud-policies::crud.nothingtoshow') }} +
+ {{ $models->links() }} +
+
+@include('crud-policies::crud.modules.scriptConstants') +@endsection diff --git a/resources/views/credit_logs/index.blade.php b/resources/views/credit_logs/index.blade.php new file mode 100644 index 0000000..b9a7385 --- /dev/null +++ b/resources/views/credit_logs/index.blade.php @@ -0,0 +1,117 @@ +@extends($viewLayout) + +@section('title', sprintf('%s - %s', transFb(sprintf('models.classes.%s', strtolower(Str::plural(class_basename($modelClass)))), Str::plural(class_basename($modelClass))), __('crud-policies::crud.viewAny'))) +@section('description', sprintf('%s %s', __('crud-policies::crud.viewAny'), trans(Str::plural(class_basename($modelClass))))) +@section('metaIndex', 'noindex,nofollow') + +@section('content') +
+ @include('crud-policies::crud.modules.flashMessage') +
+ @include('crud-policies::crud.modules.breadcrumb', ['action' => CrudAction::viewAny()]) + @include('crud-policies::crud.modules.actionbar.bar', ['action' => CrudAction::viewAny()]) +
+
+
+
+ {{ $models->links() }} +
+
+
+
+ + + + {{ __('crud-policies::crud.reset') }} + +
+
+
+
+ + + @include('crud-policies::crud.modules.sorter') + + + @if(count($models)) + + {{-- INIT QUERIES for OPTIMIZING --}} + @foreach ($modelClass::getEditableProperties() as $fieldName => $prop) + @if($prop['type']->equals(CrudType::order())) + @php ${"$modelTable$fieldName"} = $modelClass::max($fieldName); @endphp + @endif + @if($prop['type']->equals(CrudType::belongsTo())) + @php try { $models->load(Str::singular((new $prop['belongsTo']())->getTable())); } catch(\Exception $e) {} @endphp + @endif + @if($prop['type']->equals(CrudType::belongsToMany())) + @php $models->load("$fieldName"); @endphp + @endif + @if($prop['type']->equals(CrudType::hasMany())) + @php $models->load("$fieldName"); @endphp + @endif + @endforeach + + {{-- DISPLAY MODELS --}} + @foreach($models as $model) + + @foreach ($modelClass::getEditableProperties() as $fieldName => $prop) + @if(in_array(CrudAction::viewAny(), $prop['actions'])) + @if($fieldName === 'amount') + + @continue + @endif + @if($fieldName === 'type') + + @continue + @endif + @switch($prop['type']) + @case(CrudType::password()) + {{-- Ignore --}} + @break + @default + + @endswitch + @endif + @endforeach + + {{-- MODEL ACTIONS --}} + + + @endforeach + @else + + + + @endif + +
{{ __(':amount jour(s)/homme', ['amount' => number_format($model->{$fieldName}, 1, ',', '')]) }}{{ $model->{$fieldName}->label }}@include('crud-policies::crud.printfield', [ + 'action' => CrudAction::viewAny(), + 'field' => $model->{$fieldName}, + 'props' => $prop, + "$modelTable$fieldName" => ${"$modelTable$fieldName"} ?? null + ]) +
+ @can('view', $model) + {{ __('crud-policies::crud.view') }} + @endcan + @can('update', $model) + {{ __('crud-policies::crud.edit') }} + @endcan + @can('delete', $model) +
+ @csrf + @method('DELETE') + +
+ @endcan +
+
+ {{ __('crud-policies::crud.nothingtoshow') }} +
+ {{ $models->links() }} +
+
+@include('crud-policies::crud.modules.scriptConstants') +@endsection diff --git a/resources/views/estimations/create.blade.php b/resources/views/estimations/create.blade.php new file mode 100644 index 0000000..912a009 --- /dev/null +++ b/resources/views/estimations/create.blade.php @@ -0,0 +1,25 @@ +@extends('layout.app') + +@section('bodyClass', 'col-md-8') + +@section('content') +
+
+

+ + {{ $ticket->full_name }} + @if ($ticket->is_internal)@endif +  {{ __('Créer une estimation') }} +

+
+
+
+ @csrf + @include('estimations.form-inputs') +
+ +
+
+
+
+@endsection diff --git a/resources/views/estimations/form-inputs.blade.php b/resources/views/estimations/form-inputs.blade.php new file mode 100644 index 0000000..3b9e639 --- /dev/null +++ b/resources/views/estimations/form-inputs.blade.php @@ -0,0 +1,19 @@ +
+ + + @include('modules.input-error', [ + 'inputName' => 'amount', 'helperId' => 'amountHelper' , 'helper' => __('Le montant en homme jour estimé.') + ]) +
+
+ + + @include('modules.input-error', [ + 'inputName' => 'brand_id', 'helperId' => 'creditAccountHelp' , 'helper' => __('Le compte de la marque à débiter') + ]) +
diff --git a/resources/views/estimations/pendingCard.blade.php b/resources/views/estimations/pendingCard.blade.php new file mode 100644 index 0000000..b20586d --- /dev/null +++ b/resources/views/estimations/pendingCard.blade.php @@ -0,0 +1,45 @@ +
+
+ {{ __('Estimation faite par :user', ['user' => $estimation->estimator->name]) }} +
+
+
+
+
+ {{ __('Temps estimé :time jours/homme', ['time' => $estimation->amount]) }} +
+ @if($estimation->status->equals(App\Enums\Credit\EstimationStatus::pending())) +
+ {{ __('En attente') }} +
+ @endif + @if($estimation->status->equals(App\Enums\Credit\EstimationStatus::accepted())) +
+ {{ __('Validé par :user', ['user' => optional($estimation->validator)->name ?? 'personne supprimée']) }} +
+ @endif + @if($estimation->status->equals(App\Enums\Credit\EstimationStatus::refused())) +
+ {{ __('Refusé par :user', ['user' => optional($estimation->validator)->name ?? 'personne supprimée']) }} +
+ @endif + @can('validateEstimate', $ticket) + @can('answer', $estimation) +
+
+ @csrf + + +
+
+ @csrf + + +
+
+ @endcan + @endcan +
+
+
+
diff --git a/resources/views/layout/app.blade.php b/resources/views/layout/app.blade.php index 31be5ac..329ed82 100644 --- a/resources/views/layout/app.blade.php +++ b/resources/views/layout/app.blade.php @@ -18,8 +18,9 @@ - - + + + @crudPoliciesCSS @stack('customCss') @@ -40,7 +41,8 @@ @include('layout.footer') @stack('scriptsConstants') - + + @stack('scripts') @crudPoliciesJS() @auth diff --git a/resources/views/layout/nav.blade.php b/resources/views/layout/nav.blade.php index 9ea5527..121b43e 100644 --- a/resources/views/layout/nav.blade.php +++ b/resources/views/layout/nav.blade.php @@ -11,19 +11,47 @@ @auth