From b2befb0a2197fd6419014a33a36f5ba02eea00be Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Thu, 30 Apr 2026 15:42:04 +0200 Subject: [PATCH] feat: :sparkles: add invoice packages crud #118 --- .../Back/InvoicePackageController.php | 245 ++++++++++++++++++ .../Middlewares/HandleInertiaRequests.php | 10 +- .../StoreInvoicePackageRequest.php | 51 ++++ .../UpdateInvoicePackageRequest.php | 21 ++ app/Models/InvoicePackage.php | 61 +++++ app/Models/Notice.php | 36 --- app/Policies/InvoicePackagePolicy.php | 112 ++++++++ app/Policies/NoticePolicy.php | 18 +- database/factories/InvoicePackageFactory.php | 33 +++ ...0_145058_create_invoice_packages_table.php | 36 +++ .../seeders/Classic/InvoicePackageSeeder.php | 25 ++ lang/fr/models.php | 3 +- lang/fr/validation.php | 3 + .../Layout/Sidebar/Links/InvoicePackages.tsx | 73 ++++++ .../ts/back/Pages/InvoicePackages/Create.tsx | 63 +++++ .../ts/back/Pages/InvoicePackages/Edit.tsx | 71 +++++ .../ts/back/Pages/InvoicePackages/Form.tsx | 125 +++++++++ .../ts/back/Pages/InvoicePackages/Index.tsx | 215 +++++++++++++++ .../ts/back/Pages/InvoicePackages/Show.tsx | 161 ++++++++++++ resources/ts/back/types/project.d.ts | 6 + routes/back/invoice-packages.php | 14 + routes/breadcrumbs/invoice-packages.php | 39 +++ 22 files changed, 1373 insertions(+), 48 deletions(-) create mode 100644 app/Http/Controllers/Back/InvoicePackageController.php create mode 100644 app/Http/Requests/Back/InvoicePackages/StoreInvoicePackageRequest.php create mode 100644 app/Http/Requests/Back/InvoicePackages/UpdateInvoicePackageRequest.php create mode 100644 app/Models/InvoicePackage.php create mode 100644 app/Policies/InvoicePackagePolicy.php create mode 100644 database/factories/InvoicePackageFactory.php create mode 100644 database/migrations/2026_04_30_145058_create_invoice_packages_table.php create mode 100644 database/seeders/Classic/InvoicePackageSeeder.php create mode 100644 resources/ts/back/Components/Layout/Sidebar/Links/InvoicePackages.tsx create mode 100644 resources/ts/back/Pages/InvoicePackages/Create.tsx create mode 100644 resources/ts/back/Pages/InvoicePackages/Edit.tsx create mode 100644 resources/ts/back/Pages/InvoicePackages/Form.tsx create mode 100644 resources/ts/back/Pages/InvoicePackages/Index.tsx create mode 100644 resources/ts/back/Pages/InvoicePackages/Show.tsx create mode 100644 routes/back/invoice-packages.php create mode 100644 routes/breadcrumbs/invoice-packages.php diff --git a/app/Http/Controllers/Back/InvoicePackageController.php b/app/Http/Controllers/Back/InvoicePackageController.php new file mode 100644 index 0000000..8a42611 --- /dev/null +++ b/app/Http/Controllers/Back/InvoicePackageController.php @@ -0,0 +1,245 @@ +authorizeResource(InvoicePackage::class); + $this->modelName = InvoicePackage::modelName(); + $this->modelPage = InvoicePackage::modelPage(); + } + + /** + * Display a listing of the resource. + * + * @param \Illuminate\Http\Request $request + * @return \Inertia\Response + */ + public function index(Request $request): \Inertia\Response + { + /** @var \Illuminate\Database\Eloquent\Builder<\App\Models\InvoicePackage> $query */ + $query = InvoicePackage::query(); + + if ($request->routeIs("back.$this->modelName.trash")) { + $query = $query->onlyTrashed(); + } + + /** @var string $search Search field */ + $search = $request->search; + if ($search) { + $this->searchQuery( + $query, + $search, + null, + // TODO: Fields here. + ); + } + $searchFields = \implode(', ', [ + // TODO: Translations here. + ]); + + /** Sort columns with a query */ + $this->sortQuery($query); + + /** Custom pagination */ + $models = $this->paginate($query); + + return $this->inertiaRender("back/Pages/$this->modelPage/Index", [ + 'models' => CommonResource::collection($models), + 'search' => $search, + 'searchFields' => $searchFields, + ]); + } + + /** + * Show the form for creating a new resource. + * + * @param \App\Models\InvoicePackage $invoice_package + * @return \Inertia\Response + */ + public function create(InvoicePackage $invoice_package): \Inertia\Response + { + return $this->inertiaRender("back/Pages/$this->modelPage/Create", [ + 'data' => CommonResource::make($invoice_package), + ]); + } + + /** + * Show a specific resource. + * + * @param \App\Models\InvoicePackage $invoice_package + * @return \Inertia\Response + */ + public function show(InvoicePackage $invoice_package): \Inertia\Response + { + return $this->inertiaRender("back/Pages/$this->modelPage/Show", [ + 'data' => CommonResource::make($invoice_package), + ]); + } + + /** + * Store a newly created resource. + * + * @param \App\Http\Requests\Back\InvoicePackages\StoreInvoicePackageRequest $request + * @return \Illuminate\Http\RedirectResponse + */ + public function store(StoreInvoicePackageRequest $request): \Illuminate\Http\RedirectResponse + { + return DB::transaction(function () use ($request) { + $invoice_package = new InvoicePackage(); + $invoice_package->inertiaFill($request->validated()); + + if ($invoice_package->saveOrFail()) { + return redirect()->route("back.$this->modelName.edit", $invoice_package) + ->with('success', trans('uwil-backend::crud.messages.has_been_created', [ + 'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)), + ])); + } + return redirect()->back() + ->with('error', trans('uwil-backend::crud.messages.cannot_be_created', [ + 'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)), + ])); + }); + } + + /** + * Show the form for editing the specified resource. + * + * @param \App\Models\InvoicePackage $invoice_package + * @return \Inertia\Response + */ + public function edit(InvoicePackage $invoice_package): \Inertia\Response + { + return $this->inertiaRender("back/Pages/$this->modelPage/Edit", [ + 'data' => CommonResource::make($invoice_package), + ]); + } + + /** + * Update the specified resource. + * + * @param \App\Http\Requests\Back\InvoicePackages\UpdateInvoicePackageRequest $request + * @param \App\Models\InvoicePackage $invoice_package + * @return \Illuminate\Http\RedirectResponse + */ + public function update( + UpdateInvoicePackageRequest $request, + InvoicePackage $invoice_package + ): \Illuminate\Http\RedirectResponse { + return DB::transaction(function () use ($request, $invoice_package) { + $invoice_package->inertiaFill($request->validated()); + + if ($invoice_package->saveOrFail()) { + return redirect()->route("back.$this->modelName.edit", $invoice_package) + ->with('success', trans('uwil-backend::crud.messages.has_been_updated', [ + 'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)), + ])); + } + return redirect()->route("back.$this->modelName.edit", $invoice_package) + ->with('error', trans('uwil-backend::crud.messages.cannot_be_updated', [ + 'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)), + ])); + }); + } + + /** + * Duplicate the specified resource. + * + * @param \App\Models\InvoicePackage $invoice_package + * @return \Inertia\Response + */ + public function duplicate(InvoicePackage $invoice_package): \Inertia\Response + { + return $this->create($invoice_package->replicate()); + } + + /** + * Restore the specified resource. + * + * @param integer $invoice_package + * @return \Illuminate\Http\RedirectResponse + */ + public function restore(int $invoice_package): \Illuminate\Http\RedirectResponse + { + $invoice_package = InvoicePackage::withTrashed()->findOrFail($invoice_package); + /** @var array */ + $previousQueries = []; + \parse_str(\parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries); + if ($invoice_package->restore()) { + return redirect()->route("back.$this->modelName.trash", $previousQueries) + ->with('success', trans('uwil-backend::crud.messages.has_been_restored', [ + 'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)), + ])); + } + return redirect()->back() + ->with('error', trans('uwil-backend::crud.messages.cannot_be_restored', [ + 'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)), + ])); + } + + /** + * Destroy the specified resource. + * + * @param \App\Models\InvoicePackage $invoice_package + * @return \Illuminate\Http\RedirectResponse + */ + public function destroy(InvoicePackage $invoice_package): \Illuminate\Http\RedirectResponse + { + /** @var array */ + $previousQueries = []; + \parse_str(\parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries); + if ($invoice_package->delete()) { + return redirect()->route("back.$this->modelName.index", $previousQueries) + ->with('success', trans('uwil-backend::crud.messages.has_been_deleted', [ + 'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)), + ])); + } + return redirect()->back() + ->with('error', trans('uwil-backend::crud.messages.cannot_be_deleted', [ + 'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)), + ])); + } + + /** + * Destroy the specified resource. + * + * @param \App\Models\InvoicePackage|integer $invoice_package + * @return \Illuminate\Http\RedirectResponse + */ + public function forceDestroy(InvoicePackage|int $invoice_package): \Illuminate\Http\RedirectResponse + { + if (is_int($invoice_package)) { + $invoice_package = InvoicePackage::withTrashed()->findOrFail($invoice_package); + } + /** @var array */ + $previousQueries = []; + \parse_str(\parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries); + if ($invoice_package->forceDelete()) { + return redirect()->route("back.$this->modelName.trash", $previousQueries) + ->with('success', trans('uwil-backend::crud.messages.has_been_deleted', [ + 'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)), + ])); + } + return redirect()->back() + ->with('error', trans('uwil-backend::crud.messages.cannot_be_deleted', [ + 'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)), + ])); + } +} diff --git a/app/Http/Middlewares/HandleInertiaRequests.php b/app/Http/Middlewares/HandleInertiaRequests.php index 783d49a..1aa680f 100644 --- a/app/Http/Middlewares/HandleInertiaRequests.php +++ b/app/Http/Middlewares/HandleInertiaRequests.php @@ -3,6 +3,7 @@ namespace App\Http\Middlewares; use App\Enums\NoticeTypeEnum; +use App\Models\InvoicePackage; use App\Models\Notice; use Illuminate\Http\Request; use Illuminate\Support\Facades\Gate; @@ -28,8 +29,13 @@ class HandleInertiaRequests extends Middleware 'viewAny' => Gate::allows('viewAny', Notice::class), 'viewTrash' => Gate::allows('viewTrash', Notice::class), 'create' => Gate::allows('create', Notice::class), - ] - ] + ], + 'invoicePackages' => [ + 'viewAny' => Gate::allows('viewAny', InvoicePackage::class), + 'viewTrash' => Gate::allows('viewTrash', InvoicePackage::class), + 'create' => Gate::allows('create', InvoicePackage::class), + ], + ], ], 'noticeTypeEnum' => NoticeTypeEnum::toArray(), ]; diff --git a/app/Http/Requests/Back/InvoicePackages/StoreInvoicePackageRequest.php b/app/Http/Requests/Back/InvoicePackages/StoreInvoicePackageRequest.php new file mode 100644 index 0000000..c546d09 --- /dev/null +++ b/app/Http/Requests/Back/InvoicePackages/StoreInvoicePackageRequest.php @@ -0,0 +1,51 @@ + + */ + public function rules(): array + { + return [ + 'name' => 'required|string|between:3,255', + 'amount' => 'required|numeric|decimal:0,2|between:1,10000', + 'published' => 'required|boolean', + ]; + } + + /** + * Get custom attributes for validator errors. + * + * @return array + */ + public function attributes(): array + { + return [ + 'name' => trans('validation.custom.name'), + 'amount' => trans('validation.custom.amount'), + 'published' => trans('validation.custom.published'), + ]; + } +} diff --git a/app/Http/Requests/Back/InvoicePackages/UpdateInvoicePackageRequest.php b/app/Http/Requests/Back/InvoicePackages/UpdateInvoicePackageRequest.php new file mode 100644 index 0000000..0f4b13e --- /dev/null +++ b/app/Http/Requests/Back/InvoicePackages/UpdateInvoicePackageRequest.php @@ -0,0 +1,21 @@ +route('invoice_package')); + } +} diff --git a/app/Models/InvoicePackage.php b/app/Models/InvoicePackage.php new file mode 100644 index 0000000..432b5de --- /dev/null +++ b/app/Models/InvoicePackage.php @@ -0,0 +1,61 @@ + + */ + protected $fillable = [ + 'name', + 'amount', + 'published', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'published' => 'boolean', + ]; + + /** + * The translatable columns. + * + * @var list + */ + public $translatable = [ + 'name', + ]; +} diff --git a/app/Models/Notice.php b/app/Models/Notice.php index a3c121b..dba2d71 100644 --- a/app/Models/Notice.php +++ b/app/Models/Notice.php @@ -60,40 +60,4 @@ class Notice extends Model 'title', 'message', ]; - - /** - * Relations should be included in the cache. - * - * @var list - */ - protected static $cacheRelations = [ - // TODO: Relations here. - ]; - - /** - * Perform any actions required after the model boots. - * - * @return void - */ - protected static function booted(): void - { - static::creating(function (self $model) { - // TODO: Methods here. - }); - static::updating(function (self $model) { - // TODO: Methods here. - }); - static::updated(function (self $model) { - // TODO: Methods here. - }); - static::restoring(function (self $model) { - // TODO: Methods here. - }); - static::deleted(function (self $model) { - // TODO: Methods here. - }); - static::forceDeleted(function (self $model) { - // TODO: Methods here. - }); - } } diff --git a/app/Policies/InvoicePackagePolicy.php b/app/Policies/InvoicePackagePolicy.php new file mode 100644 index 0000000..6f0fb12 --- /dev/null +++ b/app/Policies/InvoicePackagePolicy.php @@ -0,0 +1,112 @@ +value); + } + + /** + * Determine whether the user can view trash models. + * + * @param \Illuminate\Database\Eloquent\Model $authModel + * @return boolean + */ + public function viewTrash(Model $authModel): bool + { + return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('developper')->value); + } + + /** + * Determine whether the user can view the model. + * + * @param \Illuminate\Database\Eloquent\Model $authModel + * @return boolean + */ + public function view(Model $authModel): bool + { + return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('developper')->value); + } + + /** + * Determine whether the user can create models. + * + * @param \Illuminate\Database\Eloquent\Model $authModel + * @return boolean + */ + public function create(Model $authModel): bool + { + return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('developper')->value); + } + + /** + * Determine whether the user can update the model. + * + * @param \Illuminate\Database\Eloquent\Model $authModel + * @return boolean + */ + public function update(Model $authModel): bool + { + return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('developper')->value); + } + + /** + * Determine whether the user can duplicate the model. + * + * @param \Illuminate\Database\Eloquent\Model $authModel + * @return boolean + */ + public function duplicate(Model $authModel): bool + { + return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('developper')->value); + } + + /** + * Determine whether the user can restore the model. + * + * @param \Illuminate\Database\Eloquent\Model $authModel + * @return boolean + */ + public function restore(Model $authModel): bool + { + return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('developper')->value); + } + + /** + * Determine whether the user can soft delete the model. + * + * @param \Illuminate\Database\Eloquent\Model $authModel + * @return boolean + */ + public function delete(Model $authModel): bool + { + return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('developper')->value); + } + + /** + * Determine whether the user can permanently delete the model. + * + * @param \Illuminate\Database\Eloquent\Model $authModel + * @return boolean + */ + public function forceDelete(Model $authModel): bool + { + return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('developper')->value); + } +} diff --git a/app/Policies/NoticePolicy.php b/app/Policies/NoticePolicy.php index 75a6df6..6437626 100644 --- a/app/Policies/NoticePolicy.php +++ b/app/Policies/NoticePolicy.php @@ -19,7 +19,7 @@ class NoticePolicy */ public function viewAny(Model $authModel): bool { - return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('client')->value); + return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('developper')->value); } /** @@ -30,7 +30,7 @@ class NoticePolicy */ public function viewTrash(Model $authModel): bool { - return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('client')->value); + return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('developper')->value); } /** @@ -41,7 +41,7 @@ class NoticePolicy */ public function view(Model $authModel): bool { - return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('client')->value); + return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('developper')->value); } /** @@ -52,7 +52,7 @@ class NoticePolicy */ public function create(Model $authModel): bool { - return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('client')->value); + return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('developper')->value); } /** @@ -63,7 +63,7 @@ class NoticePolicy */ public function update(Model $authModel): bool { - return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('client')->value); + return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('developper')->value); } /** @@ -74,7 +74,7 @@ class NoticePolicy */ public function duplicate(Model $authModel): bool { - return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('client')->value); + return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('developper')->value); } /** @@ -85,7 +85,7 @@ class NoticePolicy */ public function restore(Model $authModel): bool { - return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('client')->value); + return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('developper')->value); } /** @@ -96,7 +96,7 @@ class NoticePolicy */ public function delete(Model $authModel): bool { - return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('client')->value); + return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('developper')->value); } /** @@ -107,6 +107,6 @@ class NoticePolicy */ public function forceDelete(Model $authModel): bool { - return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('client')->value); + return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('developper')->value); } } diff --git a/database/factories/InvoicePackageFactory.php b/database/factories/InvoicePackageFactory.php new file mode 100644 index 0000000..39521ce --- /dev/null +++ b/database/factories/InvoicePackageFactory.php @@ -0,0 +1,33 @@ + + */ + protected $model = InvoicePackage::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + "name" => fake()->word(), + "amount" => fake()->randomFloat(2, 1, 1000), + "published" => fake()->boolean(75), + ]; + } +} diff --git a/database/migrations/2026_04_30_145058_create_invoice_packages_table.php b/database/migrations/2026_04_30_145058_create_invoice_packages_table.php new file mode 100644 index 0000000..18f8e36 --- /dev/null +++ b/database/migrations/2026_04_30_145058_create_invoice_packages_table.php @@ -0,0 +1,36 @@ +id()->comment('Id of the invoice package.'); + $table->string('name')->comment('Name of the invoice package.'); + $table->double('amount')->comment('Amount of the invoice package.'); + $table->boolean('published')->comment('Published status of the invoice package.'); + $table->timestamp('created_at')->nullable()->comment('Date the invoice package password was created.'); + $table->timestamp('updated_at')->nullable()->comment('Date the invoice package password was updated.'); + $table->timestamp('deleted_at')->nullable()->comment('Date the invoice package password was deleted.'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + Schema::dropIfExists('invoice_packages'); + } +}; diff --git a/database/seeders/Classic/InvoicePackageSeeder.php b/database/seeders/Classic/InvoicePackageSeeder.php new file mode 100644 index 0000000..33b5a85 --- /dev/null +++ b/database/seeders/Classic/InvoicePackageSeeder.php @@ -0,0 +1,25 @@ +create(); + } +} diff --git a/lang/fr/models.php b/lang/fr/models.php index 31b5704..8f1be0c 100644 --- a/lang/fr/models.php +++ b/lang/fr/models.php @@ -4,6 +4,7 @@ declare(strict_types=1); return [ "classes" => [ - "notices" => "information|informations", + "notices" => "information|informations", + "invoice-packages" => "devis - forfait|devis - forfaits", ], ]; diff --git a/lang/fr/validation.php b/lang/fr/validation.php index 2edf4d1..942aea9 100644 --- a/lang/fr/validation.php +++ b/lang/fr/validation.php @@ -9,5 +9,8 @@ return [ "title" => "titre", "message" => "message", "type" => "type", + "name" => "nom", + "amount" => "jour", + "published" => "publiƩ", ], ]; \ No newline at end of file diff --git a/resources/ts/back/Components/Layout/Sidebar/Links/InvoicePackages.tsx b/resources/ts/back/Components/Layout/Sidebar/Links/InvoicePackages.tsx new file mode 100644 index 0000000..1470a13 --- /dev/null +++ b/resources/ts/back/Components/Layout/Sidebar/Links/InvoicePackages.tsx @@ -0,0 +1,73 @@ +// * Packages +import { HandCoinsIcon } from "lucide-react"; +import { route } from "ziggy-js"; +import { Link } from "@inertiajs/react"; +// * Components shadcn +import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; +import { Button } from "@/components/ui/button"; +// * Other +import { + filterActions, + NavListLinksProps, +} from "@/back/Components/Layout/Sidebar/AppSidebar"; +import { SidebarGroupLink } from "@/back/Components/Layout/Sidebar/SidebarRegistry"; +import { AuthProp } from "@/back/types/global"; +import str from "@/hooks/use-string"; +import { GetTranslation } from "@/lib/utils"; +import useTrans from "@/hooks/use-translations"; +import Views from "@/back/lib/actions"; + +export default function SidebarLinkInvoicePackages(auth: AuthProp): SidebarGroupLink { + const modelName = "invoice-packages"; + + // Translations. + const transModel = GetTranslation(`models.classes.${modelName}`, { + choice: 2, + }); + const transCreate = str( + useTrans(`uwil-backend::crud.actions.${Views.CREATION.label}`), + ) + .ucFirst() + .value(); + const transArchives = str(useTrans("uwil-backend::back.archives")) + .ucFirst() + .value(); + + const link: NavListLinksProps = { + title: str(transModel).ucFirst().value(), + model: modelName, + icon: HandCoinsIcon, + show: auth.policies.invoicePackages.viewAny, + actions: (model: string) => + filterActions({ + auth, + model, + items: [ + auth.policies.invoicePackages.create && ( + + + + ), + auth.policies.invoicePackages.viewTrash && ( + + + + ), + ], + }), + }; + + return { link, group: "modules" }; +} diff --git a/resources/ts/back/Pages/InvoicePackages/Create.tsx b/resources/ts/back/Pages/InvoicePackages/Create.tsx new file mode 100644 index 0000000..880ac64 --- /dev/null +++ b/resources/ts/back/Pages/InvoicePackages/Create.tsx @@ -0,0 +1,63 @@ +// * Packages +import { usePage } from "@inertiajs/react"; +import { route } from "ziggy-js"; +import { ArrowLeftIcon } from "lucide-react"; +// * Other +import AuthenticatedLayout from "@/back/Layouts/AuthenticatedLayout"; +import Form from "@/back/Pages/InvoicePackages/Form"; +import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip"; +import useTrans from "@/hooks/use-translations"; +import { Actions, Methods } from "@/back/lib/enums"; +import { AuthProp, DefaultModel, SingleResource } from "@/back/types/global"; + +export default function Create({ + data, + modelName, +}: { + data: SingleResource; + modelName: string; +}) { + const { viewAny } = (usePage().props.auth as AuthProp).policies.invoicePackages; + const model = data.data as DefaultModel; + + // Translations. + const transBackToList = useTrans("uwil-backend::back.crud_back_to_list"); + const transTitle = useTrans( + route().current(`back.${modelName}.duplicate`) + ? "uwil-backend::crud.helpers.duplication_model" + : "uwil-backend::crud.helpers.creation_model", + { + params: { + model: useTrans(`models.classes.${modelName}`, { + choice: 1, + }), + }, + }, + ); + + const beforeNode = () => { + return ( + viewAny && ( +
+ } + tooltipContent={

{transBackToList}

} + /> +
+ ) + ); + }; + + return ( + +
+ + ); +} diff --git a/resources/ts/back/Pages/InvoicePackages/Edit.tsx b/resources/ts/back/Pages/InvoicePackages/Edit.tsx new file mode 100644 index 0000000..854ce5b --- /dev/null +++ b/resources/ts/back/Pages/InvoicePackages/Edit.tsx @@ -0,0 +1,71 @@ +// * Packages +import { usePage } from "@inertiajs/react"; +import { route } from "ziggy-js"; +import { ArrowLeftIcon } from "lucide-react"; +// * Other +import AuthenticatedLayout from "@/back/Layouts/AuthenticatedLayout"; +import ModelActions from "@/back/Components/ModelActions"; +import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip"; +import Form from "@/back/Pages/InvoicePackages/Form"; +import useTrans from "@/hooks/use-translations"; +import { Actions, Methods } from "@/back/lib/enums"; +import { Views } from "@/back/lib/actions"; +import { AuthProp, DefaultModel, SingleResource } from "@/back/types/global"; + +export default function Edit({ + data, + modelName, +}: { + data: SingleResource; + modelName: string; +}) { + const { viewAny } = (usePage().props.auth as AuthProp).policies.invoicePackages; + const model = data.data as DefaultModel; + const actionModels = [ + Views.VISUALIZATION, + Views.DUPLICATION, + Views.ARCHIVATION, + ]; + + // Translations. + const transBackToList = useTrans("uwil-backend::back.crud_back_to_list"); + const transTitle = useTrans("uwil-backend::crud.helpers.edition_model", { + params: { + model: useTrans(`models.classes.${modelName}`, { + choice: 1, + }), + }, + }); + + const beforeNode = () => { + return ( + viewAny && ( +
+ } + tooltipContent={

{transBackToList}

} + /> +
+ ) + ); + }; + + return ( + + + + + ); +} diff --git a/resources/ts/back/Pages/InvoicePackages/Form.tsx b/resources/ts/back/Pages/InvoicePackages/Form.tsx new file mode 100644 index 0000000..4a1d7a9 --- /dev/null +++ b/resources/ts/back/Pages/InvoicePackages/Form.tsx @@ -0,0 +1,125 @@ +// * Packages +import { useForm, usePage } from "@inertiajs/react"; +import { BookTextIcon, EyeIcon } from "lucide-react"; +// * Components shadcn +// +// * Other +import FieldsetWrapper from "@/back/Components/FieldsetWrapper"; +import useTrans from "@/hooks/use-translations"; +import InputError from "@/back/Components/InputError"; +import SubmitButton from "@/back/Components/SubmitButton"; +import useCrudSubmit from "@/back/hooks/use-crud-submit"; +import { AppEnv } from "@/types/global"; +import { InvoicePackageModel } from "@/back/types/project"; +import SimpleString from "@/back/Components/Inputs/SimpleString"; +import SimpleSwitch from "@/back/Components/Inputs/SimpleSwitch"; +import SimpleNumber from "@/back/Components/Inputs/SimpleNumber"; +import str from "@/hooks/use-string"; + +export default function Form({ + model, + modelName, + modelRoute, + action, + method, +}: { + model?: InvoicePackageModel; + modelName: string; + modelRoute: string; + action: string; + method: number; +}) { + const { appEnv } = usePage().props as unknown as { + appEnv: AppEnv; + }; + + /** Form section */ + const { post, setData, data } = useForm({ + name: model?.name ? model?.name[appEnv.locale] : "", + amount: model?.amount ?? 0, + published: model?.published ?? false, + }); + + const submit = useCrudSubmit({ + method, + modelRoute, + modelName, + action, + model, + post, + data, + modelNameTrans: useTrans(`models.classes.${modelName}`, { + choice: 1, + }), + }); + + return ( + + + } + description={useTrans("uwil-backend::back.form_subtitle_global")} + className="sm:grid-cols-2 gap-4" + > + + + + } + description={useTrans("uwil-backend::back.form_subtitle_visibility")} + inline + > + + +
+ +
+ + ); +} diff --git a/resources/ts/back/Pages/InvoicePackages/Index.tsx b/resources/ts/back/Pages/InvoicePackages/Index.tsx new file mode 100644 index 0000000..145f280 --- /dev/null +++ b/resources/ts/back/Pages/InvoicePackages/Index.tsx @@ -0,0 +1,215 @@ +// * Packages +import { route } from "ziggy-js"; +import { usePage } from "@inertiajs/react"; +import { Row } from "@tanstack/react-table"; +import { ArchiveIcon, ArrowLeftIcon, CheckIcon, PlusIcon, XIcon } from "lucide-react"; +// * Components shadcn +import { Badge } from "@/components/ui/badge"; +// * Other +import AuthenticatedLayout from "@/back/Layouts/AuthenticatedLayout"; +import DataTable from "@/back/Components/Table/DataTable"; +import DataTableSearch from "@/back/Components/Table/DataTableSearch"; +import DataTableFilterButton from "@/back/Components/Table/DataTableFilterButton"; +import SoftdeleteWarning from "@/back/Components/SoftdeleteWarning"; +import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip"; +import RenderTranslationCell from "@/back/Components/Table/RenderTranslationCell"; +import { GetFormatedDate, GetTranslation } from "@/lib/utils"; +import { Views } from "@/back/lib/actions"; +import { AuthProp, DefaultModel, Resource } from "@/back/types/global"; +import str from "@/hooks/use-string"; +import useTrans from "@/hooks/use-translations"; +import { InvoicePackageModel } from "@/back/types/project"; + +const getColumns = () => { + return [ + { + accessorKey: "name", + header: ({ column }: { column: { id: string } }) => ( + + ), + cell: ({ row }: { row: Row }) => ( + + ), + }, + { + accessorKey: "amount", + header: ({ column }: { column: { id: string } }) => ( + + ), + cell: ({ row }: { row: Row }) => row.getValue("amount"), + }, + { + accessorKey: "published", + header: ({ column }: { column: { id: string } }) => ( + + ), + cell: ({ row }: { row: Row }) => ( + + {row.getValue("published") ? ( + + ) : ( + + )} + {str( + GetTranslation( + row.getValue("published") + ? "uwil-backend::back.yes" + : "uwil-backend::back.no", + ), + ) + .ucFirst() + .value()} + + ), + }, + { + accessorKey: "updated_at", + header: ({ column }: { column: { id: string } }) => ( + + ), + cell: ({ row }: { row: Row }) => ( + + {GetFormatedDate(row.getValue("updated_at"))} + + ), + }, + ]; +}; + +export default function Index({ + models, + searchFields, + modelName, +}: { + models: Resource; + searchFields: string; + modelName: string; +}) { + const { auth } = usePage().props as unknown as { + auth: AuthProp; + }; + const { viewAny, viewTrash, create } = auth.policies.invoicePackages; + const isArchiveRoute = route().current(`back.${modelName}.trash`); + const actionModels = isArchiveRoute + ? [Views.RESTORATION, Views.FORCEDELETION] + : [ + Views.VISUALIZATION, + Views.EDITION, + Views.DUPLICATION, + Views.ARCHIVATION, + ]; + const columns = getColumns(); + + // Translations. + const transModel = useTrans(`models.classes.${modelName}`, { choice: 2 }); + const transArchiveTitle = useTrans( + "uwil-backend::crud.helpers.archives_model", + { + params: { model: transModel }, + }, + ); + const transTitle = str(isArchiveRoute ? transArchiveTitle : transModel) + .ucFirst() + .value(); + const transCrudBackList = useTrans("uwil-backend::back.crud_back_to_list"); + + const beforeNode = () => { + return ( + isArchiveRoute && + viewAny && ( +
+ } + tooltipContent={

{transCrudBackList}

} + /> +
+ ) + ); + }; + const transCrudAddEntry = useTrans("uwil-backend::back.crud_add_entry"); + const transAccessTo = useTrans("uwil-backend::back.crud_access_to", { + params: { + target: useTrans("uwil-backend::back.archived_entries"), + }, + }); + const afterNode = () => { + return ( + !isArchiveRoute && ( +
+ {viewTrash && ( + } + tooltipContent={

{transAccessTo}

} + variant="outline" + /> + )} + {create && ( + } + tooltipContent={

{transCrudAddEntry}

} + /> + )} +
+ ) + ); + }; + + return ( + + {isArchiveRoute && } + + + + ); +} diff --git a/resources/ts/back/Pages/InvoicePackages/Show.tsx b/resources/ts/back/Pages/InvoicePackages/Show.tsx new file mode 100644 index 0000000..b8bc7e5 --- /dev/null +++ b/resources/ts/back/Pages/InvoicePackages/Show.tsx @@ -0,0 +1,161 @@ +// * Packages +import { usePage } from "@inertiajs/react"; +import { route } from "ziggy-js"; +import { ArrowLeftIcon, CheckIcon, XIcon } from "lucide-react"; +// * Components shadcn +import { Badge } from "@/components/ui/badge"; +// * Other +import AuthenticatedLayout from "@/back/Layouts/AuthenticatedLayout"; +import DetailsTable from "@/back/Components/Table/DetailsTable"; +import ModelActions from "@/back/Components/ModelActions"; +import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip"; +import RenderTranslationCell from "@/back/Components/Table/RenderTranslationCell"; +import { GetFormatedDate, GetTranslation } from "@/lib/utils"; +import { Views } from "@/back/lib/actions"; +import { AuthProp, DefaultModel, SingleResource } from "@/back/types/global"; +import str from "@/hooks/use-string"; +import useTrans from "@/hooks/use-translations"; +import { TranslationField } from "@/back/types/i18n"; + +const getColumns = () => { + return [ + { + accessorKey: "name", + header: str( + GetTranslation("validation.custom.name"), + ) + .ucFirst() + .value(), + cell: ({ value }: { value: TranslationField }) => ( + + ), + }, + { + accessorKey: "amount", + header: str( + GetTranslation("validation.custom.amount"), + ) + .ucFirst() + .value(), + cell: ({ value }: { value: number }) => value, + }, + { + accessorKey: "published", + header: str(GetTranslation("uwil-backend::validation.custom.published")) + .ucFirst() + .value(), + cell: ({ value }: { value: boolean }) => ( + + {value ? ( + + ) : ( + + )} + {str( + GetTranslation( + value ? "uwil-backend::back.yes" : "uwil-backend::back.no", + ), + ) + .ucFirst() + .value()} + + ), + }, + { + accessorKey: "created_at", + header: str( + GetTranslation("uwil-backend::validation.attributes.created_at"), + ) + .ucFirst() + .value(), + cell: ({ value }: { value: string }) => ( + {GetFormatedDate(value)} + ), + }, + { + accessorKey: "updated_at", + header: str( + GetTranslation("uwil-backend::validation.attributes.updated_at"), + ) + .ucFirst() + .value(), + cell: ({ value }: { value: string }) => ( + {GetFormatedDate(value)} + ), + }, + { + accessorKey: "deleted_at", + header: str( + GetTranslation("uwil-backend::validation.attributes.deleted_at"), + ) + .ucFirst() + .value(), + cell: ({ value }: { value: string }) => ( + {GetFormatedDate(value)} + ), + }, + ]; +}; + +export default function Show({ + data, + modelName, +}: { + data: SingleResource; + modelName: string; +}) { + const { auth } = usePage().props as unknown as { + auth: AuthProp; + }; + const { viewAny } = auth.policies.invoicePackages; + const model = data.data as DefaultModel; + const actionModels = [Views.EDITION, Views.DUPLICATION, Views.ARCHIVATION]; + const transTitle = useTrans( + "uwil-backend::crud.helpers.visualization_model", + { + params: { + model: useTrans(`models.classes.${modelName}`, { + choice: 1, + }), + }, + }, + ); + const columns = getColumns(); + + // Translations. + const transCrudBackToList = useTrans("uwil-backend::back.crud_back_to_list"); + + const beforeNode = () => { + return ( + viewAny && ( +
+ } + tooltipContent={

{transCrudBackToList}

} + /> +
+ ) + ); + }; + + return ( + + + {/* @ts-expect-error Weird error type on cell. */} + + + ); +} diff --git a/resources/ts/back/types/project.d.ts b/resources/ts/back/types/project.d.ts index bb13bb9..c26f79d 100644 --- a/resources/ts/back/types/project.d.ts +++ b/resources/ts/back/types/project.d.ts @@ -6,3 +6,9 @@ type NoticeModel = DefaultModel & { message: TranslationField; type: LaravelEnum; }; + +type InvoicePackageModel = DefaultModel & { + name: TranslationField; + amount: number; + published: boolean; +}; diff --git a/routes/back/invoice-packages.php b/routes/back/invoice-packages.php new file mode 100644 index 0000000..e9e914e --- /dev/null +++ b/routes/back/invoice-packages.php @@ -0,0 +1,14 @@ +name('invoice-packages.trash'); +Route::patch('/invoice-packages/{invoice_package}/restore', [InvoicePackageController::class, 'restore']) + ->name('invoice-packages.restore'); +Route::delete('/invoice-packages/{invoice_package}/force', [InvoicePackageController::class, 'forceDestroy']) + ->name('invoice-packages.forceDestroy'); +Route::resource('invoice-packages', InvoicePackageController::class); +Route::get('/invoice-packages/{invoice_package}/duplicate', [InvoicePackageController::class, 'duplicate']) + ->name('invoice-packages.duplicate'); diff --git a/routes/breadcrumbs/invoice-packages.php b/routes/breadcrumbs/invoice-packages.php new file mode 100644 index 0000000..09dc243 --- /dev/null +++ b/routes/breadcrumbs/invoice-packages.php @@ -0,0 +1,39 @@ +push( + Str::ucfirst(trans_choice('models.classes.invoice-packages', \INF)), + route('back.invoice-packages.index') + ); +}); +Breadcrumbs::for('back.invoice-packages.create', function (Generator $trail) { + $trail->parent('back.invoice-packages.index'); + $trail->push( + Str::ucfirst(trans_choice('models.classes.invoice-packages', 1)), + route('back.invoice-packages.create') + ); +}); +Breadcrumbs::for('back.invoice-packages.edit', function (Generator $trail) { + $trail->parent('back.invoice-packages.index'); + $trail->push(Str::ucfirst(trans('uwil-backend::crud.actions.edit')), route('back.invoice-packages.edit', [ + 'invoice_package' => request()->invoice_package + ])); +}); +Breadcrumbs::for('back.invoice-packages.duplicate', function (Generator $trail) { + $trail->parent('back.invoice-packages.index'); + $trail->push(Str::ucfirst(trans('uwil-backend::crud.actions.duplicate'))); +}); +Breadcrumbs::for('back.invoice-packages.show', function (Generator $trail) { + $trail->parent('back.invoice-packages.index'); + $trail->push(Str::ucfirst(trans('uwil-backend::crud.actions.visualize')), route('back.invoice-packages.show', [ + 'invoice_package' => request()->invoice_package + ])); +}); +Breadcrumbs::for('back.invoice-packages.trash', function (Generator $trail) { + $trail->parent('back.invoice-packages.index'); + $trail->push(Str::ucfirst(trans('uwil-backend::back.archives'))); +});