From 106508419e0152b8de56b90b38ff82009e882c5b Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Tue, 19 May 2026 16:44:38 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20add=20brands=20crud=20#122?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/Back/BrandController.php | 261 ++++++++++++++++++ .../Back/Brands/StoreBrandRequest.php | 50 ++++ .../Back/Brands/UpdateBrandRequest.php | 43 +++ app/Models/Brand.php | 65 +++++ app/Models/Site.php | 18 +- app/Policies/BrandPolicy.php | 112 ++++++++ app/Providers/AppServiceProvider.php | 6 + database/factories/BrandFactory.php | 31 +++ .../2026_05_19_144844_create_brands_table.php | 34 +++ database/seeders/Classic/BrandSeeder.php | 25 ++ lang/fr/models.php | 1 + .../Layout/Sidebar/Links/Brands.tsx | 73 +++++ resources/ts/back/Pages/Brands/Create.tsx | 63 +++++ resources/ts/back/Pages/Brands/Edit.tsx | 71 +++++ resources/ts/back/Pages/Brands/Form.tsx | 127 +++++++++ resources/ts/back/Pages/Brands/Index.tsx | 161 +++++++++++ resources/ts/back/Pages/Brands/Show.tsx | 119 ++++++++ resources/ts/back/types/project.d.ts | 5 + routes/back/brands.php | 14 + routes/breadcrumbs/brands.php | 45 +++ 20 files changed, 1323 insertions(+), 1 deletion(-) create mode 100644 app/Http/Controllers/Back/BrandController.php create mode 100644 app/Http/Requests/Back/Brands/StoreBrandRequest.php create mode 100644 app/Http/Requests/Back/Brands/UpdateBrandRequest.php create mode 100644 app/Models/Brand.php create mode 100644 app/Policies/BrandPolicy.php create mode 100644 database/factories/BrandFactory.php create mode 100644 database/migrations/2026_05_19_144844_create_brands_table.php create mode 100644 database/seeders/Classic/BrandSeeder.php create mode 100644 resources/ts/back/Components/Layout/Sidebar/Links/Brands.tsx create mode 100644 resources/ts/back/Pages/Brands/Create.tsx create mode 100644 resources/ts/back/Pages/Brands/Edit.tsx create mode 100644 resources/ts/back/Pages/Brands/Form.tsx create mode 100644 resources/ts/back/Pages/Brands/Index.tsx create mode 100644 resources/ts/back/Pages/Brands/Show.tsx create mode 100644 routes/back/brands.php create mode 100644 routes/breadcrumbs/brands.php diff --git a/app/Http/Controllers/Back/BrandController.php b/app/Http/Controllers/Back/BrandController.php new file mode 100644 index 0000000..4deeec0 --- /dev/null +++ b/app/Http/Controllers/Back/BrandController.php @@ -0,0 +1,261 @@ + + */ + private \Illuminate\Support\Collection $siteModels; + + /** + * Create the controller instance. + */ + public function __construct() + { + $this->authorizeResource(Brand::class); + $this->modelName = Brand::modelName(); + $this->modelPage = Brand::modelPage(); + $this->siteModels = Site::getModelsCached(); + } + + /** + * 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\Brand> $query */ + $query = Brand::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, + 'name', + ); + } + $searchFields = \implode(', ', [ + trans('validation.attributes.name'), + ]); + + /** 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\Brand $brand + * @return \Inertia\Response + */ + public function create(Brand $brand): \Inertia\Response + { + return $this->inertiaRender("back/Pages/$this->modelPage/Create", [ + 'data' => CommonResource::make($brand), + 'siteModels' => $this->siteModels, + ]); + } + + /** + * Show a specific resource. + * + * @param \App\Models\Brand $brand + * @return \Inertia\Response + */ + public function show(Brand $brand): \Inertia\Response + { + return $this->inertiaRender("back/Pages/$this->modelPage/Show", [ + 'data' => CommonResource::make($brand->load(['sites'])), + ]); + } + + /** + * Store a newly created resource. + * + * @param \App\Http\Requests\Back\Brands\StoreBrandRequest $request + * @return \Illuminate\Http\RedirectResponse + */ + public function store(StoreBrandRequest $request): \Illuminate\Http\RedirectResponse + { + return DB::transaction(function () use ($request) { + $validatedData = $request->validated(); + $brand = new Brand(); + $brand->fill(Arr::except($validatedData, 'sites')); + + if ($brand->saveOrFail()) { + $brand->sites()->syncWithoutDetaching(Arr::get($validatedData, 'sites')); + return redirect()->route("back.$this->modelName.edit", $brand) + ->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\Brand $brand + * @return \Inertia\Response + */ + public function edit(Brand $brand): \Inertia\Response + { + return $this->inertiaRender("back/Pages/$this->modelPage/Edit", [ + 'data' => CommonResource::make($brand->load(['sites'])), + 'siteModels' => $this->siteModels, + ]); + } + + /** + * Update the specified resource. + * + * @param \App\Http\Requests\Back\Brands\UpdateBrandRequest $request + * @param \App\Models\Brand $brand + * @return \Illuminate\Http\RedirectResponse + */ + public function update( + UpdateBrandRequest $request, + Brand $brand, + ): \Illuminate\Http\RedirectResponse { + return DB::transaction(function () use ($request, $brand) { + $validatedData = $request->validated(); + $brand->fill(Arr::except($validatedData, 'sites')); + + if ($brand->saveOrFail()) { + $brand->sites()->sync(Arr::get($validatedData, 'sites')); + return redirect()->route("back.$this->modelName.edit", $brand) + ->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", $brand) + ->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\Brand $brand + * @return \Inertia\Response + */ + public function duplicate(Brand $brand): \Inertia\Response + { + return $this->create($brand->replicate()); + } + + /** + * Restore the specified resource. + * + * @param integer $brand + * @return \Illuminate\Http\RedirectResponse + */ + public function restore(int $brand): \Illuminate\Http\RedirectResponse + { + $brand = Brand::withTrashed()->findOrFail($brand); + /** @var array */ + $previousQueries = []; + \parse_str(\parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries); + if ($brand->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\Brand $brand + * @return \Illuminate\Http\RedirectResponse + */ + public function destroy(Brand $brand): \Illuminate\Http\RedirectResponse + { + /** @var array */ + $previousQueries = []; + \parse_str(\parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries); + if ($brand->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\Brand|integer $brand + * @return \Illuminate\Http\RedirectResponse + */ + public function forceDestroy(Brand|int $brand): \Illuminate\Http\RedirectResponse + { + if (is_int($brand)) { + $brand = Brand::withTrashed()->findOrFail($brand); + } + /** @var array */ + $previousQueries = []; + \parse_str(\parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries); + if ($brand->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/Requests/Back/Brands/StoreBrandRequest.php b/app/Http/Requests/Back/Brands/StoreBrandRequest.php new file mode 100644 index 0000000..2107e9f --- /dev/null +++ b/app/Http/Requests/Back/Brands/StoreBrandRequest.php @@ -0,0 +1,50 @@ + + */ + public function rules(): array + { + return [ + 'name' => 'required|string|between:3,255|unique:brands,name', + 'sites' => 'nullable|array', + 'sites.*' => 'integer|exists:sites,id|distinct', + ]; + } + + /** + * Get custom attributes for validator errors. + * + * @return array + */ + public function attributes(): array + { + return [ + 'name' => trans('validation.custom.name'), + 'sites' => trans_choice('models.classes.sites', \INF), + ]; + } +} diff --git a/app/Http/Requests/Back/Brands/UpdateBrandRequest.php b/app/Http/Requests/Back/Brands/UpdateBrandRequest.php new file mode 100644 index 0000000..77b3e31 --- /dev/null +++ b/app/Http/Requests/Back/Brands/UpdateBrandRequest.php @@ -0,0 +1,43 @@ +route('brand')); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + #[\Override] + public function rules(): array + { + /** @var \App\Models\Brand $brand */ + $brand = $this->route('brand'); + return array_merge(parent::rules(), [ + 'name' => [ + 'required', + 'string', + 'between:3,255', + Rule::unique('brands', 'name')->ignore($brand->getRouteKey()), + ], + ]); + } +} diff --git a/app/Models/Brand.php b/app/Models/Brand.php new file mode 100644 index 0000000..1a137d7 --- /dev/null +++ b/app/Models/Brand.php @@ -0,0 +1,65 @@ + */ + use HasFactory; + use HasModelNaming; + use HasTranslations; + use SoftDeletes; + + /** + * The attributes that are mass assignable. + * + * @var list + */ + protected $fillable = [ + 'name', + ]; + + /** + * The translatable columns. + * + * @var list + */ + public $translatable = [ + 'name', + ]; + + // * RELATIONS. + + /** + * Get sites associated to the brand. + * + * @return \Illuminate\Database\Eloquent\Relations\MorphToMany<\App\Models\Site> + */ + public function sites(): \Illuminate\Database\Eloquent\Relations\MorphToMany + { + return $this->morphToMany( + Site::class, + 'sitable', + 'sitables', + 'sitable_id', + 'site_id', + )->withPivot('created_at'); + } +} diff --git a/app/Models/Site.php b/app/Models/Site.php index 429a883..fd63cf9 100644 --- a/app/Models/Site.php +++ b/app/Models/Site.php @@ -98,7 +98,23 @@ class Site extends Model 'sitable', 'sitables', 'site_id', - 'sitable_id' + 'sitable_id', + )->withPivot('created_at'); + } + + /** + * Get brands associated to the site. + * + * @return \Illuminate\Database\Eloquent\Relations\MorphToMany<\App\Models\Brand> + */ + public function brands(): \Illuminate\Database\Eloquent\Relations\MorphToMany + { + return $this->morphedByMany( + Brand::class, + 'sitable', + 'sitables', + 'site_id', + 'sitable_id', )->withPivot('created_at'); } diff --git a/app/Policies/BrandPolicy.php b/app/Policies/BrandPolicy.php new file mode 100644 index 0000000..7159acb --- /dev/null +++ b/app/Policies/BrandPolicy.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/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 0752f48..761df78 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use App\Enums\NoticeTypeEnum; +use App\Models\Brand; use App\Models\GitlabProject; use App\Models\InvoiceCompany; use App\Models\InvoicePackage; @@ -54,5 +55,10 @@ class AppServiceProvider extends ServiceProvider 'viewTrash' => Gate::allows('viewTrash', Site::class), 'create' => Gate::allows('create', Site::class), ]); + InertiaSharedRegistry::add('auth.policies.brands', fn () => [ + 'viewAny' => Gate::allows('viewAny', Brand::class), + 'viewTrash' => Gate::allows('viewTrash', Brand::class), + 'create' => Gate::allows('create', Brand::class), + ]); } } diff --git a/database/factories/BrandFactory.php b/database/factories/BrandFactory.php new file mode 100644 index 0000000..bf17cf2 --- /dev/null +++ b/database/factories/BrandFactory.php @@ -0,0 +1,31 @@ + + */ + protected $model = Brand::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + "name" => fake()->unique()->words(3, true), + ]; + } +} diff --git a/database/migrations/2026_05_19_144844_create_brands_table.php b/database/migrations/2026_05_19_144844_create_brands_table.php new file mode 100644 index 0000000..7e883c3 --- /dev/null +++ b/database/migrations/2026_05_19_144844_create_brands_table.php @@ -0,0 +1,34 @@ +id()->comment('Id of the brand.'); + $table->string('name')->unique()->comment('Name of the brand.'); + $table->timestamp('created_at')->nullable()->comment('Date the brand was created.'); + $table->timestamp('updated_at')->nullable()->comment('Date the brand was updated.'); + $table->timestamp('deleted_at')->nullable()->comment('Date the brand was deleted.'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + Schema::dropIfExists('brands'); + } +}; diff --git a/database/seeders/Classic/BrandSeeder.php b/database/seeders/Classic/BrandSeeder.php new file mode 100644 index 0000000..9d549c6 --- /dev/null +++ b/database/seeders/Classic/BrandSeeder.php @@ -0,0 +1,25 @@ +create(); + } +} diff --git a/lang/fr/models.php b/lang/fr/models.php index fb60184..40bf8b1 100644 --- a/lang/fr/models.php +++ b/lang/fr/models.php @@ -9,5 +9,6 @@ return [ "invoice-companies" => "devis - société|devis - sociétés", "gitlab-projects" => "gitlab - projet|gitlab - projets", "sites" => "site|sites", + "brands" => "marque|marques", ], ]; diff --git a/resources/ts/back/Components/Layout/Sidebar/Links/Brands.tsx b/resources/ts/back/Components/Layout/Sidebar/Links/Brands.tsx new file mode 100644 index 0000000..c90ff28 --- /dev/null +++ b/resources/ts/back/Components/Layout/Sidebar/Links/Brands.tsx @@ -0,0 +1,73 @@ +// * Packages +import { InfoIcon } 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 SidebarLinkBrands(auth: AuthProp): SidebarGroupLink { + const modelName = "brands"; + + // 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: InfoIcon, + show: auth.policies.brands.viewAny, + actions: (model: string) => + filterActions({ + auth, + model, + items: [ + auth.policies.brands.create && ( + + + + ), + auth.policies.brands.viewTrash && ( + + + + ), + ], + }), + }; + + return { link, group: "modules" }; +} diff --git a/resources/ts/back/Pages/Brands/Create.tsx b/resources/ts/back/Pages/Brands/Create.tsx new file mode 100644 index 0000000..824f970 --- /dev/null +++ b/resources/ts/back/Pages/Brands/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/Brands/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.brands; + 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/Brands/Edit.tsx b/resources/ts/back/Pages/Brands/Edit.tsx new file mode 100644 index 0000000..2fb1642 --- /dev/null +++ b/resources/ts/back/Pages/Brands/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/Brands/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.brands; + 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/Brands/Form.tsx b/resources/ts/back/Pages/Brands/Form.tsx new file mode 100644 index 0000000..6c56abf --- /dev/null +++ b/resources/ts/back/Pages/Brands/Form.tsx @@ -0,0 +1,127 @@ +// * Packages +import { useForm, usePage } from "@inertiajs/react"; +import { BookTextIcon } from "lucide-react"; +// * Components shadcn +// +// * Other +import FieldsetWrapper from "@/back/Components/FieldsetWrapper"; +import SimpleString from "@/back/Components/Inputs/SimpleString"; +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 { BrandModel, SiteModel } from "@/back/types/project"; +import str from "@/hooks/use-string"; +import { AppEnv } from "@/types/global"; +import { MultiSelect } from "@/back/Components/Inputs/MultiSelect"; +import { extractIdsFromDatabasePivotRelation, mapPivotOptions } from "@/lib/mapPivotOptions"; + +export default function Form({ + model, + modelName, + modelRoute, + action, + method, +}: { + model?: BrandModel; + modelName: string; + modelRoute: string; + action: string; + method: number; +}) { + const { + appEnv, + siteModels + } = usePage().props as unknown as { + appEnv: AppEnv; + siteModels: Array; + }; + + /** Form section */ + const { post, setData, data } = useForm({ + name: model?.name ? model?.name[appEnv.locale] : undefined, + sites: extractIdsFromDatabasePivotRelation(model?.sites), + }); + + const submit = useCrudSubmit({ + method, + modelRoute, + modelName, + action, + model, + post, + data, + modelNameTrans: useTrans(`models.classes.${modelName}`, { + choice: 1, + }), + }); + + const selectOptionsSites = mapPivotOptions(siteModels, model?.sites, 'name', appEnv.locale); + + return ( + + + } + description={useTrans("uwil-backend::back.form_subtitle_global")} + className="sm:grid-cols-2 gap-4" + > + + + +
+ +
+ + ); +} diff --git a/resources/ts/back/Pages/Brands/Index.tsx b/resources/ts/back/Pages/Brands/Index.tsx new file mode 100644 index 0000000..eb28ceb --- /dev/null +++ b/resources/ts/back/Pages/Brands/Index.tsx @@ -0,0 +1,161 @@ +// * Packages +import { route } from "ziggy-js"; +import { usePage } from "@inertiajs/react"; +import { Row } from "@tanstack/react-table"; +import { ArchiveIcon, ArrowLeftIcon, PlusIcon } 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 { GetFormatedDate, GetTranslation } from "@/lib/utils"; +import { Views } from "@/back/lib/actions"; +import { AuthProp, Resource } from "@/back/types/global"; +import str from "@/hooks/use-string"; +import useTrans from "@/hooks/use-translations"; +import RenderTranslationCell from "@/back/Components/Table/RenderTranslationCell"; +import { BrandModel } from "@/back/types/project"; + +const getColumns = () => { + return [ + { + accessorKey: "name", + header: ({ column }: { column: { id: string } }) => ( + + ), + cell: ({ row }: { row: Row }) => ( + + ), + }, + { + 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.brands; + 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/Brands/Show.tsx b/resources/ts/back/Pages/Brands/Show.tsx new file mode 100644 index 0000000..75cb060 --- /dev/null +++ b/resources/ts/back/Pages/Brands/Show.tsx @@ -0,0 +1,119 @@ +// * Packages +import { usePage } from "@inertiajs/react"; +import { route } from "ziggy-js"; +import { ArrowLeftIcon } 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 { 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"; +import RenderTranslationCell from "@/back/Components/Table/RenderTranslationCell"; + +const getColumns = () => { + return [ + { + accessorKey: "name", + header: str(GetTranslation("validation.custom.name")).ucFirst().value(), + cell: ({ value }: { value: TranslationField }) => ( + + ), + }, + { + 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.brands; + 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 8538852..7e1ff23 100644 --- a/resources/ts/back/types/project.d.ts +++ b/resources/ts/back/types/project.d.ts @@ -34,3 +34,8 @@ type SiteModel = DefaultModel & { gitlab_project: GitlabProjectModel; users: Array; }; + +type BrandModel = DefaultModel & { + name: TranslationField; + sites: Array; +} \ No newline at end of file diff --git a/routes/back/brands.php b/routes/back/brands.php new file mode 100644 index 0000000..ec3a0b6 --- /dev/null +++ b/routes/back/brands.php @@ -0,0 +1,14 @@ +name('brands.trash'); +Route::patch('/brands/{brand}/restore', [BrandController::class, 'restore']) + ->name('brands.restore'); +Route::delete('/brands/{brand}/force', [BrandController::class, 'forceDestroy']) + ->name('brands.forceDestroy'); +Route::resource('brands', BrandController::class); +Route::get('/brands/{brand}/duplicate', [BrandController::class, 'duplicate']) + ->name('brands.duplicate'); diff --git a/routes/breadcrumbs/brands.php b/routes/breadcrumbs/brands.php new file mode 100644 index 0000000..1790b10 --- /dev/null +++ b/routes/breadcrumbs/brands.php @@ -0,0 +1,45 @@ +push( + Str::ucfirst(trans_choice('models.classes.brands', \INF)), + route('back.brands.index') + ); +}); +Breadcrumbs::for('back.brands.create', function (Generator $trail) { + $trail->parent('back.brands.index'); + $trail->push( + Str::ucfirst(trans('uwil-backend::crud.actions.create')), + route('back.brands.create'), + ); +}); +Breadcrumbs::for('back.brands.edit', function (Generator $trail) { + $trail->parent('back.brands.index'); + $trail->push( + Str::ucfirst(trans('uwil-backend::crud.actions.edit')), + route('back.brands.edit', [ + 'brand' => request()->brand, + ]) + ); +}); +Breadcrumbs::for('back.brands.duplicate', function (Generator $trail) { + $trail->parent('back.brands.index'); + $trail->push(Str::ucfirst(trans('uwil-backend::crud.actions.duplicate'))); +}); +Breadcrumbs::for('back.brands.show', function (Generator $trail) { + $trail->parent('back.brands.index'); + $trail->push( + Str::ucfirst(trans('uwil-backend::crud.actions.visualize')), + route('back.brands.show', [ + 'brand' => request()->brand, + ]), + ); +}); +Breadcrumbs::for('back.brands.trash', function (Generator $trail) { + $trail->parent('back.brands.index'); + $trail->push(Str::ucfirst(trans('uwil-backend::back.archives'))); +});