diff --git a/app/Enums/NoticeTypeEnum.php b/app/Enums/NoticeTypeEnum.php new file mode 100644 index 0000000..0931a8c --- /dev/null +++ b/app/Enums/NoticeTypeEnum.php @@ -0,0 +1,57 @@ +name => 'bg-primary text-primary-foreground', + self::secondary->name => 'bg-secondary text-secondary-foreground', + self::success->name => 'bg-successive text-successive-foreground', + self::danger->name => 'bg-destructive text-destructive-foreground', + self::warning->name => 'bg-preventive text-preventive-foreground', + self::info->name => 'bg-informative text-informative-foreground', + self::light->name => 'bg-muted text-muted-foreground', + self::dark->name => 'bg-foreground text-background', + ]; + + /** + * Get Class. + * + * @return string + */ + public function class(): string + { + return self::CLASSES[$this->name]; + } + + /** + * Get enum classes. + * + * @return array + */ + public static function toClasses(): array + { + return self::CLASSES; + } +} + \ No newline at end of file diff --git a/app/Http/Controllers/Back/NoticeController.php b/app/Http/Controllers/Back/NoticeController.php new file mode 100644 index 0000000..9cc679d --- /dev/null +++ b/app/Http/Controllers/Back/NoticeController.php @@ -0,0 +1,245 @@ +authorizeResource(Notice::class); + $this->modelName = Notice::modelName(); + $this->modelPage = Notice::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\Notice> $query */ + $query = Notice::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\Notice $notice + * @return \Inertia\Response + */ + public function create(Notice $notice): \Inertia\Response + { + return $this->inertiaRender("back/Pages/$this->modelPage/Create", [ + 'data' => CommonResource::make($notice), + ]); + } + + /** + * Show a specific resource. + * + * @param \App\Models\Notice $notice + * @return \Inertia\Response + */ + public function show(Notice $notice): \Inertia\Response + { + return $this->inertiaRender("back/Pages/$this->modelPage/Show", [ + 'data' => CommonResource::make($notice), + ]); + } + + /** + * Store a newly created resource. + * + * @param \App\Http\Requests\Back\Notices\StoreNoticeRequest $request + * @return \Illuminate\Http\RedirectResponse + */ + public function store(StoreNoticeRequest $request): \Illuminate\Http\RedirectResponse + { + return DB::transaction(function () use ($request) { + $notice = new Notice(); + $notice->inertiaFill($request->validated()); + + if ($notice->saveOrFail()) { + return redirect()->route("back.$this->modelName.edit", $notice) + ->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\Notice $notice + * @return \Inertia\Response + */ + public function edit(Notice $notice): \Inertia\Response + { + return $this->inertiaRender("back/Pages/$this->modelPage/Edit", [ + 'data' => CommonResource::make($notice), + ]); + } + + /** + * Update the specified resource. + * + * @param \App\Http\Requests\Back\Notices\UpdateNoticeRequest $request + * @param \App\Models\Notice $notice + * @return \Illuminate\Http\RedirectResponse + */ + public function update( + UpdateNoticeRequest $request, + Notice $notice + ): \Illuminate\Http\RedirectResponse { + return DB::transaction(function () use ($request, $notice) { + $notice->inertiaFill($request->validated()); + + if ($notice->saveOrFail()) { + return redirect()->route("back.$this->modelName.edit", $notice) + ->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", $notice) + ->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\Notice $notice + * @return \Inertia\Response + */ + public function duplicate(Notice $notice): \Inertia\Response + { + return $this->create($notice->replicate()); + } + + /** + * Restore the specified resource. + * + * @param integer $notice + * @return \Illuminate\Http\RedirectResponse + */ + public function restore(int $notice): \Illuminate\Http\RedirectResponse + { + $notice = Notice::withTrashed()->findOrFail($notice); + /** @var array */ + $previousQueries = []; + \parse_str(\parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries); + if ($notice->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\Notice $notice + * @return \Illuminate\Http\RedirectResponse + */ + public function destroy(Notice $notice): \Illuminate\Http\RedirectResponse + { + /** @var array */ + $previousQueries = []; + \parse_str(\parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries); + if ($notice->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\Notice|integer $notice + * @return \Illuminate\Http\RedirectResponse + */ + public function forceDestroy(Notice|int $notice): \Illuminate\Http\RedirectResponse + { + if (is_int($notice)) { + $notice = Notice::withTrashed()->findOrFail($notice); + } + /** @var array */ + $previousQueries = []; + \parse_str(\parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries); + if ($notice->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 6db131c..783d49a 100644 --- a/app/Http/Middlewares/HandleInertiaRequests.php +++ b/app/Http/Middlewares/HandleInertiaRequests.php @@ -2,7 +2,10 @@ namespace App\Http\Middlewares; +use App\Enums\NoticeTypeEnum; +use App\Models\Notice; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Gate; use Inertia\Middleware; use Uwil\LaravelCommon\Helpers\InertiaHelper; use Uwil\LaravelCommon\Support\InertiaSharedRegistry; @@ -19,7 +22,16 @@ class HandleInertiaRequests extends Middleware { $props = [ ...InertiaHelper::errorProps(parent::share($request)), - // More props here. + 'auth' => [ + 'policies' => [ + 'notices' => [ + 'viewAny' => Gate::allows('viewAny', Notice::class), + 'viewTrash' => Gate::allows('viewTrash', Notice::class), + 'create' => Gate::allows('create', Notice::class), + ] + ] + ], + 'noticeTypeEnum' => NoticeTypeEnum::toArray(), ]; return array_replace_recursive( $props, diff --git a/app/Http/Requests/Back/Notices/StoreNoticeRequest.php b/app/Http/Requests/Back/Notices/StoreNoticeRequest.php new file mode 100644 index 0000000..5067d66 --- /dev/null +++ b/app/Http/Requests/Back/Notices/StoreNoticeRequest.php @@ -0,0 +1,54 @@ + + */ + public function rules(): array + { + return [ + 'title' => 'required|string|min:3|max:255', + 'message' => ['required', 'string', new WysiwygLength()], + 'type' => ['required', 'integer', new Enum(NoticeTypeEnum::class)], + ]; + } + + /** + * Get custom attributes for validator errors. + * + * @return array + */ + public function attributes(): array + { + return [ + 'title' => trans('validation.custom.title'), + 'message' => trans('validation.custom.message'), + 'type' => trans('validation.custom.type'), + ]; + } +} diff --git a/app/Http/Requests/Back/Notices/UpdateNoticeRequest.php b/app/Http/Requests/Back/Notices/UpdateNoticeRequest.php new file mode 100644 index 0000000..8cd40a5 --- /dev/null +++ b/app/Http/Requests/Back/Notices/UpdateNoticeRequest.php @@ -0,0 +1,21 @@ +route('notice')); + } +} diff --git a/app/Models/Notice.php b/app/Models/Notice.php new file mode 100644 index 0000000..a3c121b --- /dev/null +++ b/app/Models/Notice.php @@ -0,0 +1,99 @@ + + */ + protected $fillable = [ + 'title', + 'message', + 'type', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'type' => NoticeTypeEnum::class, + ]; + + /** + * The translatable columns. + * + * @var list + */ + public $translatable = [ + '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/NoticePolicy.php b/app/Policies/NoticePolicy.php new file mode 100644 index 0000000..75a6df6 --- /dev/null +++ b/app/Policies/NoticePolicy.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('client')->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('client')->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('client')->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('client')->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('client')->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('client')->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('client')->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('client')->value); + } +} diff --git a/database/factories/.gitkeep b/database/factories/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/database/factories/NoticeFactory.php b/database/factories/NoticeFactory.php new file mode 100644 index 0000000..b85f049 --- /dev/null +++ b/database/factories/NoticeFactory.php @@ -0,0 +1,34 @@ + + */ + protected $model = Notice::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + "title" => fake()->word(), + "message" => sprintf("

%s

", fake()->text(\rand(20, 40))), + "type" => fake()->randomElement(NoticeTypeEnum::cases()), + ]; + } +} diff --git a/database/migrations/2026_04_28_145141_add_password_users_table.php b/database/migrations/2026_04_28_145141_add_password_users_table.php index cf07bc1..40d370a 100644 --- a/database/migrations/2026_04_28_145141_add_password_users_table.php +++ b/database/migrations/2026_04_28_145141_add_password_users_table.php @@ -14,7 +14,7 @@ return new class extends Migration public function up(): void { Schema::create('user_passwords', function (Blueprint $table): void { - $table->id()->comment('Id of the user password.');; + $table->id()->comment('Id of the user password.'); $table->foreignId('user_id')->unique()->comment('Associated user.') ->constrained('users')->cascadeOnDelete(); $table->string('password')->comment('Password of the user.'); diff --git a/database/migrations/2026_04_30_123018_create_notices_table.php b/database/migrations/2026_04_30_123018_create_notices_table.php new file mode 100644 index 0000000..cc8dd6c --- /dev/null +++ b/database/migrations/2026_04_30_123018_create_notices_table.php @@ -0,0 +1,36 @@ +id()->comment('Id of the notice.'); + $table->string('title')->comment('Title of the notice.'); + $table->text('message')->comment('Message of the notice.'); + $table->unsignedTinyInteger('type')->comment('Type of the notice.'); + $table->timestamp('created_at')->nullable()->comment('Date the notice password was created.'); + $table->timestamp('updated_at')->nullable()->comment('Date the notice password was updated.'); + $table->timestamp('deleted_at')->nullable()->comment('Date the notice password was deleted.'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + Schema::dropIfExists('notices'); + } +}; diff --git a/database/seeders/Classic/NoticeSeeder.php b/database/seeders/Classic/NoticeSeeder.php new file mode 100644 index 0000000..46569a5 --- /dev/null +++ b/database/seeders/Classic/NoticeSeeder.php @@ -0,0 +1,25 @@ +create(); + } +} diff --git a/lang/fr/models.php b/lang/fr/models.php index 690ae38..31b5704 100644 --- a/lang/fr/models.php +++ b/lang/fr/models.php @@ -4,6 +4,6 @@ declare(strict_types=1); return [ "classes" => [ - // "users" => "utilisateur|utilisateurs", + "notices" => "information|informations", ], ]; diff --git a/lang/fr/validation.php b/lang/fr/validation.php index 99d60a5..2edf4d1 100644 --- a/lang/fr/validation.php +++ b/lang/fr/validation.php @@ -6,5 +6,8 @@ return [ "custom" => [ "token" => "token", "new_password" => "nouveau mot de passe", + "title" => "titre", + "message" => "message", + "type" => "type", ], ]; \ No newline at end of file diff --git a/resources/ts/back/Components/Layout/Sidebar/Links/Notices.tsx b/resources/ts/back/Components/Layout/Sidebar/Links/Notices.tsx new file mode 100644 index 0000000..532f35a --- /dev/null +++ b/resources/ts/back/Components/Layout/Sidebar/Links/Notices.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 SidebarLinkNotices(auth: AuthProp): SidebarGroupLink { + const modelName = "notices"; + + // 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.notices.viewAny, + actions: (model: string) => + filterActions({ + auth, + model, + items: [ + auth.policies.notices.create && ( + + + + ), + auth.policies.notices.viewTrash && ( + + + + ), + ], + }), + }; + + return { link, group: "modules" }; +} diff --git a/resources/ts/back/Components/Table/RenderTranslationCell.tsx b/resources/ts/back/Components/Table/RenderTranslationCell.tsx index fafa870..d6c848e 100644 --- a/resources/ts/back/Components/Table/RenderTranslationCell.tsx +++ b/resources/ts/back/Components/Table/RenderTranslationCell.tsx @@ -2,27 +2,22 @@ import { usePage } from "@inertiajs/react"; import { ReactNode } from "react"; // * Other -import { AppEnv } from "@/back/types/global"; +import { AppEnv } from "@/types/global"; import { TranslationField } from "@/back/types/i18n"; import useTrans from "@/hooks/use-translations"; +import str from "@/hooks/use-string"; + +const PREVIEW_LENGTH = 50; + +function normalizePreview(value: string): string { + return str(value).normalize().value().substring(0, PREVIEW_LENGTH); +} -/** - * Resolves the best translation value for the current locale. - * - * Priority: - * 1. Value for the current app locale - * 2. First non-empty available translation (fallback) - * - * @param value - Translation field containing localized values - * @return An object with the resolved value and optional fallback locale, - * or null if no translation is available - */ function useGetTranslationValue( value?: TranslationField, ): { value: string; locale?: string } | null { - const { appEnv } = usePage().props as unknown as { - appEnv: AppEnv; - }; + const { appEnv } = usePage().props as unknown as { appEnv: AppEnv }; + if (!value) return null; const local = value[appEnv.locale]; @@ -34,20 +29,23 @@ function useGetTranslationValue( return { value: fallback[1], locale: fallback[0] }; } -/** - * Renders a table cell displaying a translated value. - * - * - Displays the value directly if it matches the current locale - * - Displays "locale: value" when a fallback translation is used - * - Displays a "not known" message when no translation is available - * - * @param value - Translation field containing localized values - * @return A ReactNode ready to be rendered inside a table cell - */ +function RenderValue({ value, renderHtml }: { value: string; renderHtml: boolean }): ReactNode { + const preview = normalizePreview(value); + + return renderHtml ? ( + // eslint-disable-next-line react/no-danger +

+ ) : ( + preview + ); +} + export default function RenderTranslationCell({ value, + renderHtml = false, }: { value?: TranslationField; + renderHtml?: boolean; }): ReactNode { const result = useGetTranslationValue(value); const notKnown = useTrans("uwil-i18n::trans.value_not_known"); @@ -57,13 +55,13 @@ export default function RenderTranslationCell({ } if (!result.locale) { - return result.value; + return ; } return ( {result.locale}: - {result.value} + ); -} +} \ No newline at end of file diff --git a/resources/ts/back/Pages/Notices/Create.tsx b/resources/ts/back/Pages/Notices/Create.tsx new file mode 100644 index 0000000..3134568 --- /dev/null +++ b/resources/ts/back/Pages/Notices/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/Notices/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.notices; + 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/Notices/Edit.tsx b/resources/ts/back/Pages/Notices/Edit.tsx new file mode 100644 index 0000000..64c2906 --- /dev/null +++ b/resources/ts/back/Pages/Notices/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/Notices/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.notices; + 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/Notices/Form.tsx b/resources/ts/back/Pages/Notices/Form.tsx new file mode 100644 index 0000000..c94e441 --- /dev/null +++ b/resources/ts/back/Pages/Notices/Form.tsx @@ -0,0 +1,131 @@ +// * Packages +import { useForm, usePage } from "@inertiajs/react"; +import { BookTextIcon } 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 { NoticeModel } from "@/back/types/project"; +import SimpleString from "@/back/Components/Inputs/SimpleString"; +import str from "@/hooks/use-string"; +import { AppEnv, LaravelEnum } from "@/types/global"; +import SimpleWysiwyg from "@/back/Components/Inputs/Wysiwyg/SimpleWysiwyg"; +import EnumSelect from "@/back/Components/Inputs/EnumSelect"; + +export default function Form({ + model, + modelName, + modelRoute, + action, + method, +}: { + model?: NoticeModel; + modelName: string; + modelRoute: string; + action: string; + method: number; +}) { + const { appEnv, noticeTypeEnum } = usePage().props as unknown as { + appEnv: AppEnv; + noticeTypeEnum: LaravelEnum; + }; + + /** Form section */ + const { post, setData, data } = useForm({ + title: model?.title ? model?.title[appEnv.locale] : "", + message: model?.message ? model?.message[appEnv.locale] : "", + type: model?.type ?? noticeTypeEnum.primary, + }); + + 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" + > + + + + +
+ +
+ + ); +} diff --git a/resources/ts/back/Pages/Notices/Index.tsx b/resources/ts/back/Pages/Notices/Index.tsx new file mode 100644 index 0000000..be1ff3b --- /dev/null +++ b/resources/ts/back/Pages/Notices/Index.tsx @@ -0,0 +1,207 @@ +// * 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, DefaultModel, 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 { NoticeModel } from "@/back/types/project"; +import { LaravelEnum } from "@/types/global"; + +const getColumns = (noticeTypeEnum: LaravelEnum) => { + return [ + { + accessorKey: "title", + header: ({ column }: { column: { id: string } }) => ( + + ), + cell: ({ row }: { row: Row }) => ( + + ), + }, + { + accessorKey: "message", + header: ({ column }: { column: { id: string } }) => ( + + ), + cell: ({ row }: { row: Row }) => ( + + ), + }, + { + accessorKey: "type", + header: ({ column }: { column: { id: string } }) => ( + + ), + cell: ({ row }: { row: Row }) => ( + + {str( + String( + Object.values(noticeTypeEnum).find( + (type) => type.value === row.getValue("type"), + )?.label, + ), + ) + .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, noticeTypeEnum } = usePage().props as unknown as { + auth: AuthProp; + noticeTypeEnum: LaravelEnum; + }; + const { viewAny, viewTrash, create } = auth.policies.notices; + 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(noticeTypeEnum); + + // 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/Notices/Show.tsx b/resources/ts/back/Pages/Notices/Show.tsx new file mode 100644 index 0000000..6ee3cdb --- /dev/null +++ b/resources/ts/back/Pages/Notices/Show.tsx @@ -0,0 +1,154 @@ +// * 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 { LaravelEnum } from "@/types/global"; +import { TranslationField } from "@/back/types/i18n"; +import RenderTranslationCell from "@/back/Components/Table/RenderTranslationCell"; + +const getColumns = (noticeTypeEnum: LaravelEnum) => { + return [ + { + accessorKey: "title", + header: str( + GetTranslation("validation.custom.title"), + ) + .ucFirst() + .value(), + cell: ({ value }: { value: TranslationField }) => ( + + ), + }, + { + accessorKey: "message", + header: str( + GetTranslation("validation.custom.message"), + ) + .ucFirst() + .value(), + cell: ({ value }: { value: TranslationField }) => ( + + ), + }, + { + accessorKey: "type", + header: str(GetTranslation("validation.custom.type")) + .ucFirst() + .value(), + cell: ({ value }: { value: number }) => ( + + {str( + String( + Object.values(noticeTypeEnum).find((role) => role.value === value) + ?.label, + ), + ) + .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, noticeTypeEnum } = usePage().props as unknown as { + auth: AuthProp; + noticeTypeEnum: LaravelEnum; + }; + const { viewAny } = auth.policies.notices; + 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(noticeTypeEnum); + + // 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 new file mode 100644 index 0000000..bb13bb9 --- /dev/null +++ b/resources/ts/back/types/project.d.ts @@ -0,0 +1,8 @@ +import { LaravelEnum } from "@/types/global"; +import { DefaultModel } from "@/back/types/global"; + +type NoticeModel = DefaultModel & { + title: TranslationField; + message: TranslationField; + type: LaravelEnum; +}; diff --git a/routes/back/notices.php b/routes/back/notices.php new file mode 100644 index 0000000..7f16c34 --- /dev/null +++ b/routes/back/notices.php @@ -0,0 +1,14 @@ +name('notices.trash'); +Route::patch('/notices/{notice}/restore', [NoticeController::class, 'restore']) + ->name('notices.restore'); +Route::delete('/notices/{notice}/force', [NoticeController::class, 'forceDestroy']) + ->name('notices.forceDestroy'); +Route::resource('notices', NoticeController::class); +Route::get('/notices/{notice}/duplicate', [NoticeController::class, 'duplicate']) + ->name('notices.duplicate'); diff --git a/routes/breadcrumbs/notices.php b/routes/breadcrumbs/notices.php new file mode 100644 index 0000000..36d2693 --- /dev/null +++ b/routes/breadcrumbs/notices.php @@ -0,0 +1,39 @@ +push( + Str::ucfirst(trans_choice('models.classes.notices', \INF)), + route('back.notices.index') + ); +}); +Breadcrumbs::for('back.notices.create', function (Generator $trail) { + $trail->parent('back.notices.index'); + $trail->push( + Str::ucfirst(trans_choice('models.classes.notices', 1)), + route('back.notices.create') + ); +}); +Breadcrumbs::for('back.notices.edit', function (Generator $trail) { + $trail->parent('back.notices.index'); + $trail->push(Str::ucfirst(trans('uwil-backend::crud.actions.edit')), route('back.notices.edit', [ + 'notice' => request()->notice + ])); +}); +Breadcrumbs::for('back.notices.duplicate', function (Generator $trail) { + $trail->parent('back.notices.index'); + $trail->push(Str::ucfirst(trans('uwil-backend::crud.actions.duplicate'))); +}); +Breadcrumbs::for('back.notices.show', function (Generator $trail) { + $trail->parent('back.notices.index'); + $trail->push(Str::ucfirst(trans('uwil-backend::crud.actions.visualize')), route('back.notices.show', [ + 'notice' => request()->notice + ])); +}); +Breadcrumbs::for('back.notices.trash', function (Generator $trail) { + $trail->parent('back.notices.index'); + $trail->push(Str::ucfirst(trans('uwil-backend::back.archives'))); +});