diff --git a/README.md b/README.md index 5cecd01..24a70ef 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# SUTERA 3.0 – Guide +# MyKOPKB 1.0 – Guide -Welcome to **SUTERA 3.0**, the streamlined training deployment setup using **Docker**, **Laravel**, and **Vite**. +Welcome to **MyKOPKB 1.0**, the streamlined training deployment setup using **Docker**, **Laravel**, and **Vite**. --- @@ -13,10 +13,6 @@ Welcome to **SUTERA 3.0**, the streamlined training deployment setup using **Doc --- -## Formula Used: -1. KEUPAYAAN (%) = (PEGANGAN/PERJAWATAN) * 100 -2. SIAPSIAGA or KESIAGAAN (%) = (BDG/perjawatan) * 100 -3. SERVISIBILITI (%) = (BDG/pegangan) * 100 ## Laravel command to clear ```bash @@ -38,58 +34,15 @@ sail artisan permission:cache-reset - Servisibility always higher than siapsiaga - Solution — put specific routes first before resource routes - to use soft delete, make id and deleted_at as unique + +# Nwidart Command - to make new module, run: ```bash sail artisan module:make Example ``` -- There are two User model files - -## PKJ Default Varian (DO NOT DELETE) or update. If require update, be sure to update the export name for each PKJ report: -- BANTUAN MOBILITI -- BEKALAN AIR -- EOD/IED/PEMUSNAHAN -- JURUUKUR -- KENDERAAN JENIS A -- KENDERAAN JENIS B -- OPTRONIK DAN PERALATAN LATIHAN -- PELURU DAN BAHAN LETUPAN -- PEMUSNAHAN -- PERALATAN KETUKANGAN -- PERALATAN KOMUNIKASI PERTAHANAN -- PERALATAN PD&P -- PERALATAN PERAIRAN -- PERALATAN PNBK -- SENJATA PERTAHANAN - - -```sql ---- KJC TO UPDATE PEGANGAN IN PERJAWATAN TABLE; CHECK WILL BE UPDATED FIRST -SELECT - ae.id, - ae.entitlement, - ae.holding AS current_holding, - ( - SELECT COUNT(*) - FROM kjc_asset_holdings ah - WHERE ah.kjc_category_id = ae.kjc_category_id - AND ah.kjc_category_id = ae.kjc_category_id - AND ah.unit_id = ae.unit_id - AND ah.deleted_at IS NULL - ) AS actual_holding_count -FROM kjc_asset_entitlements ae -WHERE ae.deleted_at IS NULL; - ---- EXECUTE IT -UPDATE kjc_asset_entitlements ae -SET ae.holding = ( - SELECT COUNT(*) - FROM kjc_asset_holdings ah - WHERE ah.kjc_category_id = ae.kjc_category_id - AND ah.kjc_category_id = ae.kjc_category_id - AND ah.unit_id = ae.unit_id - AND ah.deleted_at IS NULL -) -WHERE ae.deleted_at IS NULL; +- create migration in module +```bash +sail artisan module:make-migration add_more_details_to_users_table User ``` ## Run specific laravel migration: @@ -117,23 +70,7 @@ SELECT last_value FROM public.permissions_id_seq; SELECT setval('public.permissions_id_seq', COALESCE((SELECT MAX(id) FROM permissions), 0) + 1, false); ``` -## Run capture data command -```bash -# kjc -sail artisan kjc:historical:trigger - -# pkj -sail artisan pkj:historical:trigger -``` - -## Auto-generate weekly KJC report (Jabatan Arah RAJD) - Scheduled to run every Monday at 3:00 AM. -```bash -# Manual trigger -sail artisan kjc:auto-generate-weekly-report "Jabatan Arah RAJD" - -# Force regenerate even if reports exist -sail artisan kjc:auto-generate-weekly-report "Jabatan Arah RAJD" --force -``` +--- # Git ## DO NOT RUN THIS BECAUSE IT WILL REMOVE ALL THE UNTAGGED FILES IN LOCAL diff --git a/TODO.md b/TODO.md index 9069e57..ce714ce 100644 --- a/TODO.md +++ b/TODO.md @@ -12,4 +12,7 @@ [ ] wasi/penama [ ] pendaftaran anggota/meneruskan anggota/pencen [ ] daftar lembaga (backdated) -[ ] boleh print semua borang \ No newline at end of file +[ ] boleh print semua borang +[ ] jana surat lepas lulus anggota + +## \ No newline at end of file diff --git a/be/.env.example b/be/.env.example index 2e82c21..30da688 100644 --- a/be/.env.example +++ b/be/.env.example @@ -66,6 +66,14 @@ AWS_USE_PATH_STYLE_ENDPOINT=false VITE_APP_NAME="${APP_NAME}" +# Browsershot (PDF generation via headless Chrome + Puppeteer in be/) +# BROWSERSHOT_NODE_BINARY= +# BROWSERSHOT_NPM_BINARY= +# BROWSERSHOT_NODE_MODULE_PATH= +# BROWSERSHOT_CHROME_PATH= +# BROWSERSHOT_NO_SANDBOX=false +# BROWSERSHOT_TIMEOUT=60 + # HttpOnly Sanctum token cookie (set on api.* domain) AUTH_COOKIE_NAME=auth_token AUTH_COOKIE_LIFETIME=720 diff --git a/be/Modules/Activity/Actions/.gitkeep b/be/Modules/Activity/Actions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Activity/Config/.gitkeep b/be/Modules/Activity/Config/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Activity/Config/config.php b/be/Modules/Activity/Config/config.php new file mode 100644 index 0000000..c4ab535 --- /dev/null +++ b/be/Modules/Activity/Config/config.php @@ -0,0 +1,5 @@ + 'Activity', +]; diff --git a/be/Modules/Activity/Console/.gitkeep b/be/Modules/Activity/Console/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Activity/Database/Factories/.gitkeep b/be/Modules/Activity/Database/Factories/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Activity/Database/Migrations/.gitkeep b/be/Modules/Activity/Database/Migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/database/migrations/2025_07_25_051334_create_countries_table.php b/be/Modules/Activity/Database/Migrations/2026_06_24_003108_create_activity_types_table.php similarity index 66% rename from be/database/migrations/2025_07_25_051334_create_countries_table.php rename to be/Modules/Activity/Database/Migrations/2026_06_24_003108_create_activity_types_table.php index 76ad330..e6c73a5 100644 --- a/be/database/migrations/2025_07_25_051334_create_countries_table.php +++ b/be/Modules/Activity/Database/Migrations/2026_06_24_003108_create_activity_types_table.php @@ -11,12 +11,12 @@ return new class extends Migration */ public function up(): void { - Schema::create('countries', function (Blueprint $table) { + Schema::create('activity_types', function (Blueprint $table) { $table->uuid('id')->primary(); - $table->string('code', 2); - $table->string('name', 100); + $table->string('name'); + $table->string('code')->nullable(); + $table->timestamps(); - $table->softDeletes(); }); } @@ -25,6 +25,6 @@ return new class extends Migration */ public function down(): void { - Schema::dropIfExists('countries'); + Schema::dropIfExists('activity_types'); } }; diff --git a/be/Modules/Activity/Database/Migrations/2026_06_24_083114_create_activities_table.php b/be/Modules/Activity/Database/Migrations/2026_06_24_083114_create_activities_table.php new file mode 100644 index 0000000..c189a33 --- /dev/null +++ b/be/Modules/Activity/Database/Migrations/2026_06_24_083114_create_activities_table.php @@ -0,0 +1,36 @@ +uuid('id')->primary(); + $table->foreignUuid('activity_type_id')->constrained('activity_types')->onDelete('cascade'); + $table->string('title'); + $table->string('reference_number')->nullable(); + $table->text('description')->nullable(); + $table->timestamp('start_datetime')->nullable(); + $table->timestamp('end_datetime')->nullable(); + $table->string('organizer')->nullable(); + $table->string('location')->nullable(); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('activities'); + } +}; diff --git a/be/Modules/Activity/Database/Migrations/2026_06_25_004305_create_activity_reports_table.php b/be/Modules/Activity/Database/Migrations/2026_06_25_004305_create_activity_reports_table.php new file mode 100644 index 0000000..2a4f5e6 --- /dev/null +++ b/be/Modules/Activity/Database/Migrations/2026_06_25_004305_create_activity_reports_table.php @@ -0,0 +1,30 @@ +uuid('id')->primary(); + $table->foreignUuid('activity_id')->constrained('activities')->onDelete('cascade'); + $table->text('report_text'); + $table->foreignUuid('prepared_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('activity_reports'); + } +}; diff --git a/be/Modules/Activity/Database/Seeders/.gitkeep b/be/Modules/Activity/Database/Seeders/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Activity/Database/Seeders/ActivityDatabaseSeeder.php b/be/Modules/Activity/Database/Seeders/ActivityDatabaseSeeder.php new file mode 100644 index 0000000..63dbc3b --- /dev/null +++ b/be/Modules/Activity/Database/Seeders/ActivityDatabaseSeeder.php @@ -0,0 +1,16 @@ +call([]); + } +} diff --git a/be/Modules/Activity/Emails/.gitkeep b/be/Modules/Activity/Emails/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Activity/Entities/.gitkeep b/be/Modules/Activity/Entities/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Activity/Entities/Activity.php b/be/Modules/Activity/Entities/Activity.php new file mode 100644 index 0000000..407b8d5 --- /dev/null +++ b/be/Modules/Activity/Entities/Activity.php @@ -0,0 +1,65 @@ + 'string', + 'reference_number' => 'string', + 'description' => 'string', + 'start_datetime' => 'datetime', + 'end_datetime' => 'datetime', + 'organizer' => 'string', + 'location' => 'string', + 'is_active' => 'boolean', + ]; + } + + public function activityType(): BelongsTo + { + return $this->belongsTo(ActivityType::class); + } + + public function reports(): HasMany + { + return $this->hasMany(ActivityReport::class)->orderByDesc('created_at'); + } + + public function galleryImages(): MorphMany + { + return $this->documents()->where('type', self::GALLERY_DOCUMENT_TYPE); + } + + public function attachments(): MorphMany + { + return $this->documents()->where('type', '!=', self::GALLERY_DOCUMENT_TYPE); + } +} diff --git a/be/Modules/Activity/Entities/ActivityReport.php b/be/Modules/Activity/Entities/ActivityReport.php new file mode 100644 index 0000000..eed0baf --- /dev/null +++ b/be/Modules/Activity/Entities/ActivityReport.php @@ -0,0 +1,38 @@ + 'string', + ]; + } + + public function activity(): BelongsTo + { + return $this->belongsTo(Activity::class); + } + + public function preparedBy(): BelongsTo + { + return $this->belongsTo(User::class, 'prepared_by'); + } +} diff --git a/be/Modules/Activity/Entities/ActivityType.php b/be/Modules/Activity/Entities/ActivityType.php new file mode 100644 index 0000000..692fa9e --- /dev/null +++ b/be/Modules/Activity/Entities/ActivityType.php @@ -0,0 +1,32 @@ + 'string', + 'code' => 'string', + ]; + } + + public function activities(): HasMany + { + return $this->hasMany(Activity::class); + } +} diff --git a/be/Modules/Activity/Helpers/.gitkeep b/be/Modules/Activity/Helpers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Activity/Http/Controllers/.gitkeep b/be/Modules/Activity/Http/Controllers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Activity/Http/Controllers/ActivityController.php b/be/Modules/Activity/Http/Controllers/ActivityController.php new file mode 100644 index 0000000..a0ebdbf --- /dev/null +++ b/be/Modules/Activity/Http/Controllers/ActivityController.php @@ -0,0 +1,345 @@ +authorize('viewAny', $this->modelClass); + + try { + $perPage = $request->get('per_page', 10) ?? 10; + $perPage = min($perPage, 1000); + $search = (string) ($request->get('search', '') ?? ''); + $sortBy = (string) ($request->get('sort_by', 'start_datetime') ?? 'start_datetime'); + $sortOrder = (string) ($request->get('sort_order', 'desc') ?? 'desc'); + + $items = $this->getIndexData($request, $perPage, $search, $sortBy, $sortOrder); + + if ($items instanceof LengthAwarePaginator) { + return response()->json([ + 'success' => true, + 'data' => ActivityListResource::collection($items->items()), + 'pagination' => [ + 'current_page' => $items->currentPage(), + 'per_page' => $items->perPage(), + 'total' => $items->total(), + 'last_page' => $items->lastPage(), + 'from' => $items->firstItem(), + 'to' => $items->lastItem(), + 'has_more_pages' => $items->hasMorePages(), + ], + 'message' => $this->getSuccessMessage('index'), + ]); + } + + return response()->json([ + 'success' => true, + 'data' => ActivityListResource::collection($items), + 'message' => $this->getSuccessMessage('index'), + ]); + } catch (Exception $e) { + Log::error("Error fetching {$this->resourceNamePlural}: ".$e->getMessage()); + + return $this->errorResponse($this->getErrorMessage('index'), 500); + } + } + + protected function getIndexData(Request $request, int $perPage = 10, string $search = '', string $sortBy = 'start_datetime', string $sortOrder = 'desc') + { + if ($perPage > 0) { + return $this->repository->getIndexPaginated($perPage, $search, $sortBy, $sortOrder); + } + + return $this->repository->all($search); + } + + public function store(Request $request): JsonResponse + { + $this->authorize('create', $this->modelClass); + + try { + $validated = $this->validateRequest($request); + $documents = collect($request->file('documents', [])) + ->filter() + ->all(); + $galleryImages = collect($request->file('gallery', [])) + ->filter() + ->all(); + + $activity = $this->activityService->create( + $this->prepareStoreData(collect($validated)->except(['documents', 'gallery'])->all()), + $documents, + $galleryImages, + ); + + ActivityLogger::log("Created {$this->resourceName}: {$this->getItemName($activity)}", $activity); + + return response()->json([ + 'success' => true, + 'data' => new $this->resourceClass($activity), + 'message' => $this->getSuccessMessage('store'), + ], 201); + } catch (Exception $e) { + Log::error("Error creating {$this->resourceName}: ".$e->getMessage()); + + return $this->errorResponse($this->getErrorMessage('store').': '.$e->getMessage(), 500); + } + } + + public function update(Request $request, string $id): JsonResponse + { + $this->authorize('update', $this->modelClass); + + try { + $activity = $this->repository->findById($id); + + if (! $activity) { + return $this->errorResponse($this->getNotFoundMessage(), 404); + } + + $validated = $this->validateRequest($request); + $documents = collect($request->file('documents', [])) + ->filter() + ->all(); + $galleryImages = collect($request->file('gallery', [])) + ->filter() + ->all(); + + $activity = $this->activityService->update( + $activity, + $this->prepareUpdateData(collect($validated)->except(['documents', 'gallery'])->all(), $activity), + $documents, + $galleryImages, + ); + + ActivityLogger::log("Updated {$this->resourceName}: {$this->getItemName($activity)}", $activity); + + return response()->json([ + 'success' => true, + 'data' => new $this->resourceClass($activity), + 'message' => $this->getSuccessMessage('update'), + ]); + } catch (Exception $e) { + Log::error("Error updating {$this->resourceName}: ".$e->getMessage()); + + return $this->errorResponse($this->getErrorMessage('update').': '.$e->getMessage(), 500); + } + } + + protected function loadShowRelations($item) + { + return $this->repository->findByIdWithRelations($item->id) ?? $item; + } + + protected function getItemName($item): string + { + return $item->title ?? $item->id; + } + + protected function checkDependencies($activity): ?JsonResponse + { + if ($activity->reports()->count() > 0) { + return response()->json([ + 'success' => false, + 'message' => 'Aktiviti tidak boleh dipadam kerana masih mempunyai laporan.', + ], 422); + } + + return null; + } + + public function uploadDocument( + UploadActivityDocumentRequest $request, + Activity $activity + ): JsonResponse { + $this->authorize('update', $this->modelClass); + + try { + $activity = $this->activityService->uploadDocument( + $activity, + $request->file('file'), + $request->validated('type', 'general'), + $request->validated('description'), + ); + + return response()->json([ + 'success' => true, + 'message' => 'Dokumen berjaya dimuat naik.', + 'data' => new ActivityResource($activity), + ]); + } catch (Exception $e) { + Log::error("Error uploading document for {$this->resourceName}: ".$e->getMessage()); + + return $this->errorResponse('Gagal memuat naik dokumen: '.$e->getMessage(), 500); + } + } + + public function uploadGalleryImage(UploadActivityGalleryImageRequest $request, Activity $activity): JsonResponse { + $this->authorize('update', $this->modelClass); + + try { + $activity = $this->activityService->uploadGalleryImage( + $activity, + $request->file('file'), + $request->validated('description'), + ); + + return response()->json([ + 'success' => true, + 'message' => 'Imej galeri berjaya dimuat naik.', + 'data' => new ActivityResource($activity), + ]); + } catch (Exception $e) { + Log::error("Error uploading gallery image for {$this->resourceName}: ".$e->getMessage()); + + return $this->errorResponse('Gagal memuat naik imej galeri: '.$e->getMessage(), 500); + } + } + + public function downloadDocument(Activity $activity, string $documentId): StreamedResponse + { + $this->authorize('view', $this->modelClass); + + $document = $activity->documents() + ->where('type', '!=', Activity::GALLERY_DOCUMENT_TYPE) + ->findOrFail($documentId); + + return $this->documentService->downloadDocument($document->id); + } + + public function serveGalleryImage(Activity $activity, string $imageId): BinaryFileResponse + { + $this->authorize('view', $this->modelClass); + + $document = $activity->galleryImages()->findOrFail($imageId); + + $disk = Storage::disk(Document::STORAGE_DISK); + + if (! $disk->exists($document->path)) { + abort(404, 'File not found'); + } + + return response()->file($disk->path($document->path), [ + 'Content-Type' => $document->mime_type ?? 'application/octet-stream', + 'Content-Disposition' => 'inline; filename="'.$document->name.'"', + ]); + } + + public function deleteDocument(Activity $activity, string $documentId): JsonResponse + { + $this->authorize('update', $this->modelClass); + + try { + $activity->documents() + ->where('type', '!=', Activity::GALLERY_DOCUMENT_TYPE) + ->findOrFail($documentId); + + $activity->deleteDocument($documentId); + $activity = $this->repository->findByIdWithRelations($activity->id); + + return response()->json([ + 'success' => true, + 'message' => 'Dokumen berjaya dipadam.', + 'data' => new ActivityResource($activity), + ]); + } catch (Exception $e) { + Log::error("Error deleting document for {$this->resourceName}: ".$e->getMessage()); + + return $this->errorResponse('Gagal memadam dokumen: '.$e->getMessage(), 500); + } + } + + public function deleteGalleryImage(Activity $activity, string $imageId): JsonResponse + { + $this->authorize('update', $this->modelClass); + + try { + $activity->galleryImages()->findOrFail($imageId); + $activity->deleteDocument($imageId); + $activity = $this->repository->findByIdWithRelations($activity->id); + + return response()->json([ + 'success' => true, + 'message' => 'Imej galeri berjaya dipadam.', + 'data' => new ActivityResource($activity), + ]); + } catch (Exception $e) { + Log::error("Error deleting gallery image for {$this->resourceName}: ".$e->getMessage()); + + return $this->errorResponse('Gagal memadam imej galeri: '.$e->getMessage(), 500); + } + } + + public function destroy(string $id): JsonResponse + { + $this->authorize('deleteAny', $this->modelClass); + + try { + $activity = $this->repository->findByIdWithRelations($id); + + if (! $activity) { + return $this->errorResponse($this->getNotFoundMessage(), 404); + } + + $dependencyCheck = $this->checkDependencies($activity); + if ($dependencyCheck !== null) { + return $dependencyCheck; + } + + $activity->documents->each( + fn ($document) => $this->documentService->deleteDocument($document->id) + ); + + $this->repository->delete($id); + + ActivityLogger::log("Deleted {$this->resourceName}: {$this->getItemName($activity)}", $activity); + + return response()->json([ + 'success' => true, + 'message' => $this->getSuccessMessage('destroy'), + ]); + } catch (Exception $e) { + Log::error("Error deleting {$this->resourceName}: ".$e->getMessage()); + + return $this->errorResponse($this->getErrorMessage('destroy').': '.$e->getMessage(), 500); + } + } +} diff --git a/be/Modules/Activity/Http/Controllers/ActivityReportController.php b/be/Modules/Activity/Http/Controllers/ActivityReportController.php new file mode 100644 index 0000000..006ba41 --- /dev/null +++ b/be/Modules/Activity/Http/Controllers/ActivityReportController.php @@ -0,0 +1,40 @@ + auth()->id(), + ]); + } + + protected function getItemName($item): string + { + return $item->activity?->title ?? $item->id; + } +} diff --git a/be/Modules/Activity/Http/Controllers/ActivityTypeController.php b/be/Modules/Activity/Http/Controllers/ActivityTypeController.php new file mode 100644 index 0000000..7707fc4 --- /dev/null +++ b/be/Modules/Activity/Http/Controllers/ActivityTypeController.php @@ -0,0 +1,42 @@ +name ?? $item->id; + } + + protected function checkDependencies($activityType): ?JsonResponse + { + if ($activityType->activities()->count() > 0) { + return response()->json([ + 'success' => false, + 'message' => 'Jenis aktiviti tidak boleh dipadam kerana masih digunakan oleh aktiviti.', + ], 422); + } + + return null; + } +} diff --git a/be/Modules/Activity/Http/Requests/.gitkeep b/be/Modules/Activity/Http/Requests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Activity/Http/Requests/ActivityReportRequest.php b/be/Modules/Activity/Http/Requests/ActivityReportRequest.php new file mode 100644 index 0000000..73ff3aa --- /dev/null +++ b/be/Modules/Activity/Http/Requests/ActivityReportRequest.php @@ -0,0 +1,40 @@ +isMethod('POST'); + $isUpdate = $this->isMethod('PUT') || $this->isMethod('PATCH'); + + if (! $isCreate && ! $isUpdate) { + return [ + 'activity_id' => 'nullable|uuid|exists:activities,id', + 'report_text' => 'nullable|string', + ]; + } + + return [ + 'activity_id' => 'required|uuid|exists:activities,id', + 'report_text' => 'required|string', + ]; + } + + public function messages(): array + { + return [ + 'activity_id.required' => 'Aktiviti diperlukan.', + 'activity_id.exists' => 'Aktiviti tidak sah.', + 'report_text.required' => 'Laporan diperlukan.', + ]; + } +} diff --git a/be/Modules/Activity/Http/Requests/ActivityRequest.php b/be/Modules/Activity/Http/Requests/ActivityRequest.php new file mode 100644 index 0000000..fed44a4 --- /dev/null +++ b/be/Modules/Activity/Http/Requests/ActivityRequest.php @@ -0,0 +1,76 @@ +isMethod('POST'); + $isUpdate = $this->isMethod('PUT') || $this->isMethod('PATCH'); + + if (! $isCreate && ! $isUpdate) { + return [ + 'activity_type_id' => 'nullable|uuid|exists:activity_types,id', + 'title' => 'nullable|string|max:255', + 'reference_number' => 'nullable|string|max:255', + 'description' => 'nullable|string', + 'start_datetime' => 'nullable|date', + 'end_datetime' => 'nullable|date|after_or_equal:start_datetime', + 'organizer' => 'nullable|string|max:255', + 'location' => 'nullable|string|max:255', + 'is_active' => 'nullable|boolean', + ]; + } + + $referenceRule = Rule::unique('activities', 'reference_number'); + + if ($isUpdate) { + $referenceRule = $referenceRule->ignore($this->route('activity')); + } + + return [ + 'activity_type_id' => 'required|uuid|exists:activity_types,id', + 'title' => 'required|string|max:255', + 'reference_number' => ['nullable', 'string', 'max:255', $referenceRule], + 'description' => 'nullable|string', + 'start_datetime' => 'nullable|date', + 'end_datetime' => 'nullable|date|after_or_equal:start_datetime', + 'organizer' => 'nullable|string|max:255', + 'location' => 'nullable|string|max:255', + 'is_active' => 'required|boolean', + 'documents' => 'nullable|array', + 'documents.*' => 'file|mimes:pdf,jpg,jpeg,png,doc,docx|max:10240', + 'gallery' => 'nullable|array', + 'gallery.*' => 'file|mimes:jpg,jpeg,png,webp,gif|max:10240', + ]; + } + + public function messages(): array + { + return [ + 'activity_type_id.required' => 'Jenis aktiviti diperlukan.', + 'activity_type_id.exists' => 'Jenis aktiviti tidak sah.', + 'title.required' => 'Tajuk aktiviti diperlukan.', + 'title.max' => 'Tajuk aktiviti tidak boleh melebihi 255 aksara.', + 'reference_number.unique' => 'Nombor rujukan sudah wujud.', + 'end_datetime.after_or_equal' => 'Tarikh tamat mestilah selepas atau sama dengan tarikh mula.', + 'is_active.required' => 'Status aktif diperlukan.', + 'is_active.boolean' => 'Status aktif tidak sah.', + 'documents.*.file' => 'Dokumen mestilah fail yang sah.', + 'documents.*.mimes' => 'Format dokumen tidak disokong.', + 'documents.*.max' => 'Saiz dokumen melebihi had maksimum 10MB.', + 'gallery.*.file' => 'Imej galeri mestilah fail yang sah.', + 'gallery.*.mimes' => 'Format imej galeri tidak disokong. Gunakan jpg, jpeg, png, webp atau gif.', + 'gallery.*.max' => 'Saiz imej galeri melebihi had maksimum 10MB.', + ]; + } +} diff --git a/be/Modules/Activity/Http/Requests/ActivityTypeRequest.php b/be/Modules/Activity/Http/Requests/ActivityTypeRequest.php new file mode 100644 index 0000000..723dc2b --- /dev/null +++ b/be/Modules/Activity/Http/Requests/ActivityTypeRequest.php @@ -0,0 +1,48 @@ +isMethod('POST'); + $isUpdate = $this->isMethod('PUT') || $this->isMethod('PATCH'); + + if (! $isCreate && ! $isUpdate) { + return [ + 'name' => 'nullable|string|max:255', + 'code' => 'nullable|string|max:255', + ]; + } + + $codeRule = Rule::unique('activity_types', 'code'); + + if ($isUpdate) { + $codeRule = $codeRule->ignore($this->route('activity_type')); + } + + return [ + 'name' => 'required|string|max:255', + 'code' => ['nullable', 'string', 'max:255', $codeRule], + ]; + } + + public function messages(): array + { + return [ + 'name.required' => 'Nama jenis aktiviti diperlukan.', + 'name.max' => 'Nama jenis aktiviti tidak boleh melebihi 255 aksara.', + 'code.max' => 'Kod jenis aktiviti tidak boleh melebihi 255 aksara.', + 'code.unique' => 'Kod jenis aktiviti sudah wujud.', + ]; + } +} diff --git a/be/Modules/Activity/Http/Requests/UploadActivityDocumentRequest.php b/be/Modules/Activity/Http/Requests/UploadActivityDocumentRequest.php new file mode 100644 index 0000000..e715ab7 --- /dev/null +++ b/be/Modules/Activity/Http/Requests/UploadActivityDocumentRequest.php @@ -0,0 +1,39 @@ + [ + 'nullable', + 'string', + 'max:255', + Rule::notIn([Activity::GALLERY_DOCUMENT_TYPE]), + ], + 'description' => 'nullable|string|max:1000', + 'file' => 'required|file|mimes:pdf,jpg,jpeg,png,doc,docx|max:10240', + ]; + } + + public function messages(): array + { + return [ + 'type.not_in' => 'Imej galeri mesti dimuat naik melalui endpoint galeri aktiviti.', + 'file.required' => 'Fail diperlukan.', + 'file.mimes' => 'Format fail tidak disokong.', + 'file.max' => 'Saiz fail melebihi had maksimum 10MB.', + ]; + } +} diff --git a/be/Modules/Activity/Http/Requests/UploadActivityGalleryImageRequest.php b/be/Modules/Activity/Http/Requests/UploadActivityGalleryImageRequest.php new file mode 100644 index 0000000..c8d21e1 --- /dev/null +++ b/be/Modules/Activity/Http/Requests/UploadActivityGalleryImageRequest.php @@ -0,0 +1,30 @@ + 'required|file|mimes:jpg,jpeg,png,webp,gif|max:10240', + 'description' => 'nullable|string|max:1000', + ]; + } + + public function messages(): array + { + return [ + 'file.required' => 'Imej diperlukan.', + 'file.mimes' => 'Format imej tidak disokong. Gunakan jpg, jpeg, png, webp atau gif.', + 'file.max' => 'Saiz imej melebihi had maksimum 10MB.', + ]; + } +} diff --git a/be/Modules/Activity/Jobs/.gitkeep b/be/Modules/Activity/Jobs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Activity/Notifications/.gitkeep b/be/Modules/Activity/Notifications/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Activity/Policies/.gitkeep b/be/Modules/Activity/Policies/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Activity/Policies/ActivityPolicy.php b/be/Modules/Activity/Policies/ActivityPolicy.php new file mode 100644 index 0000000..ac7a46b --- /dev/null +++ b/be/Modules/Activity/Policies/ActivityPolicy.php @@ -0,0 +1,41 @@ +hasPermissionTo('lihat aktiviti'); + } + + public function view($user, ?Activity $activity = null): bool + { + return $user->hasPermissionTo('lihat aktiviti'); + } + + public function create($user): bool + { + return $user->hasPermissionTo('tambah aktiviti'); + } + + public function update($user, ?Activity $activity = null): bool + { + return $user->hasPermissionTo('kemaskini aktiviti'); + } + + public function deleteAny($user): bool + { + return $user->hasPermissionTo('hapus aktiviti'); + } + + public function delete($user, ?Activity $activity = null): bool + { + return $user->hasPermissionTo('hapus aktiviti'); + } +} diff --git a/be/Modules/Activity/Policies/ActivityReportPolicy.php b/be/Modules/Activity/Policies/ActivityReportPolicy.php new file mode 100644 index 0000000..a82f136 --- /dev/null +++ b/be/Modules/Activity/Policies/ActivityReportPolicy.php @@ -0,0 +1,41 @@ +hasPermissionTo('tambah laporan aktiviti'); + } + + public function update($user, ?ActivityReport $activityReport = null): bool + { + return $user->hasPermissionTo('kemaskini laporan aktiviti'); + } + + public function deleteAny($user): bool + { + return $user->hasPermissionTo('hapus laporan aktiviti'); + } + + public function delete($user, ?ActivityReport $activityReport = null): bool + { + return $user->hasPermissionTo('hapus laporan aktiviti'); + } +} diff --git a/be/Modules/Activity/Policies/ActivityTypePolicy.php b/be/Modules/Activity/Policies/ActivityTypePolicy.php new file mode 100644 index 0000000..4824517 --- /dev/null +++ b/be/Modules/Activity/Policies/ActivityTypePolicy.php @@ -0,0 +1,41 @@ +hasPermissionTo('tambah jenis aktiviti'); + } + + public function update($user, ?ActivityType $activityType = null): bool + { + return $user->hasPermissionTo('kemaskini jenis aktiviti'); + } + + public function deleteAny($user): bool + { + return $user->hasPermissionTo('hapus jenis aktiviti'); + } + + public function delete($user, ?ActivityType $activityType = null): bool + { + return $user->hasPermissionTo('hapus jenis aktiviti'); + } +} diff --git a/be/Modules/Activity/Providers/.gitkeep b/be/Modules/Activity/Providers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Activity/Providers/ActivityServiceProvider.php b/be/Modules/Activity/Providers/ActivityServiceProvider.php new file mode 100644 index 0000000..d41dab5 --- /dev/null +++ b/be/Modules/Activity/Providers/ActivityServiceProvider.php @@ -0,0 +1,184 @@ +registerCommands(); + $this->registerCommandSchedules(); + $this->registerTranslations(); + $this->registerConfig(); + $this->registerViews(); + $this->loadMigrationsFrom(module_path($this->name, 'Database/Migrations')); + $this->registerPolicies(); + } + + /** + * Register the service provider. + */ + public function register(): void + { + $this->app->register(EventServiceProvider::class); + $this->app->register(RouteServiceProvider::class); + + $this->app->bind( + \Modules\Activity\Repositories\Contracts\ActivityTypeRepositoryInterface::class, + \Modules\Activity\Repositories\ActivityTypeRepository::class + ); + + $this->app->bind( + \Modules\Activity\Repositories\Contracts\ActivityRepositoryInterface::class, + \Modules\Activity\Repositories\ActivityRepository::class + ); + + $this->app->bind( + \Modules\Activity\Repositories\Contracts\ActivityReportRepositoryInterface::class, + \Modules\Activity\Repositories\ActivityReportRepository::class + ); + } + + protected function registerPolicies(): void + { + Gate::policy(ActivityType::class, ActivityTypePolicy::class); + Gate::policy(Activity::class, ActivityPolicy::class); + Gate::policy(ActivityReport::class, ActivityReportPolicy::class); + } + + /** + * Register commands in the format of Command::class + */ + protected function registerCommands(): void + { + // $this->commands([]); + } + + /** + * Register command Schedules. + */ + protected function registerCommandSchedules(): void + { + // $this->app->booted(function () { + // $schedule = $this->app->make(Schedule::class); + // $schedule->command('inspire')->hourly(); + // }); + } + + /** + * Register translations. + */ + public function registerTranslations(): void + { + $langPath = resource_path('lang/modules/'.$this->nameLower); + + if (is_dir($langPath)) { + $this->loadTranslationsFrom($langPath, $this->nameLower); + $this->loadJsonTranslationsFrom($langPath); + } else { + $this->loadTranslationsFrom(module_path($this->name, 'Lang'), $this->nameLower); + $this->loadJsonTranslationsFrom(module_path($this->name, 'Lang')); + } + } + + /** + * Register config. + */ + protected function registerConfig(): void + { + $configPath = module_path($this->name, config('modules.paths.generator.config.path')); + + if (is_dir($configPath)) { + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($configPath)); + + foreach ($iterator as $file) { + if ($file->isFile() && $file->getExtension() === 'php') { + $config = str_replace($configPath.DIRECTORY_SEPARATOR, '', $file->getPathname()); + $config_key = str_replace([DIRECTORY_SEPARATOR, '.php'], ['.', ''], $config); + $segments = explode('.', $this->nameLower.'.'.$config_key); + + // Remove duplicated adjacent segments + $normalized = []; + foreach ($segments as $segment) { + if (end($normalized) !== $segment) { + $normalized[] = $segment; + } + } + + $key = ($config === 'config.php') ? $this->nameLower : implode('.', $normalized); + + $this->publishes([$file->getPathname() => config_path($config)], 'config'); + $this->merge_config_from($file->getPathname(), $key); + } + } + } + } + + /** + * Merge config from the given path recursively. + */ + protected function merge_config_from(string $path, string $key): void + { + $existing = config($key, []); + $module_config = require $path; + + config([$key => array_replace_recursive($existing, $module_config)]); + } + + /** + * Register views. + */ + public function registerViews(): void + { + $viewPath = resource_path('views/modules/'.$this->nameLower); + $sourcePath = module_path($this->name, 'Resources/Views'); + + $this->publishes([$sourcePath => $viewPath], ['views', $this->nameLower.'-module-views']); + + $this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), $this->nameLower); + + Blade::componentNamespace(config('modules.namespace').'\\'.$this->name.'\\View\\Components', $this->nameLower); + } + + /** + * Get the services provided by the provider. + */ + public function provides(): array + { + return []; + } + + private function getPublishableViewPaths(): array + { + $paths = []; + foreach (config('view.paths') as $path) { + if (is_dir($path.'/modules/'.$this->nameLower)) { + $paths[] = $path.'/modules/'.$this->nameLower; + } + } + + return $paths; + } +} diff --git a/be/Modules/Activity/Providers/EventServiceProvider.php b/be/Modules/Activity/Providers/EventServiceProvider.php new file mode 100644 index 0000000..67ff054 --- /dev/null +++ b/be/Modules/Activity/Providers/EventServiceProvider.php @@ -0,0 +1,27 @@ +> + */ + protected $listen = []; + + /** + * Indicates if events should be discovered. + * + * @var bool + */ + protected static $shouldDiscoverEvents = true; + + /** + * Configure the proper event listeners for email verification. + */ + protected function configureEmailVerification(): void {} +} diff --git a/be/Modules/Activity/Providers/RouteServiceProvider.php b/be/Modules/Activity/Providers/RouteServiceProvider.php new file mode 100644 index 0000000..f2d7d5e --- /dev/null +++ b/be/Modules/Activity/Providers/RouteServiceProvider.php @@ -0,0 +1,50 @@ +mapApiRoutes(); + + } + + /** + * Define the "web" routes for the application. + * + * These routes all receive session state, CSRF protection, etc. + */ + protected function mapWebRoutes(): void + { + Route::middleware('web')->group(module_path($this->name, '/Routes/web.php')); + } + + /** + * Define the "api" routes for the application. + * + * These routes are typically stateless. + */ + protected function mapApiRoutes(): void + { + Route::middleware('api')->name('api.')->group(module_path($this->name, '/Routes/api.php')); + } +} diff --git a/be/Modules/Activity/Repositories/.gitkeep b/be/Modules/Activity/Repositories/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Activity/Repositories/ActivityReportRepository.php b/be/Modules/Activity/Repositories/ActivityReportRepository.php new file mode 100644 index 0000000..54e2458 --- /dev/null +++ b/be/Modules/Activity/Repositories/ActivityReportRepository.php @@ -0,0 +1,98 @@ +with($this->defaultRelations) + ->orderBy('created_at', 'desc'); + } + + protected function applySearch($query, string $search) + { + if ($search !== '') { + $query->where(function ($q) use ($search) { + $q->where('report_text', 'ILIKE', "%{$search}%") + ->orWhereHas('activity', function ($activityQuery) use ($search) { + $activityQuery->where('title', 'ILIKE', "%{$search}%"); + }); + }); + } + + return $query; + } + + public function getAllPaginated(int $perPage = 10, string $search = '') + { + $query = $this->baseQuery(); + $this->applySearch($query, $search); + + return $query->paginate($perPage); + } + + public function getAllWithRelationsPaginated( + int $perPage = 10, + string $search = '', + string $sortBy = 'created_at', + string $sortOrder = 'desc' + ) { + $allowedSortColumns = ['id', 'report_text', 'created_at']; + $sortBy = in_array($sortBy, $allowedSortColumns, true) ? $sortBy : 'created_at'; + $sortOrder = strtolower($sortOrder) === 'desc' ? 'desc' : 'asc'; + + $query = ActivityReport::query() + ->with($this->defaultRelations) + ->orderBy($sortBy, $sortOrder); + $this->applySearch($query, $search); + + return $query->paginate($perPage); + } + + public function getAllWithRelations(string $search = ''): Collection + { + $query = $this->baseQuery(); + $this->applySearch($query, $search); + + return $query->get(); + } + + public function create(array $data): ActivityReport + { + return ActivityReport::create($data); + } + + public function findById(string $id): ?ActivityReport + { + return ActivityReport::query() + ->with($this->defaultRelations) + ->find($id); + } + + public function delete(string $id): bool + { + $report = ActivityReport::query()->find($id); + + if ($report) { + return $report->delete(); + } + + return false; + } + + public function all(string $search = ''): Collection + { + $query = $this->baseQuery(); + $this->applySearch($query, $search); + + return $query->get(); + } +} diff --git a/be/Modules/Activity/Repositories/ActivityRepository.php b/be/Modules/Activity/Repositories/ActivityRepository.php new file mode 100644 index 0000000..d45617d --- /dev/null +++ b/be/Modules/Activity/Repositories/ActivityRepository.php @@ -0,0 +1,151 @@ +with($this->defaultRelations) + ->orderBy('start_datetime', 'desc'); + } + + protected function applySearch($query, string $search) + { + if ($search !== '') { + $query->where(function ($q) use ($search) { + $q->where('title', 'ILIKE', "%{$search}%") + ->orWhere('reference_number', 'ILIKE', "%{$search}%") + ->orWhere('organizer', 'ILIKE', "%{$search}%") + ->orWhere('location', 'ILIKE', "%{$search}%"); + }); + } + + return $query; + } + + public function getAllPaginated(int $perPage = 10, string $search = '') + { + $query = $this->baseQuery(); + $this->applySearch($query, $search); + + return $query->paginate($perPage); + } + + public function getAllWithRelationsPaginated( + int $perPage = 10, + string $search = '', + string $sortBy = 'title', + string $sortOrder = 'asc' + ) { + $allowedSortColumns = [ + 'id', + 'title', + 'reference_number', + 'start_datetime', + 'end_datetime', + 'organizer', + 'location', + 'is_active', + 'created_at', + ]; + $sortBy = in_array($sortBy, $allowedSortColumns, true) ? $sortBy : 'start_datetime'; + $sortOrder = strtolower($sortOrder) === 'desc' ? 'desc' : 'asc'; + + $query = Activity::query() + ->with($this->defaultRelations) + ->orderBy($sortBy, $sortOrder); + $this->applySearch($query, $search); + + return $query->paginate($perPage); + } + + public function getIndexPaginated( + int $perPage = 10, + string $search = '', + string $sortBy = 'start_datetime', + string $sortOrder = 'desc' + ) { + $allowedSortColumns = [ + 'id', + 'title', + 'reference_number', + 'start_datetime', + 'end_datetime', + 'organizer', + 'location', + 'is_active', + 'created_at', + ]; + $sortBy = in_array($sortBy, $allowedSortColumns, true) ? $sortBy : 'start_datetime'; + $sortOrder = strtolower($sortOrder) === 'desc' ? 'desc' : 'asc'; + + $query = Activity::query() + ->with([ + 'activityType', + 'galleryImages' => fn ($q) => $q->orderBy('created_at')->limit(1), + ]) + ->withCount(['galleryImages', 'attachments']) + ->orderBy($sortBy, $sortOrder); + $this->applySearch($query, $search); + + return $query->paginate($perPage); + } + + public function getAllWithRelations(string $search = ''): Collection + { + $query = $this->baseQuery(); + $this->applySearch($query, $search); + + return $query->get(); + } + + public function create(array $data): Activity + { + return Activity::create($data); + } + + public function findById(string $id): ?Activity + { + return Activity::query()->find($id); + } + + public function findByIdWithRelations(string $id): ?Activity + { + return Activity::query() + ->with($this->detailRelations) + ->find($id); + } + + public function delete(string $id): bool + { + $activity = $this->findById($id); + + if ($activity) { + return $activity->delete(); + } + + return false; + } + + public function all(string $search = ''): Collection + { + $query = $this->baseQuery(); + $this->applySearch($query, $search); + + return $query->get(); + } +} diff --git a/be/Modules/Activity/Repositories/ActivityTypeRepository.php b/be/Modules/Activity/Repositories/ActivityTypeRepository.php new file mode 100644 index 0000000..8d79d89 --- /dev/null +++ b/be/Modules/Activity/Repositories/ActivityTypeRepository.php @@ -0,0 +1,88 @@ +orderBy('name'); + } + + protected function applySearch($query, string $search) + { + if ($search !== '') { + $query->where(function ($q) use ($search) { + $q->where('name', 'ILIKE', "%{$search}%") + ->orWhere('code', 'ILIKE', "%{$search}%"); + }); + } + + return $query; + } + + public function getAllPaginated(int $perPage = 10, string $search = '') + { + $query = $this->baseQuery(); + $this->applySearch($query, $search); + + return $query->paginate($perPage); + } + + public function getAllWithRelationsPaginated( + int $perPage = 10, + string $search = '', + string $sortBy = 'name', + string $sortOrder = 'asc' + ) { + $allowedSortColumns = ['id', 'name', 'code', 'created_at']; + $sortBy = in_array($sortBy, $allowedSortColumns, true) ? $sortBy : 'name'; + $sortOrder = strtolower($sortOrder) === 'desc' ? 'desc' : 'asc'; + + $query = ActivityType::query()->orderBy($sortBy, $sortOrder); + $this->applySearch($query, $search); + + return $query->paginate($perPage); + } + + public function getAllWithRelations(string $search = ''): Collection + { + $query = $this->baseQuery(); + $this->applySearch($query, $search); + + return $query->get(); + } + + public function create(array $data): ActivityType + { + return ActivityType::create($data); + } + + public function findById(string $id): ?ActivityType + { + return ActivityType::query()->find($id); + } + + public function delete(string $id): bool + { + $activityType = $this->findById($id); + + if ($activityType) { + return $activityType->delete(); + } + + return false; + } + + public function all(string $search = ''): Collection + { + $query = $this->baseQuery(); + $this->applySearch($query, $search); + + return $query->get(); + } +} diff --git a/be/Modules/Activity/Repositories/Contracts/.gitkeep b/be/Modules/Activity/Repositories/Contracts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Activity/Repositories/Contracts/ActivityReportRepositoryInterface.php b/be/Modules/Activity/Repositories/Contracts/ActivityReportRepositoryInterface.php new file mode 100644 index 0000000..a771c18 --- /dev/null +++ b/be/Modules/Activity/Repositories/Contracts/ActivityReportRepositoryInterface.php @@ -0,0 +1,28 @@ +prefix('v1')->group(function () { + Route::apiResource('activity-types', ActivityTypeController::class)->names('activity-type'); + Route::apiResource('activities', ActivityController::class)->names('activity'); + + // gallery images + Route::post('activities/{activity}/gallery', [ActivityController::class, 'uploadGalleryImage'])->name('activity.upload-gallery-image'); + Route::get('activities/{activity}/gallery/{imageId}', [ActivityController::class, 'serveGalleryImage'])->name('activity.serve-gallery-image'); + Route::delete('activities/{activity}/gallery/{imageId}', [ActivityController::class, 'deleteGalleryImage'])->name('activity.delete-gallery-image'); + + // documents + Route::post('activities/{activity}/documents', [ActivityController::class, 'uploadDocument'])->name('activity.upload-document'); + Route::get('activities/{activity}/documents/{documentId}/download', [ActivityController::class, 'downloadDocument'])->name('activity.download-document'); + Route::delete('activities/{activity}/documents/{documentId}', [ActivityController::class, 'deleteDocument'])->name('activity.delete-document'); + + // reports + Route::apiResource('activity-reports', ActivityReportController::class)->names('activity-report'); +}); diff --git a/be/Modules/Activity/Routes/web.php b/be/Modules/Activity/Routes/web.php new file mode 100644 index 0000000..31f6ce8 --- /dev/null +++ b/be/Modules/Activity/Routes/web.php @@ -0,0 +1,8 @@ +group(function () { + Route::resource('activities', ActivityController::class)->names('activity'); +}); diff --git a/be/Modules/Activity/Services/.gitkeep b/be/Modules/Activity/Services/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Activity/Services/ActivityService.php b/be/Modules/Activity/Services/ActivityService.php new file mode 100644 index 0000000..d47c76b --- /dev/null +++ b/be/Modules/Activity/Services/ActivityService.php @@ -0,0 +1,140 @@ + $data + * @param array|array $documents + * @param array $galleryImages + */ + public function create(array $data, array $documents = [], array $galleryImages = []): Activity + { + return DB::transaction(function () use ($data, $documents, $galleryImages) { + $activity = $this->repository->create($data); + + $this->uploadDocuments($activity, $documents); + $this->uploadGalleryImages($activity, $galleryImages); + + return $this->repository->findByIdWithRelations($activity->id); + }); + } + + /** + * @param array $data + * @param array|array $documents + * @param array $galleryImages + */ + public function update(Activity $activity, array $data, array $documents = [], array $galleryImages = []): Activity + { + return DB::transaction(function () use ($activity, $data, $documents, $galleryImages) { + $activity->update($data); + + $this->uploadDocuments($activity, $documents); + $this->uploadGalleryImages($activity, $galleryImages); + + return $this->repository->findByIdWithRelations($activity->id); + }); + } + + public function uploadDocument( + Activity $activity, + UploadedFile $file, + string $type = 'general', + ?string $description = null + ): Activity { + if ($type === Activity::GALLERY_DOCUMENT_TYPE) { + throw new Exception('Imej galeri mesti dimuat naik melalui endpoint galeri aktiviti.'); + } + + $this->documentService->validateFile($file); + $this->documentService->uploadDocument($activity, $file, $type, $description); + + return $this->repository->findByIdWithRelations($activity->id); + } + + public function uploadGalleryImage( + Activity $activity, + UploadedFile $file, + ?string $description = null + ): Activity { + $this->validateGalleryImage($file); + $this->documentService->uploadDocument( + $activity, + $file, + Activity::GALLERY_DOCUMENT_TYPE, + $description + ); + + return $this->repository->findByIdWithRelations($activity->id); + } + + /** + * @param array|array $documents + */ + protected function uploadDocuments(Activity $activity, array $documents): void + { + foreach ($documents as $key => $file) { + if (! $file instanceof UploadedFile) { + continue; + } + + $documentType = is_string($key) ? $key : 'general'; + + if ($documentType === Activity::GALLERY_DOCUMENT_TYPE) { + $this->validateGalleryImage($file); + $this->documentService->uploadDocument($activity, $file, Activity::GALLERY_DOCUMENT_TYPE); + + continue; + } + + $this->documentService->validateFile($file); + $this->documentService->uploadDocument($activity, $file, $documentType); + } + } + + /** + * @param array $galleryImages + */ + protected function uploadGalleryImages(Activity $activity, array $galleryImages): void + { + foreach ($galleryImages as $file) { + if (! $file instanceof UploadedFile) { + continue; + } + + $this->validateGalleryImage($file); + $this->documentService->uploadDocument( + $activity, + $file, + Activity::GALLERY_DOCUMENT_TYPE + ); + } + } + + protected function validateGalleryImage(UploadedFile $file): void + { + $validator = Validator::make( + ['file' => $file], + ['file' => 'required|file|mimes:jpg,jpeg,png,webp,gif|max:10240'] + ); + + if ($validator->fails()) { + throw new Exception($validator->errors()->first('file')); + } + } +} diff --git a/be/Modules/Activity/Tests/Feature/.gitkeep b/be/Modules/Activity/Tests/Feature/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Activity/Tests/Unit/.gitkeep b/be/Modules/Activity/Tests/Unit/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Activity/Transformers/.gitkeep b/be/Modules/Activity/Transformers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Activity/Transformers/ActivityDocumentResource.php b/be/Modules/Activity/Transformers/ActivityDocumentResource.php new file mode 100644 index 0000000..9d436d5 --- /dev/null +++ b/be/Modules/Activity/Transformers/ActivityDocumentResource.php @@ -0,0 +1,32 @@ +documentable_id; + + return [ + 'id' => $this->id, + 'activity_id' => $activityId, + 'name' => $this->name, + 'type' => $this->type, + 'description' => $this->description, + 'mime_type' => $this->mime_type, + 'file_size' => $this->file_size, + 'uploaded_by' => $this->uploaded_by, + 'url' => $activityId + ? url(route('api.activity.download-document', [ + 'activity' => $activityId, + 'documentId' => $this->id, + ])) + : null, + 'created_at' => $this->created_at?->toISOString(), + ]; + } +} diff --git a/be/Modules/Activity/Transformers/ActivityGalleryImageResource.php b/be/Modules/Activity/Transformers/ActivityGalleryImageResource.php new file mode 100644 index 0000000..df3e4e9 --- /dev/null +++ b/be/Modules/Activity/Transformers/ActivityGalleryImageResource.php @@ -0,0 +1,31 @@ +documentable_id; + + return [ + 'id' => $this->id, + 'activity_id' => $activityId, + 'name' => $this->name, + 'description' => $this->description, + 'mime_type' => $this->mime_type, + 'file_size' => $this->file_size, + 'uploaded_by' => $this->uploaded_by, + 'url' => $activityId + ? url(route('api.activity.serve-gallery-image', [ + 'activity' => $activityId, + 'imageId' => $this->id, + ])) + : null, + 'created_at' => $this->created_at?->toISOString(), + ]; + } +} diff --git a/be/Modules/Activity/Transformers/ActivityListResource.php b/be/Modules/Activity/Transformers/ActivityListResource.php new file mode 100644 index 0000000..8c47d33 --- /dev/null +++ b/be/Modules/Activity/Transformers/ActivityListResource.php @@ -0,0 +1,34 @@ + $this->id, + 'activity_type_id' => $this->activity_type_id, + 'activity_type' => new ActivityTypeResource($this->whenLoaded('activityType')), + 'title' => $this->title, + 'reference_number' => $this->reference_number, + 'description' => $this->description, + 'start_datetime' => $this->start_datetime?->toISOString(), + 'end_datetime' => $this->end_datetime?->toISOString(), + 'organizer' => $this->organizer, + 'location' => $this->location, + 'is_active' => $this->is_active, + 'gallery_image_count' => $this->gallery_images_count ?? 0, + 'documents_count' => $this->attachments_count ?? 0, + 'cover_image' => $this->when( + $this->relationLoaded('galleryImages') && $this->galleryImages->isNotEmpty(), + fn () => new ActivityGalleryImageResource($this->galleryImages->first()) + ), + 'created_at' => $this->created_at?->toISOString(), + 'updated_at' => $this->updated_at?->toISOString(), + ]; + } +} diff --git a/be/Modules/Activity/Transformers/ActivityReportResource.php b/be/Modules/Activity/Transformers/ActivityReportResource.php new file mode 100644 index 0000000..cc19c9b --- /dev/null +++ b/be/Modules/Activity/Transformers/ActivityReportResource.php @@ -0,0 +1,27 @@ + $this->id, + 'activity_id' => $this->activity_id, + 'activity' => new ActivityResource($this->whenLoaded('activity')), + 'report_text' => $this->report_text, + 'prepared_by' => $this->prepared_by, + 'prepared_by_user' => $this->whenLoaded('preparedBy', fn () => [ + 'id' => $this->preparedBy->id, + 'name' => $this->preparedBy->name, + 'email' => $this->preparedBy->email, + ]), + 'created_at' => $this->created_at?->toISOString(), + 'updated_at' => $this->updated_at?->toISOString(), + ]; + } +} diff --git a/be/Modules/Activity/Transformers/ActivityResource.php b/be/Modules/Activity/Transformers/ActivityResource.php new file mode 100644 index 0000000..f2f850b --- /dev/null +++ b/be/Modules/Activity/Transformers/ActivityResource.php @@ -0,0 +1,52 @@ + $this->id, + 'activity_type_id' => $this->activity_type_id, + 'activity_type' => new ActivityTypeResource($this->whenLoaded('activityType')), + 'title' => $this->title, + 'reference_number' => $this->reference_number, + 'description' => $this->description, + 'start_datetime' => $this->start_datetime?->toISOString(), + 'end_datetime' => $this->end_datetime?->toISOString(), + 'organizer' => $this->organizer, + 'location' => $this->location, + 'is_active' => $this->is_active, + 'documents' => $this->when( + $this->relationLoaded('documents'), + fn () => ActivityDocumentResource::collection($this->attachmentDocuments()) + ), + 'gallery_images' => $this->when( + $this->relationLoaded('documents'), + fn () => ActivityGalleryImageResource::collection($this->galleryDocuments()) + ), + 'reports' => ActivityReportResource::collection($this->whenLoaded('reports')), + 'created_at' => $this->created_at?->toISOString(), + 'updated_at' => $this->updated_at?->toISOString(), + ]; + } + + protected function galleryDocuments() + { + return $this->documents + ->where('type', Activity::GALLERY_DOCUMENT_TYPE) + ->values(); + } + + protected function attachmentDocuments() + { + return $this->documents + ->where('type', '!=', Activity::GALLERY_DOCUMENT_TYPE) + ->values(); + } +} diff --git a/be/Modules/Activity/Transformers/ActivityTypeResource.php b/be/Modules/Activity/Transformers/ActivityTypeResource.php new file mode 100644 index 0000000..ca6d008 --- /dev/null +++ b/be/Modules/Activity/Transformers/ActivityTypeResource.php @@ -0,0 +1,20 @@ + $this->id, + 'name' => $this->name, + 'code' => $this->code, + 'created_at' => $this->created_at?->toISOString(), + 'updated_at' => $this->updated_at?->toISOString(), + ]; + } +} diff --git a/be/Modules/Activity/composer.json b/be/Modules/Activity/composer.json new file mode 100644 index 0000000..fb29613 --- /dev/null +++ b/be/Modules/Activity/composer.json @@ -0,0 +1,30 @@ +{ + "name": "nwidart/activity", + "description": "", + "authors": [ + { + "name": "Nicolas Widart", + "email": "n.widart@gmail.com" + } + ], + "extra": { + "laravel": { + "providers": [], + "aliases": { + + } + } + }, + "autoload": { + "psr-4": { + "Modules\\Activity\\": "App", + "Modules\\Activity\\Database\\Factories\\": "database/factories/", + "Modules\\Activity\\Database\\Seeders\\": "database/seeders/" + } + }, + "autoload-dev": { + "psr-4": { + "Modules\\Activity\\Tests\\": "tests/" + } + } +} diff --git a/be/Modules/Activity/module.json b/be/Modules/Activity/module.json new file mode 100644 index 0000000..cdb201d --- /dev/null +++ b/be/Modules/Activity/module.json @@ -0,0 +1,11 @@ +{ + "name": "Activity", + "alias": "activity", + "description": "", + "keywords": [], + "priority": 0, + "providers": [ + "Modules\\Activity\\Providers\\ActivityServiceProvider" + ], + "files": [] +} diff --git a/be/Modules/Activity/package.json b/be/Modules/Activity/package.json new file mode 100644 index 0000000..d6fbfc8 --- /dev/null +++ b/be/Modules/Activity/package.json @@ -0,0 +1,15 @@ +{ + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build" + }, + "devDependencies": { + "axios": "^1.1.2", + "laravel-vite-plugin": "^0.7.5", + "sass": "^1.69.5", + "postcss": "^8.3.7", + "vite": "^4.0.0" + } +} diff --git a/be/Modules/Auth/Emails/EmailVerificationOtpEmail.php b/be/Modules/Auth/Emails/EmailVerificationOtpEmail.php index 3dcb338..2dd14a8 100644 --- a/be/Modules/Auth/Emails/EmailVerificationOtpEmail.php +++ b/be/Modules/Auth/Emails/EmailVerificationOtpEmail.php @@ -2,12 +2,14 @@ namespace Modules\Auth\Emails; +use App\Notifications\Concerns\BuildsMailMessage; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; class EmailVerificationOtpEmail extends Notification { + use BuildsMailMessage; use Queueable; public function __construct( @@ -23,13 +25,14 @@ class EmailVerificationOtpEmail extends Notification { $minutes = (int) config('auth.email_verification.expiry_minutes', 10); - return (new MailMessage) - ->subject('Pengesahan E-mel - Kod OTP') - ->markdown('auth::emails.verification-otp', [ + return $this->mailMessage( + subject: 'Pengesahan E-mel - Kod OTP', + view: 'auth::emails.verification-otp', + data: [ 'name' => $notifiable->name, 'otp' => $this->otp, 'minutes' => $minutes, - 'logoPath' => public_path('images/logo-kopkb.svg'), - ]); + ], + ); } } diff --git a/be/Modules/Auth/Emails/PasswordResetOtpEmail.php b/be/Modules/Auth/Emails/PasswordResetOtpEmail.php new file mode 100644 index 0000000..154063c --- /dev/null +++ b/be/Modules/Auth/Emails/PasswordResetOtpEmail.php @@ -0,0 +1,38 @@ +mailMessage( + subject: 'Reset Kata Laluan - Kod OTP', + view: 'auth::emails.password-reset-otp', + data: [ + 'name' => $notifiable->name, + 'otp' => $this->otp, + 'minutes' => $minutes, + ], + ); + } +} diff --git a/be/Modules/Auth/Entities/PasswordResetOtp.php b/be/Modules/Auth/Entities/PasswordResetOtp.php new file mode 100644 index 0000000..74fa260 --- /dev/null +++ b/be/Modules/Auth/Entities/PasswordResetOtp.php @@ -0,0 +1,44 @@ + 'datetime', + 'attempts' => 'integer', + ]; + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function isExpired(): bool + { + return $this->expires_at->isPast(); + } + + public function hasExceededMaxAttempts(): bool + { + return $this->attempts >= (int) config('auth.password_reset.max_attempts', 5); + } +} diff --git a/be/Modules/Auth/Entities/User.php b/be/Modules/Auth/Entities/User.php index 28ff4c1..3c094b5 100644 --- a/be/Modules/Auth/Entities/User.php +++ b/be/Modules/Auth/Entities/User.php @@ -16,14 +16,17 @@ use Spatie\Permission\PermissionRegistrar; use Spatie\Permission\Traits\HasPermissions; use Spatie\Permission\Traits\HasRoles; use App\Services\ActiveRoleService; -use App\Traits\HasVisibility; use Lab404\Impersonate\Models\Impersonate; use Modules\Role\Entities\Role; +use Modules\User\Entities\Address; +use Modules\User\Entities\BankDetail; +use Modules\User\Entities\Employment; +use Modules\User\Entities\Heir; class User extends Authenticatable { /** @use HasFactory<\Modules\Auth\Database\Factories\UserFactory> */ - use HasApiTokens, HasFactory, HasPermissions, HasRoles, HasUuids, Impersonate, LogsActivity, Notifiable, SoftDeletes, HasVisibility; + use HasApiTokens, HasFactory, HasPermissions, HasRoles, HasUuids, Impersonate, LogsActivity, Notifiable, SoftDeletes; protected $table = 'users'; @@ -44,6 +47,13 @@ class User extends Authenticatable 'two_factor_secret', 'two_factor_recovery_codes', 'two_factor_confirmed_at', + 'gender', + 'marriage_status', + 'member_number', + 'member_type', + 'join_date', + 'birth_date', + 'birth_place', ]; /** @@ -70,6 +80,13 @@ class User extends Authenticatable 'email_verified_at' => 'datetime', 'password' => 'hashed', 'status' => 'string', + 'gender' => 'string', + 'marriage_status' => 'string', + 'member_number' => 'integer', + 'member_type' => 'string', + 'join_date' => 'date', + 'birth_date' => 'date', + 'birth_place' => 'string', ]; } @@ -82,6 +99,26 @@ class User extends Authenticatable return ActiveRoleService::getActiveRole($this); } + public function addresses() + { + return $this->hasMany(Address::class); + } + + public function employments() + { + return $this->hasMany(Employment::class); + } + + public function bankDetails() + { + return $this->hasMany(BankDetail::class); + } + + public function heirs() + { + return $this->hasMany(Heir::class); + } + public function hasPermissionAction($action): bool { $role = $this->activeRole(); diff --git a/be/Modules/Auth/Http/Controllers/PasswordResetController.php b/be/Modules/Auth/Http/Controllers/PasswordResetController.php new file mode 100644 index 0000000..ffd5b20 --- /dev/null +++ b/be/Modules/Auth/Http/Controllers/PasswordResetController.php @@ -0,0 +1,63 @@ +validate([ + 'email' => ['required', 'string', 'email'], + ]); + + $user = User::where('email', $validated['email'])->first(); + + if ($user) { + $this->passwordResetOtpService->send($user); + } + + return response()->json([ + 'success' => true, + 'message' => 'Jika e-mel wujud dalam sistem, kod OTP untuk reset kata laluan telah dihantar.', + ]); + } + + public function reset(Request $request): JsonResponse + { + $validated = $request->validate([ + 'email' => ['required', 'string', 'email'], + 'otp' => ['required', 'string', 'digits:6'], + 'password' => ['required', 'string', 'confirmed'], + ]); + + $user = User::where('email', $validated['email'])->first(); + + if (! $user) { + return response()->json([ + 'success' => false, + 'message' => 'Kod OTP tidak sah.', + ], 422); + } + + $this->passwordResetOtpService->reset( + $user, + $validated['otp'], + $request->only(['password', 'password_confirmation']), + ); + + return response()->json([ + 'success' => true, + 'message' => 'Kata laluan anda telah berjaya ditetapkan semula. Sila log masuk.', + ]); + } +} diff --git a/be/Modules/Auth/Routes/api.php b/be/Modules/Auth/Routes/api.php index 21d38a3..ea3eb18 100644 --- a/be/Modules/Auth/Routes/api.php +++ b/be/Modules/Auth/Routes/api.php @@ -2,6 +2,7 @@ use Illuminate\Support\Facades\Route; use Modules\Auth\Http\Controllers\EmailVerificationController; +use Modules\Auth\Http\Controllers\PasswordResetController; use Modules\Auth\Http\Controllers\SessionController; use Laravel\Fortify\Http\Controllers\AuthenticatedSessionController; use Laravel\Fortify\Http\Controllers\RegisteredUserController; @@ -15,6 +16,11 @@ Route::post('/verify-email', [EmailVerificationController::class, 'verify']) Route::post('/verify-email/resend', [EmailVerificationController::class, 'resend']) ->middleware('throttle:email-verification-resend'); +Route::post('/forgot-password', [PasswordResetController::class, 'requestOtp']) + ->middleware('throttle:password-reset-request'); +Route::post('/reset-password', [PasswordResetController::class, 'reset']) + ->middleware('throttle:password-reset'); + Route::middleware(['auth:sanctum'])->prefix('v1')->group(function () { // get current user Route::get('/me', [SessionController::class, 'currentUser']); diff --git a/be/Modules/Auth/Services/PasswordResetOtpService.php b/be/Modules/Auth/Services/PasswordResetOtpService.php new file mode 100644 index 0000000..0fc8017 --- /dev/null +++ b/be/Modules/Auth/Services/PasswordResetOtpService.php @@ -0,0 +1,99 @@ +generateOtp(); + + PasswordResetOtp::query() + ->where('user_id', $user->id) + ->delete(); + + PasswordResetOtp::create([ + 'user_id' => $user->id, + 'code' => Hash::make($otp), + 'expires_at' => now()->addMinutes($this->expiryMinutes()), + 'attempts' => 0, + ]); + + $user->notify(new PasswordResetOtpEmail($otp)); + } + + public function reset(User $user, string $otp, array $input): void + { + validator($input, [ + 'password' => $this->passwordRules(), + ])->validate(); + + $record = PasswordResetOtp::query() + ->where('user_id', $user->id) + ->latest() + ->first(); + + if (! $record) { + throw ValidationException::withMessages([ + 'otp' => ['Kod OTP tidak dijumpai. Sila minta kod baharu.'], + ]); + } + + if ($record->isExpired()) { + $record->delete(); + + throw ValidationException::withMessages([ + 'otp' => ['Kod OTP telah tamat tempoh. Sila minta kod baharu.'], + ]); + } + + if ($record->hasExceededMaxAttempts()) { + $record->delete(); + + throw ValidationException::withMessages([ + 'otp' => ['Terlalu banyak percubaan. Sila minta kod baharu.'], + ]); + } + + if (! Hash::check($otp, $record->code)) { + $record->increment('attempts'); + + if ($record->fresh()->hasExceededMaxAttempts()) { + $record->delete(); + } + + throw ValidationException::withMessages([ + 'otp' => ['Kod OTP tidak sah.'], + ]); + } + + $user->forceFill([ + 'password' => Hash::make($input['password']), + ])->save(); + + $user->tokens()->delete(); + + PasswordResetOtp::query() + ->where('user_id', $user->id) + ->delete(); + } + + protected function generateOtp(): string + { + return str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT); + } + + protected function expiryMinutes(): int + { + return (int) config('auth.password_reset.expiry_minutes', 10); + } +} diff --git a/be/Modules/Auth/Transformers/UserResource.php b/be/Modules/Auth/Transformers/UserResource.php index a064874..98518ef 100644 --- a/be/Modules/Auth/Transformers/UserResource.php +++ b/be/Modules/Auth/Transformers/UserResource.php @@ -5,6 +5,10 @@ namespace Modules\Auth\Transformers; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Support\Facades\Storage; +use Modules\User\Transformers\AddressResource; +use Modules\User\Transformers\BankDetailResource; +use Modules\User\Transformers\EmploymentResource; +use Modules\User\Transformers\HeirResource; class UserResource extends JsonResource { @@ -28,9 +32,20 @@ class UserResource extends JsonResource 'two_factor_recovery_codes' => $this->two_factor_recovery_codes, 'two_factor_confirmed_at' => $this->two_factor_confirmed_at, 'email_verified_at' => $this->email_verified_at, + 'gender' => $this->gender, + 'marriage_status' => $this->marriage_status, + 'member_number' => $this->member_number, + 'member_type' => $this->member_type, + 'join_date' => $this->join_date, + 'birth_date' => $this->birth_date, + 'birth_place' => $this->birth_place, 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, 'deleted_at' => $this->deleted_at, + 'addresses' => AddressResource::collection($this->whenLoaded('addresses')), + 'employments' => EmploymentResource::collection($this->whenLoaded('employments')), + 'bank_details' => BankDetailResource::collection($this->whenLoaded('bankDetails')), + 'heirs' => HeirResource::collection($this->whenLoaded('heirs')), 'roles' => $this->whenLoaded('roles', function () { return $this->roles->map(function ($role) { return [ diff --git a/be/Modules/Auth/resources/views/emails/password-reset-otp.blade.php b/be/Modules/Auth/resources/views/emails/password-reset-otp.blade.php new file mode 100644 index 0000000..95a249d --- /dev/null +++ b/be/Modules/Auth/resources/views/emails/password-reset-otp.blade.php @@ -0,0 +1,21 @@ +@component('mail::message') +@include('emails.partials.header') + +# Reset Kata Laluan + +Assalamualaikum **{{ $name }}**, + +Kami menerima permintaan untuk menetapkan semula kata laluan akaun anda. Gunakan kod OTP di bawah untuk meneruskan. + +@component('mail::panel') +
+ {{ $otp }} +
+@endcomponent + +Kod ini akan tamat tempoh dalam **{{ $minutes }} minit**. + +Jika anda tidak membuat permintaan ini, abaikan e-mel ini. + +@include('emails.partials.footer') +@endcomponent diff --git a/be/Modules/Auth/resources/views/emails/verification-otp.blade.php b/be/Modules/Auth/resources/views/emails/verification-otp.blade.php index 05c3263..dcaf385 100644 --- a/be/Modules/Auth/resources/views/emails/verification-otp.blade.php +++ b/be/Modules/Auth/resources/views/emails/verification-otp.blade.php @@ -1,9 +1,5 @@ @component('mail::message') -@if (! empty($logoPath) && file_exists($logoPath)) -
- {{ config('app.name') }} -
-@endif +@include('emails.partials.header') # Pengesahan E-mel @@ -21,6 +17,5 @@ Kod ini akan tamat tempoh dalam **{{ $minutes }} minit**. Jika anda tidak membuat pendaftaran ini, abaikan e-mel ini. -Terima kasih,
-{{ config('app.name') }} +@include('emails.partials.footer') @endcomponent diff --git a/be/Modules/MembershipApplication/Actions/.gitkeep b/be/Modules/MembershipApplication/Actions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/MembershipApplication/Config/.gitkeep b/be/Modules/MembershipApplication/Config/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/MembershipApplication/Config/config.php b/be/Modules/MembershipApplication/Config/config.php new file mode 100644 index 0000000..9b65007 --- /dev/null +++ b/be/Modules/MembershipApplication/Config/config.php @@ -0,0 +1,5 @@ + 'MembershipApplication', +]; diff --git a/be/Modules/MembershipApplication/Console/.gitkeep b/be/Modules/MembershipApplication/Console/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/MembershipApplication/Database/Factories/.gitkeep b/be/Modules/MembershipApplication/Database/Factories/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/MembershipApplication/Database/Migrations/.gitkeep b/be/Modules/MembershipApplication/Database/Migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/MembershipApplication/Database/Migrations/2026_06_14_022844_create_membership_applications_table.php b/be/Modules/MembershipApplication/Database/Migrations/2026_06_14_022844_create_membership_applications_table.php new file mode 100644 index 0000000..ced621e --- /dev/null +++ b/be/Modules/MembershipApplication/Database/Migrations/2026_06_14_022844_create_membership_applications_table.php @@ -0,0 +1,33 @@ +uuid('id')->primary(); + $table->string('application_number'); + $table->foreignUuid('user_id')->constrained('users')->onDelete('cascade')->nullable(); + $table->string('status'); + $table->string('board_result'); + $table->string('submitted_at'); + $table->string('completed_at'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('membership_applications'); + } +}; diff --git a/be/Modules/MembershipApplication/Database/Migrations/2026_06_14_023448_create_membership_application_applicants_table.php b/be/Modules/MembershipApplication/Database/Migrations/2026_06_14_023448_create_membership_application_applicants_table.php new file mode 100644 index 0000000..3f356be --- /dev/null +++ b/be/Modules/MembershipApplication/Database/Migrations/2026_06_14_023448_create_membership_application_applicants_table.php @@ -0,0 +1,57 @@ +uuid('id')->primary(); + $table->foreignUuid('membership_application_id')->constrained('membership_applications')->onDelete('cascade'); + $table->string('name'); + $table->string('email'); + $table->string('ic_number'); + $table->string('birth_date'); + $table->string('birth_place'); + $table->string('marriage_status')->comment('Belum Berkahwin, Berkahwin, Bercerai, Balu, Duda'); + + // contact information + $table->text('address'); + $table->string('phone_number'); + $table->string('office_number')->nullable(); + $table->string('postcode'); + + // Employment + $table->string('employer_name'); + $table->text('employer_address'); + $table->string('current_position'); + $table->date('start_work_date'); + + // contribution + $table->decimal('stock_monthly_contribution', 10, 2); + $table->decimal('fee_monthly_contribution', 10, 2); + + $table->timestamps(); + }); + + DB::statement(' + ALTER TABLE membership_application_applicants + ADD COLUMN gender user_gender_enum NULL + '); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('membership_application_applicants'); + } +}; diff --git a/be/Modules/MembershipApplication/Database/Migrations/2026_06_14_044009_create_membership_application_heirs_table.php b/be/Modules/MembershipApplication/Database/Migrations/2026_06_14_044009_create_membership_application_heirs_table.php new file mode 100644 index 0000000..501a190 --- /dev/null +++ b/be/Modules/MembershipApplication/Database/Migrations/2026_06_14_044009_create_membership_application_heirs_table.php @@ -0,0 +1,32 @@ +uuid('id')->primary(); + $table->foreignUuid('membership_application_id')->constrained('membership_applications')->onDelete('cascade'); + $table->string('name'); + $table->string('ic_number'); + $table->string('relationship')->comment('Isteri, Anak, Orang Tua, Saudara, Lain-lain'); + $table->string('phone_number'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('membership_application_heirs'); + } +}; diff --git a/be/Modules/MembershipApplication/Database/Migrations/2026_06_14_044224_create_membership_application_references_table.php b/be/Modules/MembershipApplication/Database/Migrations/2026_06_14_044224_create_membership_application_references_table.php new file mode 100644 index 0000000..58cbce4 --- /dev/null +++ b/be/Modules/MembershipApplication/Database/Migrations/2026_06_14_044224_create_membership_application_references_table.php @@ -0,0 +1,42 @@ +uuid('id')->primary(); + $table->foreignUuid('membership_application_id') + ->constrained('membership_applications') + ->cascadeOnDelete(); + + $table->string('reference_type')->comment('PROPOSER, SUPPORTER'); + + // Applicant may select at submit; admin may fill in later + $table->foreignUuid('user_id')->nullable()->constrained('users')->nullOnDelete(); + + // Set only when admin assigns or updates this reference + $table->foreignUuid('assigned_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamp('assigned_at')->nullable(); + + $table->timestamps(); + + $table->unique(['membership_application_id', 'reference_type']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('membership_application_references'); + } +}; diff --git a/be/Modules/MembershipApplication/Database/Migrations/2026_06_14_050728_create_membership_application_reviews_table.php b/be/Modules/MembershipApplication/Database/Migrations/2026_06_14_050728_create_membership_application_reviews_table.php new file mode 100644 index 0000000..cd5db29 --- /dev/null +++ b/be/Modules/MembershipApplication/Database/Migrations/2026_06_14_050728_create_membership_application_reviews_table.php @@ -0,0 +1,40 @@ +uuid('id')->primary(); + $table->foreignUuid('membership_application_id') + ->constrained('membership_applications') + ->cascadeOnDelete(); + + $table->string('stage')->comment('MANAGEMENT, BOARD'); + $table->string('decision')->comment('MANAGEMENT: APPROVED, REJECTED | BOARD: PASS, FAIL'); + $table->text('remarks')->nullable(); + + $table->foreignUuid('reviewer_id')->constrained('users')->restrictOnDelete(); + $table->timestamp('reviewed_at'); + + $table->timestamps(); + + $table->unique(['membership_application_id', 'stage']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('membership_application_reviews'); + } +}; diff --git a/be/Modules/MembershipApplication/Database/Seeders/.gitkeep b/be/Modules/MembershipApplication/Database/Seeders/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/MembershipApplication/Database/Seeders/MembershipApplicationDatabaseSeeder.php b/be/Modules/MembershipApplication/Database/Seeders/MembershipApplicationDatabaseSeeder.php new file mode 100644 index 0000000..31aa8e8 --- /dev/null +++ b/be/Modules/MembershipApplication/Database/Seeders/MembershipApplicationDatabaseSeeder.php @@ -0,0 +1,16 @@ +call([]); + } +} diff --git a/be/Modules/MembershipApplication/Emails/.gitkeep b/be/Modules/MembershipApplication/Emails/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/MembershipApplication/Emails/MembershipApplicationFailedNotification.php b/be/Modules/MembershipApplication/Emails/MembershipApplicationFailedNotification.php new file mode 100644 index 0000000..ba3f09a --- /dev/null +++ b/be/Modules/MembershipApplication/Emails/MembershipApplicationFailedNotification.php @@ -0,0 +1,35 @@ +application->loadMissing('applicant'); + + return (new MailMessage) + ->subject('Keputusan Permohonan Keahlian') + ->markdown('membershipapplication::emails.failed', [ + 'name' => $this->application->applicant->name, + 'applicationNumber' => $this->application->application_number, + 'logoPath' => public_path('images/logo-kopkb.svg'), + ]); + } +} diff --git a/be/Modules/MembershipApplication/Emails/MembershipApplicationPassedNotification.php b/be/Modules/MembershipApplication/Emails/MembershipApplicationPassedNotification.php new file mode 100644 index 0000000..e9ccbd8 --- /dev/null +++ b/be/Modules/MembershipApplication/Emails/MembershipApplicationPassedNotification.php @@ -0,0 +1,35 @@ +application->loadMissing('applicant'); + + return (new MailMessage) + ->subject('Permohonan Keahlian Diluluskan') + ->markdown('membershipapplication::emails.passed', [ + 'name' => $this->application->applicant->name, + 'applicationNumber' => $this->application->application_number, + 'logoPath' => public_path('images/logo-kopkb.svg'), + ]); + } +} diff --git a/be/Modules/MembershipApplication/Emails/MembershipApplicationSubmittedNotification.php b/be/Modules/MembershipApplication/Emails/MembershipApplicationSubmittedNotification.php new file mode 100644 index 0000000..4b77632 --- /dev/null +++ b/be/Modules/MembershipApplication/Emails/MembershipApplicationSubmittedNotification.php @@ -0,0 +1,43 @@ +application->loadMissing(['applicant', 'heirs', 'documents', 'references.member']); + + return (new MailMessage) + ->subject('Pengesahan Permohonan Keahlian') + ->markdown('membershipapplication::emails.submitted', [ + 'application' => $this->application, + 'applicant' => $this->application->applicant, + 'heirs' => $this->application->heirs, + 'documents' => $this->application->documents, + 'documentLabels' => [ + 'ic_copy' => 'Salinan Kad Pengenalan', + 'photo' => 'Gambar Passport', + 'salary_slip' => 'Slip Gaji', + 'employer_letter' => 'Surat Pengesahan Majikan', + ], + 'logoPath' => public_path('images/logo-kopkb.svg'), + ]); + } +} diff --git a/be/Modules/MembershipApplication/Entities/.gitkeep b/be/Modules/MembershipApplication/Entities/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/MembershipApplication/Entities/MembershipApplication.php b/be/Modules/MembershipApplication/Entities/MembershipApplication.php new file mode 100644 index 0000000..57409a1 --- /dev/null +++ b/be/Modules/MembershipApplication/Entities/MembershipApplication.php @@ -0,0 +1,68 @@ + + */ + protected $fillable = [ + 'application_number', + 'user_id', + 'status', + 'board_result', + 'submitted_at', + 'completed_at', + ]; + + /** + * @var list + */ + protected $hidden = [ + ]; + + protected function casts(): array + { + return [ + 'status' => ApplicationStatus::class, + ]; + } + + public function applicant(): HasOne + { + return $this->hasOne(MembershipApplicationApplicant::class); + } + + public function heirs(): HasMany + { + return $this->hasMany(MembershipApplicationHeir::class); + } + + public function references(): HasMany + { + return $this->hasMany(MembershipApplicationReference::class); + } + + public function reviews(): HasMany + { + return $this->hasMany(MembershipApplicationReview::class); + } + + public function user() + { + return $this->belongsTo(User::class); + } +} diff --git a/be/Modules/MembershipApplication/Entities/MembershipApplicationApplicant.php b/be/Modules/MembershipApplication/Entities/MembershipApplicationApplicant.php new file mode 100644 index 0000000..81f484a --- /dev/null +++ b/be/Modules/MembershipApplication/Entities/MembershipApplicationApplicant.php @@ -0,0 +1,58 @@ + + */ + protected $fillable = [ + 'membership_application_id', + 'name', + 'email', + 'ic_number', + 'birth_date', + 'birth_place', + 'gender', + 'marriage_status', + 'address', + 'phone_number', + 'office_number', + 'postcode', + 'employer_name', + 'employer_address', + 'current_position', + 'start_work_date', + 'stock_monthly_contribution', + 'fee_monthly_contribution', + ]; + + /** + * @var list + */ + protected $hidden = [ + ]; + + protected function casts(): array + { + return [ + 'start_work_date' => 'date', + 'stock_monthly_contribution' => 'decimal:2', + 'fee_monthly_contribution' => 'decimal:2', + ]; + } + + public function membershipApplication(): BelongsTo + { + return $this->belongsTo(MembershipApplication::class); + } +} diff --git a/be/Modules/MembershipApplication/Entities/MembershipApplicationHeir.php b/be/Modules/MembershipApplication/Entities/MembershipApplicationHeir.php new file mode 100644 index 0000000..1c11c0d --- /dev/null +++ b/be/Modules/MembershipApplication/Entities/MembershipApplicationHeir.php @@ -0,0 +1,46 @@ + + */ + protected $fillable = [ + 'membership_application_id', + 'name', + 'ic_number', + 'relationship', + 'phone_number', + ]; + + /** + * @var list + */ + protected $hidden = [ + ]; + + protected function casts(): array + { + return [ + 'name' => 'string', + 'ic_number' => 'string', + 'relationship' => 'string', + 'phone_number' => 'string', + ]; + } + + public function membershipApplication(): BelongsTo + { + return $this->belongsTo(MembershipApplication::class); + } +} diff --git a/be/Modules/MembershipApplication/Entities/MembershipApplicationReference.php b/be/Modules/MembershipApplication/Entities/MembershipApplicationReference.php new file mode 100644 index 0000000..f4c729a --- /dev/null +++ b/be/Modules/MembershipApplication/Entities/MembershipApplicationReference.php @@ -0,0 +1,56 @@ + + */ + protected $fillable = [ + 'membership_application_id', + 'reference_type', + 'user_id', + 'assigned_by', + 'assigned_at', + ]; + + /** + * @var list + */ + protected $hidden = [ + ]; + + protected function casts(): array + { + return [ + 'reference_type' => ReferenceType::class, + 'assigned_at' => 'datetime', + ]; + } + + public function membershipApplication(): BelongsTo + { + return $this->belongsTo(MembershipApplication::class); + } + + public function member(): BelongsTo + { + return $this->belongsTo(User::class, 'user_id'); + } + + public function assignedBy(): BelongsTo + { + return $this->belongsTo(User::class, 'assigned_by'); + } +} diff --git a/be/Modules/MembershipApplication/Entities/MembershipApplicationReview.php b/be/Modules/MembershipApplication/Entities/MembershipApplicationReview.php new file mode 100644 index 0000000..2fb89f2 --- /dev/null +++ b/be/Modules/MembershipApplication/Entities/MembershipApplicationReview.php @@ -0,0 +1,72 @@ + + */ + protected $fillable = [ + 'membership_application_id', + 'stage', + 'decision', + 'remarks', + 'reviewer_id', + 'reviewed_at', + ]; + + /** + * @var list + */ + protected $hidden = [ + ]; + + protected function casts(): array + { + return [ + 'stage' => ReviewStage::class, + 'reviewed_at' => 'datetime', + ]; + } + + public function membershipApplication(): BelongsTo + { + return $this->belongsTo(MembershipApplication::class); + } + + public function reviewer(): BelongsTo + { + return $this->belongsTo(User::class, 'reviewer_id'); + } + + public function managementDecision(): ?ManagementDecision + { + if ($this->stage !== ReviewStage::Management) { + return null; + } + + return ManagementDecision::tryFrom($this->decision); + } + + public function boardDecision(): ?BoardDecision + { + if ($this->stage !== ReviewStage::Board) { + return null; + } + + return BoardDecision::tryFrom($this->decision); + } +} diff --git a/be/Modules/MembershipApplication/Enums/ApplicationStatus.php b/be/Modules/MembershipApplication/Enums/ApplicationStatus.php new file mode 100644 index 0000000..29fc472 --- /dev/null +++ b/be/Modules/MembershipApplication/Enums/ApplicationStatus.php @@ -0,0 +1,12 @@ +authorize('viewAny', MembershipApplication::class); + + $perPage = min((int) $request->get('per_page', 10), 100); + $search = (string) ($request->get('search', '') ?? ''); + $status = $request->get('status'); + $sortBy = (string) ($request->get('sort_by', 'submitted_at') ?? 'submitted_at'); + $sortOrder = (string) ($request->get('sort_order', 'desc') ?? 'desc'); + + $applications = $this->membershipApplicationService->getPaginatedList( + $perPage, + $search, + $status, + $sortBy, + $sortOrder + ); + + return response()->json([ + 'success' => true, + 'data' => MembershipApplicationListResource::collection($applications->items()), + 'pagination' => [ + 'current_page' => $applications->currentPage(), + 'per_page' => $applications->perPage(), + 'total' => $applications->total(), + 'last_page' => $applications->lastPage(), + 'from' => $applications->firstItem(), + 'to' => $applications->lastItem(), + 'has_more_pages' => $applications->hasMorePages(), + ], + ]); + } + + public function show(MembershipApplication $membershipApplication): JsonResponse + { + $this->authorize('view', $membershipApplication); + + $application = $this->membershipApplicationService->getByIdWithRelations($membershipApplication->id); + + if (! $application) { + return response()->json([ + 'success' => false, + 'message' => 'Permohonan tidak dijumpai.', + ], 404); + } + + return response()->json([ + 'success' => true, + 'data' => new MembershipApplicationResource($application), + ]); + } + + public function update(UpdateMembershipApplicationRequest $request, MembershipApplication $membershipApplication): JsonResponse + { + $this->authorize('update', $membershipApplication); + + $application = $this->membershipApplicationService->update( + $membershipApplication, + $request->validated(), + ); + + return response()->json([ + 'success' => true, + 'message' => 'Permohonan keahlian berjaya dikemaskini.', + 'data' => new MembershipApplicationResource($application), + ]); + } + + public function uploadDocument(UploadMembershipApplicationDocumentRequest $request, MembershipApplication $membershipApplication): JsonResponse + { + $this->authorize('update', $membershipApplication); + + $application = $this->membershipApplicationService->uploadDocument( + $membershipApplication, + $request->validated('type'), + $request->file('file'), + ); + + return response()->json([ + 'success' => true, + 'message' => 'Dokumen berjaya dimuat naik.', + 'data' => new MembershipApplicationResource($application), + ]); + } + + public function managementReview(ManagementReviewRequest $request, MembershipApplication $membershipApplication): JsonResponse { + $this->authorize('managementReview', $membershipApplication); + + $application = $this->membershipApplicationService->managementReview( + $membershipApplication, + ManagementDecision::from($request->validated('decision')), + $request->validated('remarks'), + $request->user() + ); + + $message = $application->status === ApplicationStatus::PendingBoard + ? 'Permohonan diluluskan dan dihantar ke lembaga.' + : 'Permohonan ditolak.'; + + return response()->json([ + 'success' => true, + 'message' => $message, + 'data' => new MembershipApplicationResource($application), + ]); + } + + public function boardReview(BoardReviewRequest $request, MembershipApplication $membershipApplication): JsonResponse { + $this->authorize('boardReview', $membershipApplication); + + $application = $this->membershipApplicationService->boardReview( + $membershipApplication, + BoardDecision::from($request->validated('decision')), + $request->validated('remarks'), + $request->user() + ); + + $message = $application->board_result === BoardResult::Pass->value + ? 'Permohonan lulus semakan lembaga.' + : 'Permohonan gagal semakan lembaga.'; + + return response()->json([ + 'success' => true, + 'message' => $message, + 'data' => new MembershipApplicationResource($application), + ]); + } + + public function assignReferences(AssignReferencesRequest $request, MembershipApplication $membershipApplication): JsonResponse { + $this->authorize('assignReferences', $membershipApplication); + + $application = $this->membershipApplicationService->assignReferences( + $membershipApplication, + $request->validated(), + $request->user() + ); + + return response()->json([ + 'success' => true, + 'message' => 'Pencadang dan penyokong berjaya dikemaskini.', + 'data' => new MembershipApplicationResource($application), + ]); + } + + public function complete(Request $request, MembershipApplication $membershipApplication): JsonResponse + { + $this->authorize('complete', $membershipApplication); + + $application = $this->membershipApplicationService->complete( + $membershipApplication, + $request->user() + ); + + $message = $application->board_result === BoardResult::Pass->value + ? 'Permohonan selesai. Akaun ahli telah dicipta dan e-mel dihantar.' + : 'Permohonan selesai. E-mel keputusan dihantar.'; + + return response()->json([ + 'success' => true, + 'message' => $message, + 'data' => new MembershipApplicationResource($application), + ]); + } + + public function batchComplete(BatchCompleteRequest $request): JsonResponse + { + $this->authorize('completeBatch', MembershipApplication::class); + + $result = $this->membershipApplicationService->completeBatch( + $request->validated('application_ids'), + $request->user() + ); + + $succeededCount = count($result['succeeded']); + $failedCount = count($result['failed']); + + if ($succeededCount === 0) { + return response()->json([ + 'success' => false, + 'message' => 'Tiada permohonan berjaya diselesaikan.', + 'data' => [ + 'succeeded' => [], + 'failed' => $result['failed'], + ], + ], 422); + } + + $message = $failedCount > 0 + ? "{$succeededCount} permohonan berjaya, {$failedCount} gagal." + : "{$succeededCount} permohonan berjaya diselesaikan."; + + return response()->json([ + 'success' => true, + 'message' => $message, + 'data' => [ + 'succeeded' => MembershipApplicationResource::collection($result['succeeded']), + 'failed' => $result['failed'], + ], + ]); + } + + public function downloadDocument(MembershipApplication $membershipApplication, string $documentId): StreamedResponse { + $this->authorize('view', $membershipApplication); + + $document = $membershipApplication->documents()->findOrFail($documentId); + + return $this->documentService->downloadDocument($document->id); + } + + public function approvedLetter(Request $request, MembershipApplication $membershipApplication): Response + { + $this->authorize('view', $membershipApplication); + + $application = $this->membershipApplicationService->getByIdWithRelations($membershipApplication->id); + + if (! $application) { + abort(404, 'Permohonan tidak dijumpai.'); + } + + $view = 'membershipapplication::pdf.approved-letter'; + $data = ['application' => $application]; + $format = $request->query('format', 'html'); + $filename = 'surat-kelulusan-'.$application->application_number.'.pdf'; + + if ($format === 'pdf') { + return $this->letterService->pdfResponse($view, $data, $filename); + } + + return $this->letterService->htmlResponse($view, $data); + } +} diff --git a/be/Modules/MembershipApplication/Http/Controllers/PublicMembershipApplicationController.php b/be/Modules/MembershipApplication/Http/Controllers/PublicMembershipApplicationController.php new file mode 100644 index 0000000..5ae0c97 --- /dev/null +++ b/be/Modules/MembershipApplication/Http/Controllers/PublicMembershipApplicationController.php @@ -0,0 +1,60 @@ +file('documents', [])) + ->filter() + ->all(); + + $application = $this->membershipApplicationService->submit( + $request->safe()->except(['documents']), + $documents + ); + + return response()->json([ + 'success' => true, + 'message' => 'Permohonan keahlian berjaya dihantar.', + 'data' => new MembershipApplicationResource($application), + ], 201); + } + + public function lookupMember(LookupMemberRequest $request): JsonResponse + { + $member = $this->membershipApplicationService->lookupMemberByIcNumber( + $request->validated('ic_number') + ); + + if (! $member) { + return response()->json([ + 'success' => false, + 'message' => 'Ahli dengan nombor kad pengenalan ini tidak dijumpai.', + 'data' => null, + ]); + } + + return response()->json([ + 'success' => true, + 'message' => 'Ahli dijumpai.', + 'data' => [ + 'id' => $member->id, + 'name' => $member->name, + 'ic_number' => $member->ic_number, + ], + ]); + } +} diff --git a/be/Modules/MembershipApplication/Http/Requests/.gitkeep b/be/Modules/MembershipApplication/Http/Requests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/MembershipApplication/Http/Requests/AssignReferencesRequest.php b/be/Modules/MembershipApplication/Http/Requests/AssignReferencesRequest.php new file mode 100644 index 0000000..90b7dc2 --- /dev/null +++ b/be/Modules/MembershipApplication/Http/Requests/AssignReferencesRequest.php @@ -0,0 +1,35 @@ +|string> + */ + public function rules(): array + { + return [ + 'proposer_id' => 'nullable|uuid|exists:users,id', + 'supporter_id' => 'nullable|uuid|exists:users,id', + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'proposer_id.exists' => 'Pencadang tidak dijumpai.', + 'supporter_id.exists' => 'Penyokong tidak dijumpai.', + ]; + } +} diff --git a/be/Modules/MembershipApplication/Http/Requests/BatchCompleteRequest.php b/be/Modules/MembershipApplication/Http/Requests/BatchCompleteRequest.php new file mode 100644 index 0000000..deb9c19 --- /dev/null +++ b/be/Modules/MembershipApplication/Http/Requests/BatchCompleteRequest.php @@ -0,0 +1,38 @@ +|string> + */ + public function rules(): array + { + return [ + 'application_ids' => 'required|array|min:1|max:100', + 'application_ids.*' => 'required|uuid|distinct|exists:membership_applications,id', + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'application_ids.required' => 'Senarai permohonan diperlukan.', + 'application_ids.min' => 'Sekurang-kurangnya satu permohonan diperlukan.', + 'application_ids.max' => 'Maksimum 100 permohonan setiap batch.', + 'application_ids.*.exists' => 'Salah satu permohonan tidak dijumpai.', + 'application_ids.*.distinct' => 'Permohonan duplikat tidak dibenarkan.', + ]; + } +} diff --git a/be/Modules/MembershipApplication/Http/Requests/BoardReviewRequest.php b/be/Modules/MembershipApplication/Http/Requests/BoardReviewRequest.php new file mode 100644 index 0000000..20ea6fe --- /dev/null +++ b/be/Modules/MembershipApplication/Http/Requests/BoardReviewRequest.php @@ -0,0 +1,38 @@ +|string> + */ + public function rules(): array + { + return [ + 'decision' => ['required', Rule::enum(BoardDecision::class)], + 'remarks' => 'nullable|string|max:1000', + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'decision.required' => 'Keputusan diperlukan.', + 'decision.enum' => 'Keputusan tidak sah. Pilih PASS atau FAIL.', + 'remarks.max' => 'Catatan tidak boleh melebihi 1000 aksara.', + ]; + } +} diff --git a/be/Modules/MembershipApplication/Http/Requests/LookupMemberRequest.php b/be/Modules/MembershipApplication/Http/Requests/LookupMemberRequest.php new file mode 100644 index 0000000..5265792 --- /dev/null +++ b/be/Modules/MembershipApplication/Http/Requests/LookupMemberRequest.php @@ -0,0 +1,34 @@ +|string> + */ + public function rules(): array + { + return [ + 'ic_number' => 'required|string|max:20', + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'ic_number.required' => 'Nombor kad pengenalan diperlukan.', + 'ic_number.max' => 'Nombor kad pengenalan tidak sah.', + ]; + } +} diff --git a/be/Modules/MembershipApplication/Http/Requests/ManagementReviewRequest.php b/be/Modules/MembershipApplication/Http/Requests/ManagementReviewRequest.php new file mode 100644 index 0000000..2271a5e --- /dev/null +++ b/be/Modules/MembershipApplication/Http/Requests/ManagementReviewRequest.php @@ -0,0 +1,38 @@ +|string> + */ + public function rules(): array + { + return [ + 'decision' => ['required', Rule::enum(ManagementDecision::class)], + 'remarks' => 'nullable|string|max:1000', + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'decision.required' => 'Keputusan diperlukan.', + 'decision.enum' => 'Keputusan tidak sah. Pilih APPROVED atau REJECTED.', + 'remarks.max' => 'Catatan tidak boleh melebihi 1000 aksara.', + ]; + } +} diff --git a/be/Modules/MembershipApplication/Http/Requests/StoreMembershipApplicationRequest.php b/be/Modules/MembershipApplication/Http/Requests/StoreMembershipApplicationRequest.php new file mode 100644 index 0000000..8027dc1 --- /dev/null +++ b/be/Modules/MembershipApplication/Http/Requests/StoreMembershipApplicationRequest.php @@ -0,0 +1,94 @@ +|string> + */ + public function rules(): array + { + return [ + 'applicant.name' => 'required|string|max:255', + 'applicant.email' => 'required|email|max:255', + 'applicant.ic_number' => 'required|string|max:20', + 'applicant.birth_date' => 'required|date', + 'applicant.birth_place' => 'required|string|max:255', + 'applicant.gender' => ['required', Rule::in(['Lelaki', 'Perempuan'])], + 'applicant.marriage_status' => 'required|string|max:255', + 'applicant.address' => 'required|string', + 'applicant.phone_number' => 'required|string|max:20', + 'applicant.office_number' => 'nullable|string|max:20', + 'applicant.postcode' => 'required|string|max:10', + 'applicant.employer_name' => 'required|string|max:255', + 'applicant.employer_address' => 'required|string', + 'applicant.current_position' => 'required|string|max:255', + 'applicant.start_work_date' => 'required|date', + 'applicant.stock_monthly_contribution' => 'required|numeric|min:0', + 'applicant.fee_monthly_contribution' => 'required|numeric|min:0', + + 'heirs' => 'required|array|min:1', + 'heirs.*.name' => 'required|string|max:255', + 'heirs.*.ic_number' => 'required|string|max:20', + 'heirs.*.relationship' => 'required|string|max:255', + 'heirs.*.phone_number' => 'required|string|max:20', + + 'references.proposer_ic_number' => 'nullable|string|max:20', + 'references.supporter_ic_number' => 'nullable|string|max:20', + + 'documents.ic_copy' => 'required|file|mimes:pdf,jpg,jpeg,png|max:10240', + 'documents.photo' => 'nullable|file|mimes:jpg,jpeg,png|max:10240', + 'documents.salary_slip' => 'nullable|file|mimes:pdf,jpg,jpeg,png|max:10240', + 'documents.employer_letter' => 'nullable|file|mimes:pdf,jpg,jpeg,png|max:10240', + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'applicant.name.required' => 'Nama penuh diperlukan.', + 'applicant.email.required' => 'Emel diperlukan.', + 'applicant.email.email' => 'Emel tidak sah.', + 'applicant.ic_number.required' => 'Nombor kad pengenalan diperlukan.', + 'applicant.birth_date.required' => 'Tarikh lahir diperlukan.', + 'applicant.birth_place.required' => 'Tempat lahir diperlukan.', + 'applicant.gender.required' => 'Jantina diperlukan.', + 'applicant.gender.in' => 'Jantina tidak sah.', + 'applicant.marriage_status.required' => 'Status perkahwinan diperlukan.', + 'applicant.address.required' => 'Alamat diperlukan.', + 'applicant.phone_number.required' => 'Nombor telefon diperlukan.', + 'applicant.postcode.required' => 'Poskod diperlukan.', + 'applicant.employer_name.required' => 'Nama majikan diperlukan.', + 'applicant.employer_address.required' => 'Alamat majikan diperlukan.', + 'applicant.current_position.required' => 'Jawatan semasa diperlukan.', + 'applicant.start_work_date.required' => 'Tarikh mula berkhidmat diperlukan.', + 'applicant.stock_monthly_contribution.required' => 'Caruman saham bulanan diperlukan.', + 'applicant.fee_monthly_contribution.required' => 'Caruman yuran bulanan diperlukan.', + + 'heirs.required' => 'Maklumat waris diperlukan.', + 'heirs.min' => 'Sekurang-kurangnya satu waris diperlukan.', + 'heirs.*.name.required' => 'Nama waris diperlukan.', + 'heirs.*.ic_number.required' => 'Nombor kad pengenalan waris diperlukan.', + 'heirs.*.relationship.required' => 'Hubungan waris diperlukan.', + 'heirs.*.phone_number.required' => 'Nombor telefon waris diperlukan.', + + 'references.proposer_ic_number.max' => 'Nombor kad pengenalan pencadang tidak sah.', + 'references.supporter_ic_number.max' => 'Nombor kad pengenalan penyokong tidak sah.', + + 'documents.ic_copy.required' => 'Salinan kad pengenalan diperlukan.', + 'documents.ic_copy.mimes' => 'Salinan kad pengenalan mestilah PDF, JPG, JPEG atau PNG.', + ]; + } +} diff --git a/be/Modules/MembershipApplication/Http/Requests/UpdateMembershipApplicationRequest.php b/be/Modules/MembershipApplication/Http/Requests/UpdateMembershipApplicationRequest.php new file mode 100644 index 0000000..b987064 --- /dev/null +++ b/be/Modules/MembershipApplication/Http/Requests/UpdateMembershipApplicationRequest.php @@ -0,0 +1,95 @@ + + */ + public function validationData(): array + { + return $this->except(array_keys($this->route()?->parameters() ?? [])); + } + + /** + * @return array|string> + */ + public function rules(): array + { + return [ + 'applicant' => 'sometimes|array', + 'applicant.name' => 'sometimes|required|string|max:255', + 'applicant.email' => 'sometimes|required|email|max:255', + 'applicant.ic_number' => 'sometimes|required|string|max:20', + 'applicant.birth_date' => 'sometimes|required|date', + 'applicant.birth_place' => 'sometimes|required|string|max:255', + 'applicant.gender' => ['sometimes', 'required', Rule::in(['Lelaki', 'Perempuan'])], + 'applicant.marriage_status' => 'sometimes|required|string|max:255', + 'applicant.address' => 'sometimes|required|string', + 'applicant.phone_number' => 'sometimes|required|string|max:20', + 'applicant.office_number' => 'nullable|string|max:20', + 'applicant.postcode' => 'sometimes|required|string|max:10', + 'applicant.employer_name' => 'sometimes|required|string|max:255', + 'applicant.employer_address' => 'sometimes|required|string', + 'applicant.current_position' => 'sometimes|required|string|max:255', + 'applicant.start_work_date' => 'sometimes|required|date', + 'applicant.stock_monthly_contribution' => 'sometimes|required|numeric|min:0', + 'applicant.fee_monthly_contribution' => 'sometimes|required|numeric|min:0', + + 'heirs' => 'sometimes|array|min:1', + 'heirs.*.name' => 'required|string|max:255', + 'heirs.*.ic_number' => 'required|string|max:20', + 'heirs.*.relationship' => 'required|string|max:255', + 'heirs.*.phone_number' => 'required|string|max:20', + + 'references' => 'sometimes|array', + 'references.proposer_ic_number' => 'nullable|string|max:20', + 'references.supporter_ic_number' => 'nullable|string|max:20', + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'applicant.name.required' => 'Nama penuh diperlukan.', + 'applicant.email.required' => 'Emel diperlukan.', + 'applicant.email.email' => 'Emel tidak sah.', + 'applicant.ic_number.required' => 'Nombor kad pengenalan diperlukan.', + 'applicant.birth_date.required' => 'Tarikh lahir diperlukan.', + 'applicant.birth_place.required' => 'Tempat lahir diperlukan.', + 'applicant.gender.required' => 'Jantina diperlukan.', + 'applicant.gender.in' => 'Jantina tidak sah.', + 'applicant.marriage_status.required' => 'Status perkahwinan diperlukan.', + 'applicant.address.required' => 'Alamat diperlukan.', + 'applicant.phone_number.required' => 'Nombor telefon diperlukan.', + 'applicant.postcode.required' => 'Poskod diperlukan.', + 'applicant.employer_name.required' => 'Nama majikan diperlukan.', + 'applicant.employer_address.required' => 'Alamat majikan diperlukan.', + 'applicant.current_position.required' => 'Jawatan semasa diperlukan.', + 'applicant.start_work_date.required' => 'Tarikh mula berkhidmat diperlukan.', + 'applicant.stock_monthly_contribution.required' => 'Caruman saham bulanan diperlukan.', + 'applicant.fee_monthly_contribution.required' => 'Caruman yuran bulanan diperlukan.', + + 'heirs.min' => 'Sekurang-kurangnya satu waris diperlukan.', + 'heirs.*.name.required' => 'Nama waris diperlukan.', + 'heirs.*.ic_number.required' => 'Nombor kad pengenalan waris diperlukan.', + 'heirs.*.relationship.required' => 'Hubungan waris diperlukan.', + 'heirs.*.phone_number.required' => 'Nombor telefon waris diperlukan.', + + 'references.proposer_ic_number.max' => 'Nombor kad pengenalan pencadang tidak sah.', + 'references.supporter_ic_number.max' => 'Nombor kad pengenalan penyokong tidak sah.', + ]; + } +} diff --git a/be/Modules/MembershipApplication/Http/Requests/UploadMembershipApplicationDocumentRequest.php b/be/Modules/MembershipApplication/Http/Requests/UploadMembershipApplicationDocumentRequest.php new file mode 100644 index 0000000..542495f --- /dev/null +++ b/be/Modules/MembershipApplication/Http/Requests/UploadMembershipApplicationDocumentRequest.php @@ -0,0 +1,48 @@ +|string> + */ + public function rules(): array + { + return [ + 'type' => ['required', Rule::in(['ic_copy', 'photo', 'salary_slip', 'employer_letter'])], + 'file' => [ + 'required', + 'file', + 'max:10240', + Rule::when( + $this->input('type') === 'photo', + 'mimes:jpg,jpeg,png', + 'mimes:pdf,jpg,jpeg,png', + ), + ], + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'type.required' => 'Jenis dokumen diperlukan.', + 'type.in' => 'Jenis dokumen tidak sah.', + 'file.required' => 'Fail diperlukan.', + 'file.mimes' => 'Format fail tidak disokong.', + 'file.max' => 'Saiz fail melebihi had maksimum 10MB.', + ]; + } +} diff --git a/be/Modules/MembershipApplication/Jobs/.gitkeep b/be/Modules/MembershipApplication/Jobs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/MembershipApplication/Notifications/.gitkeep b/be/Modules/MembershipApplication/Notifications/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/MembershipApplication/Policies/.gitkeep b/be/Modules/MembershipApplication/Policies/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/MembershipApplication/Policies/MembershipApplicationPolicy.php b/be/Modules/MembershipApplication/Policies/MembershipApplicationPolicy.php new file mode 100644 index 0000000..9ce7624 --- /dev/null +++ b/be/Modules/MembershipApplication/Policies/MembershipApplicationPolicy.php @@ -0,0 +1,75 @@ +hasPermissionTo('lihat permohonan keahlian'); + } + + /** + * Determine whether the user can view the model. + */ + public function view($user, ?MembershipApplication $membershipApplication = null): bool + { + return $user->hasPermissionTo('lihat permohonan keahlian'); + } + + /** + * Management officer: approve or reject an application. + */ + public function managementReview($user, ?MembershipApplication $membershipApplication = null): bool + { + return $user->hasPermissionTo('semak permohonan keahlian pentadbiran'); + } + + /** + * Board member: pass or fail an application. + */ + public function boardReview($user, ?MembershipApplication $membershipApplication = null): bool + { + return $user->hasPermissionTo('semak permohonan keahlian lembaga'); + } + + /** + * General manager: send result notification and create account if passed. + */ + public function complete($user, ?MembershipApplication $membershipApplication = null): bool + { + return $user->hasPermissionTo('selesaikan permohonan keahlian'); + } + + /** + * General manager: batch complete multiple applications. + */ + public function completeBatch($user): bool + { + return $user->hasPermissionTo('selesaikan permohonan keahlian'); + } + + /** + * Admin: assign or update proposer and supporter. + */ + public function assignReferences($user, ?MembershipApplication $membershipApplication = null): bool + { + return $user->hasPermissionTo('tetapkan pencadang penyokong'); + } + + /** + * Admin: edit application details before completion. + */ + public function update($user, ?MembershipApplication $membershipApplication = null): bool + { + return $user->hasPermissionTo('kemaskini permohonan keahlian'); + } +} diff --git a/be/Modules/MembershipApplication/Providers/.gitkeep b/be/Modules/MembershipApplication/Providers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/MembershipApplication/Providers/EventServiceProvider.php b/be/Modules/MembershipApplication/Providers/EventServiceProvider.php new file mode 100644 index 0000000..9af6b7f --- /dev/null +++ b/be/Modules/MembershipApplication/Providers/EventServiceProvider.php @@ -0,0 +1,27 @@ +> + */ + protected $listen = []; + + /** + * Indicates if events should be discovered. + * + * @var bool + */ + protected static $shouldDiscoverEvents = true; + + /** + * Configure the proper event listeners for email verification. + */ + protected function configureEmailVerification(): void {} +} diff --git a/be/Modules/MembershipApplication/Providers/MembershipApplicationServiceProvider.php b/be/Modules/MembershipApplication/Providers/MembershipApplicationServiceProvider.php new file mode 100644 index 0000000..26c48fc --- /dev/null +++ b/be/Modules/MembershipApplication/Providers/MembershipApplicationServiceProvider.php @@ -0,0 +1,186 @@ +registerRateLimiters(); + $this->registerCommands(); + $this->registerCommandSchedules(); + $this->registerTranslations(); + $this->registerConfig(); + $this->registerViews(); + $this->loadMigrationsFrom(module_path($this->name, 'Database/Migrations')); + $this->registerPolicies(); + } + + /** + * Register policies. + */ + protected function registerPolicies(): void + { + Gate::policy(MembershipApplication::class, MembershipApplicationPolicy::class); + } + + /** + * Register the service provider. + */ + public function register(): void + { + $this->app->register(EventServiceProvider::class); + $this->app->register(RouteServiceProvider::class); + + // Register repository binding + $this->app->bind( + \Modules\MembershipApplication\Repositories\Contracts\MembershipApplicationRepositoryInterface::class, + \Modules\MembershipApplication\Repositories\MembershipApplicationRepository::class + ); + } + + protected function registerRateLimiters(): void + { + RateLimiter::for('membership-application-submit', function ($request) { + return Limit::perHour(10)->by($request->ip()); + }); + + RateLimiter::for('membership-application-lookup', function ($request) { + return Limit::perMinute(30)->by($request->ip()); + }); + } + + /** + * Register commands in the format of Command::class + */ + protected function registerCommands(): void + { + // $this->commands([]); + } + + /** + * Register command Schedules. + */ + protected function registerCommandSchedules(): void + { + // $this->app->booted(function () { + // $schedule = $this->app->make(Schedule::class); + // $schedule->command('inspire')->hourly(); + // }); + } + + /** + * Register translations. + */ + public function registerTranslations(): void + { + $langPath = resource_path('lang/modules/'.$this->nameLower); + + if (is_dir($langPath)) { + $this->loadTranslationsFrom($langPath, $this->nameLower); + $this->loadJsonTranslationsFrom($langPath); + } else { + $this->loadTranslationsFrom(module_path($this->name, 'Lang'), $this->nameLower); + $this->loadJsonTranslationsFrom(module_path($this->name, 'Lang')); + } + } + + /** + * Register config. + */ + protected function registerConfig(): void + { + $configPath = module_path($this->name, config('modules.paths.generator.config.path')); + + if (is_dir($configPath)) { + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($configPath)); + + foreach ($iterator as $file) { + if ($file->isFile() && $file->getExtension() === 'php') { + $config = str_replace($configPath.DIRECTORY_SEPARATOR, '', $file->getPathname()); + $config_key = str_replace([DIRECTORY_SEPARATOR, '.php'], ['.', ''], $config); + $segments = explode('.', $this->nameLower.'.'.$config_key); + + // Remove duplicated adjacent segments + $normalized = []; + foreach ($segments as $segment) { + if (end($normalized) !== $segment) { + $normalized[] = $segment; + } + } + + $key = ($config === 'config.php') ? $this->nameLower : implode('.', $normalized); + + $this->publishes([$file->getPathname() => config_path($config)], 'config'); + $this->merge_config_from($file->getPathname(), $key); + } + } + } + } + + /** + * Merge config from the given path recursively. + */ + protected function merge_config_from(string $path, string $key): void + { + $existing = config($key, []); + $module_config = require $path; + + config([$key => array_replace_recursive($existing, $module_config)]); + } + + /** + * Register views. + */ + public function registerViews(): void + { + $viewPath = resource_path('views/modules/'.$this->nameLower); + $sourcePath = module_path($this->name, 'Resources/Views'); + + $this->publishes([$sourcePath => $viewPath], ['views', $this->nameLower.'-module-views']); + + $this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), $this->nameLower); + + Blade::componentNamespace(config('modules.namespace').'\\' . $this->name . '\\View\\Components', $this->nameLower); + } + + /** + * Get the services provided by the provider. + */ + public function provides(): array + { + return []; + } + + private function getPublishableViewPaths(): array + { + $paths = []; + foreach (config('view.paths') as $path) { + if (is_dir($path.'/modules/'.$this->nameLower)) { + $paths[] = $path.'/modules/'.$this->nameLower; + } + } + + return $paths; + } +} diff --git a/be/Modules/MembershipApplication/Providers/RouteServiceProvider.php b/be/Modules/MembershipApplication/Providers/RouteServiceProvider.php new file mode 100644 index 0000000..41c7cf8 --- /dev/null +++ b/be/Modules/MembershipApplication/Providers/RouteServiceProvider.php @@ -0,0 +1,50 @@ +mapApiRoutes(); + + } + + /** + * Define the "web" routes for the application. + * + * These routes all receive session state, CSRF protection, etc. + */ + protected function mapWebRoutes(): void + { + Route::middleware('web')->group(module_path($this->name, '/Routes/web.php')); + } + + /** + * Define the "api" routes for the application. + * + * These routes are typically stateless. + */ + protected function mapApiRoutes(): void + { + Route::middleware('api')->name('api.')->group(module_path($this->name, '/Routes/api.php')); + } +} diff --git a/be/Modules/MembershipApplication/Repositories/.gitkeep b/be/Modules/MembershipApplication/Repositories/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/MembershipApplication/Repositories/Contracts/.gitkeep b/be/Modules/MembershipApplication/Repositories/Contracts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/MembershipApplication/Repositories/Contracts/MembershipApplicationRepositoryInterface.php b/be/Modules/MembershipApplication/Repositories/Contracts/MembershipApplicationRepositoryInterface.php new file mode 100644 index 0000000..61ee511 --- /dev/null +++ b/be/Modules/MembershipApplication/Repositories/Contracts/MembershipApplicationRepositoryInterface.php @@ -0,0 +1,23 @@ +find($id); + } + + public function getAllPaginated( + int $perPage = 10, + string $search = '', + ?string $status = null, + string $sortBy = 'submitted_at', + string $sortOrder = 'desc' + ): LengthAwarePaginator { + $allowedSortColumns = [ + 'application_number', + 'status', + 'submitted_at', + 'created_at', + ]; + $sortBy = in_array($sortBy, $allowedSortColumns, true) ? $sortBy : 'submitted_at'; + $sortOrder = strtolower($sortOrder) === 'asc' ? 'asc' : 'desc'; + + $query = MembershipApplication::query() + ->with(['applicant:id,membership_application_id,name,email,ic_number']) + ->orderBy($sortBy, $sortOrder); + + if ($status !== null && $status !== '') { + $query->where('status', $status); + } + + if ($search !== '') { + $query->where(function ($q) use ($search) { + $q->where('application_number', 'ILIKE', "%{$search}%") + ->orWhereHas('applicant', function ($applicantQuery) use ($search) { + $applicantQuery->where('name', 'ILIKE', "%{$search}%") + ->orWhere('email', 'ILIKE', "%{$search}%") + ->orWhere('ic_number', 'ILIKE', "%{$search}%"); + }); + }); + } + + return $query->paginate($perPage); + } +} diff --git a/be/Modules/MembershipApplication/Resources/views/emails/failed.blade.php b/be/Modules/MembershipApplication/Resources/views/emails/failed.blade.php new file mode 100644 index 0000000..62b70a8 --- /dev/null +++ b/be/Modules/MembershipApplication/Resources/views/emails/failed.blade.php @@ -0,0 +1,18 @@ +@component('mail::message') +@if (! empty($logoPath) && file_exists($logoPath)) +
+ {{ config('app.name') }} +
+@endif + +# Keputusan Permohonan Keahlian + +Assalamualaikum **{{ $name }}**, + +Permohonan keahlian anda (**{{ $applicationNumber }}**) **tidak diluluskan** pada peringkat lembaga. + +Terima kasih atas minat anda. Untuk sebarang pertanyaan, sila hubungi pejabat koperasi. + +Terima kasih,
+{{ config('app.name') }} +@endcomponent diff --git a/be/Modules/MembershipApplication/Resources/views/emails/passed.blade.php b/be/Modules/MembershipApplication/Resources/views/emails/passed.blade.php new file mode 100644 index 0000000..3c962c2 --- /dev/null +++ b/be/Modules/MembershipApplication/Resources/views/emails/passed.blade.php @@ -0,0 +1,18 @@ +@component('mail::message') +@if (! empty($logoPath) && file_exists($logoPath)) +
+ {{ config('app.name') }} +
+@endif + +# Keputusan Permohonan Keahlian + +Assalamualaikum **{{ $name }}**, + +Permohonan keahlian anda (**{{ $applicationNumber }}**) **diluluskan** pada peringkat lembaga. + +Terima kasih atas minat anda. Untuk sebarang pertanyaan, sila hubungi pejabat koperasi. + +Terima kasih,
+{{ config('app.name') }} +@endcomponent diff --git a/be/Modules/MembershipApplication/Resources/views/emails/submitted.blade.php b/be/Modules/MembershipApplication/Resources/views/emails/submitted.blade.php new file mode 100644 index 0000000..7a75b9e --- /dev/null +++ b/be/Modules/MembershipApplication/Resources/views/emails/submitted.blade.php @@ -0,0 +1,79 @@ +@component('mail::message') +@if (! empty($logoPath) && file_exists($logoPath)) +
+ {{ config('app.name') }} +
+@endif + +# Pengesahan Permohonan Keahlian + +Assalamualaikum **{{ $applicant->name }}**, + +Terima kasih. Permohonan keahlian anda telah berjaya diterima. + +**No. Permohonan:** {{ $application->application_number }} + +Berikut adalah ringkasan maklumat yang anda hantar: + +## Maklumat Peribadi + +- **Nama:** {{ $applicant->name }} +- **Emel:** {{ $applicant->email }} +- **No. Kad Pengenalan:** {{ $applicant->ic_number }} +- **Tarikh Lahir:** {{ $applicant->birth_date }} +- **Tempat Lahir:** {{ $applicant->birth_place }} +- **Jantina:** {{ $applicant->gender }} +- **Status Perkahwinan:** {{ $applicant->marriage_status }} + +## Hubungan & Alamat + +- **Alamat:** {{ $applicant->address }} +- **Poskod:** {{ $applicant->postcode }} +- **No. Telefon:** {{ $applicant->phone_number }} +- **No. Pejabat:** {{ $applicant->office_number ?: '-' }} + +## Maklumat Pekerjaan & Caruman + +- **Nama Majikan:** {{ $applicant->employer_name }} +- **Alamat Majikan:** {{ $applicant->employer_address }} +- **Jawatan Semasa:** {{ $applicant->current_position }} +- **Tarikh Mula Berkhidmat:** {{ $applicant->start_work_date?->format('d/m/Y') ?? $applicant->start_work_date }} +- **Caruman Saham (RM):** {{ number_format((float) $applicant->stock_monthly_contribution, 2) }} +- **Caruman Yuran (RM):** {{ number_format((float) $applicant->fee_monthly_contribution, 2) }} + +## Maklumat Waris + +@forelse ($heirs as $index => $heir) +**Waris {{ $index + 1 }}** +- **Nama:** {{ $heir->name }} +- **No. Kad Pengenalan:** {{ $heir->ic_number }} +- **Hubungan:** {{ $heir->relationship }} +- **No. Telefon:** {{ $heir->phone_number }} + +@empty +Tiada maklumat waris. +@endforelse + +## Pencadang & Penyokong + +@php + $proposer = $application->references->first(fn ($ref) => ($ref->reference_type?->value ?? $ref->reference_type) === 'PROPOSER'); + $supporter = $application->references->first(fn ($ref) => ($ref->reference_type?->value ?? $ref->reference_type) === 'SUPPORTER'); +@endphp + +- **Pencadang:** {{ $proposer?->member?->name ? $proposer->member->name . ' (' . $proposer->member->ic_number . ')' : '-' }} +- **Penyokong:** {{ $supporter?->member?->name ? $supporter->member->name . ' (' . $supporter->member->ic_number . ')' : '-' }} + +## Dokumen Dimuat Naik + +@forelse ($documents as $document) +- **{{ $documentLabels[$document->type] ?? $document->type }}:** {{ $document->name }} +@empty +Tiada dokumen. +@endforelse + +Anda akan menerima emel apabila keputusan permohonan tersedia. + +Terima kasih,
+{{ config('app.name') }} +@endcomponent diff --git a/be/Modules/MembershipApplication/Resources/views/pdf/approved-letter.blade.php b/be/Modules/MembershipApplication/Resources/views/pdf/approved-letter.blade.php new file mode 100644 index 0000000..3358ce7 --- /dev/null +++ b/be/Modules/MembershipApplication/Resources/views/pdf/approved-letter.blade.php @@ -0,0 +1,12 @@ +{{-- + Approval letter — extends the shared base layout. + Fill in sections below when the final design is ready. +--}} +@extends('pdf.layouts.letter') + +{{-- @section('letter-reference', $application->application_number) --}} +{{-- @section('letter-date', ...) --}} +{{-- @section('letter-recipient') ... @endsection --}} +{{-- @section('letter-subject') PERKARA: ... @endsection --}} +{{-- @section('letter-body') ... @endsection --}} +{{-- @section('letter-signature') ... @endsection --}} diff --git a/be/Modules/MembershipApplication/Routes/.gitkeep b/be/Modules/MembershipApplication/Routes/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/MembershipApplication/Routes/api.php b/be/Modules/MembershipApplication/Routes/api.php new file mode 100644 index 0000000..7989d78 --- /dev/null +++ b/be/Modules/MembershipApplication/Routes/api.php @@ -0,0 +1,46 @@ +group(function () { + Route::middleware('throttle:membership-application-lookup')->get('membership-applications/member-lookup',[PublicMembershipApplicationController::class, 'lookupMember'])->name('membership-application.public.member-lookup'); + + Route::middleware('throttle:membership-application-submit')->post('membership-applications',[PublicMembershipApplicationController::class, 'store'])->name('membership-application.public.store'); +}); + +// admin routes +Route::middleware(['auth:sanctum', 'single.session'])->prefix('v1')->group(function () { + // Membership application management routes + Route::get('membership-applications', [MembershipApplicationController::class, 'index'])->name('membership-application.index'); + + // Membership application detail routes + Route::get('membership-applications/{membershipApplication}', [MembershipApplicationController::class, 'show'])->name('membership-application.show'); + + // Membership application update routes - admin + Route::patch('membership-applications/{membershipApplication}', [MembershipApplicationController::class, 'update'])->name('membership-application.update'); + + // Membership application management review routes (level 1) + Route::post('membership-applications/{membershipApplication}/management-review', [MembershipApplicationController::class, 'managementReview'])->name('membership-application.management-review'); + + // Membership application board review routes (level 2) + Route::post('membership-applications/{membershipApplication}/board-review', [MembershipApplicationController::class, 'boardReview'])->name('membership-application.board-review'); + + // Membership application assign references routes + Route::put('membership-applications/{membershipApplication}/references', [MembershipApplicationController::class, 'assignReferences'])->name('membership-application.assign-references'); + + // Membership application batch complete routes (level 3) + Route::post('membership-applications/batch-complete', [MembershipApplicationController::class, 'batchComplete'])->name('membership-application.batch-complete'); + // Membership application complete routes (level 3) + Route::post('membership-applications/{membershipApplication}/complete', [MembershipApplicationController::class, 'complete'])->name('membership-application.complete'); + + // Membership application upload document routes + Route::post('membership-applications/{membershipApplication}/documents', [MembershipApplicationController::class, 'uploadDocument'])->name('membership-application.upload-document'); + // download document routes + Route::get('membership-applications/{membershipApplication}/documents/{documentId}/download', [MembershipApplicationController::class, 'downloadDocument'])->name('membership-application.download-document'); + + // approved letter preview (html or pdf) + Route::get('membership-applications/{membershipApplication}/approved-letter', [MembershipApplicationController::class, 'approvedLetter'])->name('membership-application.approved-letter'); +}); diff --git a/be/Modules/MembershipApplication/Routes/web.php b/be/Modules/MembershipApplication/Routes/web.php new file mode 100644 index 0000000..ccef19d --- /dev/null +++ b/be/Modules/MembershipApplication/Routes/web.php @@ -0,0 +1,8 @@ +group(function () { + Route::resource('membershipapplications', MembershipApplicationController::class)->names('membershipapplication'); +}); diff --git a/be/Modules/MembershipApplication/Services/.gitkeep b/be/Modules/MembershipApplication/Services/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/MembershipApplication/Services/MembershipApplicationService.php b/be/Modules/MembershipApplication/Services/MembershipApplicationService.php new file mode 100644 index 0000000..7cb0712 --- /dev/null +++ b/be/Modules/MembershipApplication/Services/MembershipApplicationService.php @@ -0,0 +1,478 @@ +, + * heirs: list>, + * references?: array{proposer_ic_number?: string|null, supporter_ic_number?: string|null} + * } $data + * @param array $documents + */ + public function submit(array $data, array $documents): MembershipApplication + { + $application = DB::transaction(function () use ($data, $documents) { + // membership_applications table + $application = $this->repository->create([ + 'application_number' => $this->generateApplicationNumber(), + 'status' => ApplicationStatus::Submitted, + 'board_result' => '', + 'submitted_at' => now()->toDateTimeString(), + 'completed_at' => '', + ]); + + // membership_application_applicants table + $application->applicant()->create($data['applicant']); + + // membership_application_heirs table + foreach ($data['heirs'] as $heir) { + $application->heirs()->create($heir); + } + + // membership_application_references table + $this->syncReferences( + $application, + $data['references'] ?? [], + (string) ($data['applicant']['ic_number'] ?? ''), + ); + + // documents table + foreach ($documents as $type => $file) { + $this->documentService->validateFile($file); + $this->documentService->uploadDocument($application, $file, $type); + } + + return $this->repository->findByIdWithRelations($application->id); + }); + + $this->sendSubmissionConfirmation($application); + + return $application; + } + + public function getPaginatedList(int $perPage, string $search, ?string $status, string $sortBy, string $sortOrder): LengthAwarePaginator + { + return $this->repository->getAllPaginated($perPage, $search, $status, $sortBy, $sortOrder); + } + + public function getByIdWithRelations(string $id): ?MembershipApplication + { + return $this->repository->findByIdWithRelations($id); + } + + public function update(MembershipApplication $application, array $data): MembershipApplication + { + if ($application->status === ApplicationStatus::Completed) { + throw ValidationException::withMessages([ + 'status' => ['Permohonan yang telah selesai tidak boleh dikemaskini.'], + ]); + } + + return DB::transaction(function () use ($application, $data) { + $application->loadMissing('applicant'); + + if (isset($data['applicant'])) { + $application->applicant->update($data['applicant']); + } + + if (isset($data['heirs'])) { + $application->heirs()->delete(); + foreach ($data['heirs'] as $heir) { + $application->heirs()->create($heir); + } + } + + if (array_key_exists('references', $data)) { + $this->syncReferences( + $application, + $data['references'] ?? [], + (string) ($data['applicant']['ic_number'] ?? $application->applicant->ic_number), + ); + } + + return $this->repository->findByIdWithRelations($application->id); + }); + } + + public function uploadDocument(MembershipApplication $application, string $type, UploadedFile $file): MembershipApplication + { + if ($application->status === ApplicationStatus::Completed) { + throw ValidationException::withMessages([ + 'status' => ['Permohonan yang telah selesai tidak boleh dikemaskini.'], + ]); + } + + $this->documentService->validateFile($file); + + $application->documentsOfType($type)->get()->each( + fn ($document) => $this->documentService->deleteDocument($document->id) + ); + + $this->documentService->uploadDocument($application, $file, $type); + + return $this->repository->findByIdWithRelations($application->id); + } + + public function managementReview(MembershipApplication $application, ManagementDecision $decision, ?string $remarks, User $reviewer): MembershipApplication { + if ($application->status !== ApplicationStatus::Submitted) { + throw ValidationException::withMessages([ + 'status' => ['Permohonan ini tidak boleh disemak pada peringkat pentadbiran.'], + ]); + } + + return DB::transaction(function () use ($application, $decision, $remarks, $reviewer) { + $application->reviews()->create([ + 'stage' => ReviewStage::Management, + 'decision' => $decision->value, + 'remarks' => $remarks, + 'reviewer_id' => $reviewer->id, + 'reviewed_at' => now(), + ]); + + $application->update([ + 'status' => $decision === ManagementDecision::Approved + ? ApplicationStatus::PendingBoard + : ApplicationStatus::ManagementRejected, + ]); + + return $this->repository->findByIdWithRelations($application->id); + }); + } + + public function boardReview(MembershipApplication $application, BoardDecision $decision, ?string $remarks, User $reviewer): MembershipApplication + { + if ($application->status !== ApplicationStatus::PendingBoard) { + throw ValidationException::withMessages([ + 'status' => ['Permohonan ini tidak boleh disemak pada peringkat lembaga.'], + ]); + } + + return DB::transaction(function () use ($application, $decision, $remarks, $reviewer) { + $application->reviews()->create([ + 'stage' => ReviewStage::Board, + 'decision' => $decision->value, + 'remarks' => $remarks, + 'reviewer_id' => $reviewer->id, + 'reviewed_at' => now(), + ]); + + $application->update([ + 'board_result' => $decision === BoardDecision::Pass + ? BoardResult::Pass->value + : BoardResult::Fail->value, + 'status' => ApplicationStatus::PendingNotification, + ]); + + return $this->repository->findByIdWithRelations($application->id); + }); + } + + /** + * @param array{proposer_id?: string|null, supporter_id?: string|null} $references + */ + public function assignReferences(MembershipApplication $application, array $references, User $assignedBy): MembershipApplication + { + return DB::transaction(function () use ($application, $references, $assignedBy) { + $referenceMap = [ + 'proposer_id' => ReferenceType::Proposer, + 'supporter_id' => ReferenceType::Supporter, + ]; + + foreach ($referenceMap as $key => $type) { + if (! array_key_exists($key, $references)) { + continue; + } + + $application->references()->updateOrCreate( + ['reference_type' => $type->value], + [ + 'user_id' => $references[$key], + 'assigned_by' => $references[$key] ? $assignedBy->id : null, + 'assigned_at' => $references[$key] ? now() : null, + ] + ); + } + + return $this->repository->findByIdWithRelations($application->id); + }); + } + + public function complete(MembershipApplication $application, User $processedBy): MembershipApplication + { + if ($application->status !== ApplicationStatus::PendingNotification) { + throw ValidationException::withMessages([ + 'status' => ['Permohonan ini tidak boleh diselesaikan pada masa ini.'], + ]); + } + + if (! in_array($application->board_result, [BoardResult::Pass->value, BoardResult::Fail->value], true)) { + throw ValidationException::withMessages([ + 'board_result' => ['Keputusan lembaga belum ditetapkan.'], + ]); + } + + return DB::transaction(function () use ($application) { + if ($application->board_result === BoardResult::Pass->value && ! $application->user_id) { + $user = $this->createMemberFromApplication($application); + $application->update(['user_id' => $user->id]); + } + + $this->sendResultNotification($application); + + $application->update([ + 'status' => ApplicationStatus::Completed, + 'completed_at' => now()->toDateTimeString(), + ]); + + return $this->repository->findByIdWithRelations($application->id); + }); + } + + /** + * @param list $applicationIds + * @return array{ + * succeeded: list, + * failed: list + * } + */ + public function completeBatch(array $applicationIds, User $processedBy): array + { + $succeeded = []; + $failed = []; + + foreach ($applicationIds as $id) { + $application = $this->repository->findById($id); + + if (! $application) { + $failed[] = [ + 'id' => $id, + 'application_number' => null, + 'message' => 'Permohonan tidak dijumpai.', + ]; + + continue; + } + + try { + $succeeded[] = $this->complete($application, $processedBy); + } catch (ValidationException $e) { + $failed[] = [ + 'id' => $id, + 'application_number' => $application->application_number, + 'message' => collect($e->errors())->flatten()->first() ?? 'Permohonan tidak boleh diselesaikan.', + ]; + } + } + + return [ + 'succeeded' => $succeeded, + 'failed' => $failed, + ]; + } + + protected function createMemberFromApplication(MembershipApplication $application): User + { + $application->loadMissing(['applicant', 'heirs']); + $applicant = $application->applicant; + + if (User::where('email', $applicant->email)->exists()) { + throw ValidationException::withMessages([ + 'email' => ['E-mel pemohon sudah didaftarkan dalam sistem.'], + ]); + } + + $user = User::create([ + 'name' => $applicant->name, + 'email' => $applicant->email, + 'password' => Hash::make(Str::password(16)), + 'ic_number' => $applicant->ic_number, + 'phone_number' => $applicant->phone_number, + 'position' => $applicant->current_position, + 'status' => 'active', + 'gender' => $applicant->gender, + 'marriage_status' => $applicant->marriage_status, + 'member_number' => $this->generateMemberNumber(), + 'member_type' => 'Anggota', + 'join_date' => now()->toDateString(), + 'birth_date' => $applicant->birth_date, + 'birth_place' => $applicant->birth_place, + ]); + + $role = Role::where('name', 'Anggota')->first(); + if ($role) { + $user->assignRole($role); + } + + $user->addresses()->create([ + 'address_type' => 'home', + 'address_line_1' => $applicant->address, + 'postcode' => $applicant->postcode, + ]); + + $user->employments()->create([ + 'company_name' => $applicant->employer_name, + 'job_title' => $applicant->current_position, + 'employment_type' => 'Permanent', + 'salary' => 0, + 'start_date' => $applicant->start_work_date, + 'is_current' => true, + ]); + + foreach ($application->heirs as $index => $heir) { + $user->heirs()->create([ + 'name' => $heir->name, + 'ic_number' => $heir->ic_number, + 'relationship' => $heir->relationship, + 'phone_number' => $heir->phone_number, + 'is_primary' => $index === 0, + ]); + } + + app(EmailVerificationOtpService::class)->send($user); + + return $user; + } + + public function lookupMemberByIcNumber(string $icNumber): ?User + { + $icNumber = trim($icNumber); + + if ($icNumber === '') { + return null; + } + + return User::query() + ->where('ic_number', $icNumber) + ->where('status', 'active') + ->first(); + } + + protected function sendSubmissionConfirmation(MembershipApplication $application): void + { + $application->loadMissing(['applicant', 'heirs', 'documents', 'references.member']); + + Notification::route('mail', $application->applicant->email) + ->notify(new MembershipApplicationSubmittedNotification($application)); + } + + protected function sendResultNotification(MembershipApplication $application): void + { + $application->loadMissing('applicant'); + + $notification = $application->board_result === BoardResult::Pass->value + ? new MembershipApplicationPassedNotification($application) + : new MembershipApplicationFailedNotification($application); + + Notification::route('mail', $application->applicant->email)->notify($notification); + } + + protected function generateMemberNumber(): int + { + return ((int) User::max('member_number')) + 1; + } + + /** + * @param array{proposer_ic_number?: string|null, supporter_ic_number?: string|null} $references + */ + protected function syncReferences(MembershipApplication $application, array $references, string $applicantIcNumber): void + { + $referenceMap = [ + 'references.proposer_ic_number' => ['field' => 'proposer_ic_number', 'type' => ReferenceType::Proposer], + 'references.supporter_ic_number' => ['field' => 'supporter_ic_number', 'type' => ReferenceType::Supporter], + ]; + + $resolvedUserIds = []; + $applicantIcNumber = trim($applicantIcNumber); + + foreach ($referenceMap as $errorKey => $config) { + if (! array_key_exists($config['field'], $references)) { + continue; + } + + $icNumber = trim((string) ($references[$config['field']] ?? '')); + + if ($icNumber === '') { + $application->references()->where('reference_type', $config['type']->value)->delete(); + + continue; + } + + if ($applicantIcNumber !== '' && strcasecmp($icNumber, $applicantIcNumber) === 0) { + throw ValidationException::withMessages([ + $errorKey => ['Pencadang/penyokong tidak boleh sama dengan pemohon.'], + ]); + } + + $member = $this->lookupMemberByIcNumber($icNumber); + + if (! $member) { + throw ValidationException::withMessages([ + $errorKey => ['Ahli dengan nombor kad pengenalan ini tidak dijumpai.'], + ]); + } + + if (in_array($member->id, $resolvedUserIds, true)) { + throw ValidationException::withMessages([ + $errorKey => ['Pencadang dan penyokong mestilah ahli yang berbeza.'], + ]); + } + + $resolvedUserIds[] = $member->id; + + $application->references()->updateOrCreate( + ['reference_type' => $config['type']->value], + ['user_id' => $member->id] + ); + } + } + + protected function generateApplicationNumber(): string + { + $year = now()->year; + $prefix = "APP-{$year}-"; + + $latestNumber = MembershipApplication::query() + ->where('application_number', 'like', "{$prefix}%") + ->orderByDesc('application_number') + ->value('application_number'); + + $sequence = 1; + + if ($latestNumber && preg_match('/-(\d+)$/', $latestNumber, $matches)) { + $sequence = ((int) $matches[1]) + 1; + } + + return sprintf('%s%05d', $prefix, $sequence); + } +} diff --git a/be/Modules/MembershipApplication/Tests/Feature/.gitkeep b/be/Modules/MembershipApplication/Tests/Feature/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/MembershipApplication/Tests/Unit/.gitkeep b/be/Modules/MembershipApplication/Tests/Unit/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/MembershipApplication/Transformers/.gitkeep b/be/Modules/MembershipApplication/Transformers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/MembershipApplication/Transformers/MembershipApplicationApplicantResource.php b/be/Modules/MembershipApplication/Transformers/MembershipApplicationApplicantResource.php new file mode 100644 index 0000000..cea8e34 --- /dev/null +++ b/be/Modules/MembershipApplication/Transformers/MembershipApplicationApplicantResource.php @@ -0,0 +1,33 @@ + $this->id, + 'name' => $this->name, + 'email' => $this->email, + 'ic_number' => $this->ic_number, + 'birth_date' => $this->birth_date, + 'birth_place' => $this->birth_place, + 'gender' => $this->gender, + 'marriage_status' => $this->marriage_status, + 'address' => $this->address, + 'phone_number' => $this->phone_number, + 'office_number' => $this->office_number, + 'postcode' => $this->postcode, + 'employer_name' => $this->employer_name, + 'employer_address' => $this->employer_address, + 'current_position' => $this->current_position, + 'start_work_date' => $this->start_work_date?->toDateString(), + 'stock_monthly_contribution' => $this->stock_monthly_contribution, + 'fee_monthly_contribution' => $this->fee_monthly_contribution, + ]; + } +} diff --git a/be/Modules/MembershipApplication/Transformers/MembershipApplicationDocumentResource.php b/be/Modules/MembershipApplication/Transformers/MembershipApplicationDocumentResource.php new file mode 100644 index 0000000..571a488 --- /dev/null +++ b/be/Modules/MembershipApplication/Transformers/MembershipApplicationDocumentResource.php @@ -0,0 +1,20 @@ + $this->id, + 'name' => $this->name, + 'type' => $this->type, + 'mime_type' => $this->mime_type, + 'file_size' => $this->file_size, + ]; + } +} diff --git a/be/Modules/MembershipApplication/Transformers/MembershipApplicationHeirResource.php b/be/Modules/MembershipApplication/Transformers/MembershipApplicationHeirResource.php new file mode 100644 index 0000000..dc03a53 --- /dev/null +++ b/be/Modules/MembershipApplication/Transformers/MembershipApplicationHeirResource.php @@ -0,0 +1,20 @@ + $this->id, + 'name' => $this->name, + 'ic_number' => $this->ic_number, + 'relationship' => $this->relationship, + 'phone_number' => $this->phone_number, + ]; + } +} diff --git a/be/Modules/MembershipApplication/Transformers/MembershipApplicationListResource.php b/be/Modules/MembershipApplication/Transformers/MembershipApplicationListResource.php new file mode 100644 index 0000000..eafacb6 --- /dev/null +++ b/be/Modules/MembershipApplication/Transformers/MembershipApplicationListResource.php @@ -0,0 +1,26 @@ + $this->id, + 'application_number' => $this->application_number, + 'status' => $this->status?->value ?? $this->status, + 'board_result' => $this->board_result ?: null, + 'submitted_at' => $this->submitted_at, + 'applicant' => $this->whenLoaded('applicant', fn () => [ + 'name' => $this->applicant->name, + 'email' => $this->applicant->email, + 'ic_number' => $this->applicant->ic_number, + ]), + 'created_at' => $this->created_at?->toISOString(), + ]; + } +} diff --git a/be/Modules/MembershipApplication/Transformers/MembershipApplicationReferenceResource.php b/be/Modules/MembershipApplication/Transformers/MembershipApplicationReferenceResource.php new file mode 100644 index 0000000..5a49e11 --- /dev/null +++ b/be/Modules/MembershipApplication/Transformers/MembershipApplicationReferenceResource.php @@ -0,0 +1,25 @@ + $this->id, + 'reference_type' => $this->reference_type?->value ?? $this->reference_type, + 'user_id' => $this->user_id, + 'member' => $this->whenLoaded('member', fn () => [ + 'id' => $this->member->id, + 'name' => $this->member->name, + 'ic_number' => $this->member->ic_number, + ]), + 'assigned_by' => $this->assigned_by, + 'assigned_at' => $this->assigned_at?->toISOString(), + ]; + } +} diff --git a/be/Modules/MembershipApplication/Transformers/MembershipApplicationResource.php b/be/Modules/MembershipApplication/Transformers/MembershipApplicationResource.php new file mode 100644 index 0000000..49989e3 --- /dev/null +++ b/be/Modules/MembershipApplication/Transformers/MembershipApplicationResource.php @@ -0,0 +1,28 @@ + $this->id, + 'application_number' => $this->application_number, + 'status' => $this->status?->value ?? $this->status, + 'board_result' => $this->board_result ?: null, + 'submitted_at' => $this->submitted_at, + 'completed_at' => $this->completed_at ?: null, + 'applicant' => new MembershipApplicationApplicantResource($this->whenLoaded('applicant')), + 'heirs' => MembershipApplicationHeirResource::collection($this->whenLoaded('heirs')), + 'references' => MembershipApplicationReferenceResource::collection($this->whenLoaded('references')), + 'documents' => MembershipApplicationDocumentResource::collection($this->whenLoaded('documents')), + 'reviews' => MembershipApplicationReviewResource::collection($this->whenLoaded('reviews')), + 'created_at' => $this->created_at?->toISOString(), + 'updated_at' => $this->updated_at?->toISOString(), + ]; + } +} diff --git a/be/Modules/MembershipApplication/Transformers/MembershipApplicationReviewResource.php b/be/Modules/MembershipApplication/Transformers/MembershipApplicationReviewResource.php new file mode 100644 index 0000000..ba7843e --- /dev/null +++ b/be/Modules/MembershipApplication/Transformers/MembershipApplicationReviewResource.php @@ -0,0 +1,24 @@ + $this->id, + 'stage' => $this->stage?->value ?? $this->stage, + 'decision' => $this->decision, + 'remarks' => $this->remarks, + 'reviewer' => $this->whenLoaded('reviewer', fn () => [ + 'id' => $this->reviewer->id, + 'name' => $this->reviewer->name, + ]), + 'reviewed_at' => $this->reviewed_at?->toISOString(), + ]; + } +} diff --git a/be/Modules/MembershipApplication/composer.json b/be/Modules/MembershipApplication/composer.json new file mode 100644 index 0000000..165fcd1 --- /dev/null +++ b/be/Modules/MembershipApplication/composer.json @@ -0,0 +1,30 @@ +{ + "name": "nwidart/membershipapplication", + "description": "", + "authors": [ + { + "name": "Nicolas Widart", + "email": "n.widart@gmail.com" + } + ], + "extra": { + "laravel": { + "providers": [], + "aliases": { + + } + } + }, + "autoload": { + "psr-4": { + "Modules\\MembershipApplication\\": "App", + "Modules\\MembershipApplication\\Database\\Factories\\": "database/factories/", + "Modules\\MembershipApplication\\Database\\Seeders\\": "database/seeders/" + } + }, + "autoload-dev": { + "psr-4": { + "Modules\\MembershipApplication\\Tests\\": "tests/" + } + } +} diff --git a/be/Modules/MembershipApplication/module.json b/be/Modules/MembershipApplication/module.json new file mode 100644 index 0000000..95a81e7 --- /dev/null +++ b/be/Modules/MembershipApplication/module.json @@ -0,0 +1,11 @@ +{ + "name": "MembershipApplication", + "alias": "membershipapplication", + "description": "", + "keywords": [], + "priority": 0, + "providers": [ + "Modules\\MembershipApplication\\Providers\\MembershipApplicationServiceProvider" + ], + "files": [] +} diff --git a/be/Modules/MembershipApplication/package.json b/be/Modules/MembershipApplication/package.json new file mode 100644 index 0000000..d6fbfc8 --- /dev/null +++ b/be/Modules/MembershipApplication/package.json @@ -0,0 +1,15 @@ +{ + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build" + }, + "devDependencies": { + "axios": "^1.1.2", + "laravel-vite-plugin": "^0.7.5", + "sass": "^1.69.5", + "postcss": "^8.3.7", + "vite": "^4.0.0" + } +} diff --git a/be/Modules/Role/Http/Controllers/RoleController.php b/be/Modules/Role/Http/Controllers/RoleController.php index f9a03ed..6320d20 100644 --- a/be/Modules/Role/Http/Controllers/RoleController.php +++ b/be/Modules/Role/Http/Controllers/RoleController.php @@ -13,6 +13,7 @@ use Modules\Role\Http\Requests\RoleRequest; use Modules\Role\Repositories\Contracts\RoleRepositoryInterface; use Modules\Role\Transformers\RoleResource; use App\Models\Permission; +use Illuminate\Support\Facades\Gate; use Illuminate\Validation\Rule; class RoleController extends BaseCrudController @@ -104,6 +105,10 @@ class RoleController extends BaseCrudController */ public function permissions(): JsonResponse { + if (! Gate::any(['create', 'update'], $this->modelClass)) { + abort(403); + } + $permissions = Permission::all(); return response()->json([ @@ -124,6 +129,8 @@ class RoleController extends BaseCrudController */ public function assignPermissions(Request $request, string $id): JsonResponse { + $this->authorize('update', $this->modelClass); + $request->validate([ 'permissions' => 'required|array', 'permissions.*' => ['required', 'uuid', Rule::exists('permissions', 'id')], @@ -143,7 +150,9 @@ class RoleController extends BaseCrudController * Get role with permissions */ public function show(string $id): JsonResponse - { + { + $this->authorize('view', $this->modelClass); + $role = $this->repository->find($id); return response()->json([ @@ -157,6 +166,8 @@ class RoleController extends BaseCrudController */ public function index(Request $request): JsonResponse { + $this->authorize('viewAny', $this->modelClass); + $search = $request->get('search', ''); $roles = $this->repository->withPermissions($search); @@ -165,17 +176,4 @@ class RoleController extends BaseCrudController 'data' => RoleResource::collection($roles), ]); } - - /** - * Public endpoint for registration form (no auth required) - */ - public function publicRole(): JsonResponse - { - $roles = $this->repository->all(); - - return response()->json([ - 'success' => true, - 'data' => RoleResource::collection($roles), - ]); - } } diff --git a/be/Modules/Role/Policies/RolePolicy.php b/be/Modules/Role/Policies/RolePolicy.php index 07a49df..b756050 100644 --- a/be/Modules/Role/Policies/RolePolicy.php +++ b/be/Modules/Role/Policies/RolePolicy.php @@ -14,7 +14,7 @@ class RolePolicy */ public function viewAny($user): bool { - return $user->hasPermissionTo('lihat jenis pengguna'); + return $user->hasPermissionTo('lihat peranan'); } /** @@ -22,7 +22,7 @@ class RolePolicy */ public function view($user, ?Role $role = null): bool { - return $user->hasPermissionTo('lihat jenis pengguna'); + return $user->hasPermissionTo('lihat peranan'); } /** @@ -30,7 +30,7 @@ class RolePolicy */ public function create($user): bool { - return $user->hasPermissionTo('tambah jenis pengguna'); + return $user->hasPermissionTo('tambah peranan'); } /** @@ -38,7 +38,7 @@ class RolePolicy */ public function update($user, ?Role $role = null): bool { - return $user->hasPermissionTo('kemaskini jenis pengguna'); + return $user->hasPermissionTo('kemaskini peranan'); } /** @@ -46,7 +46,7 @@ class RolePolicy */ public function deleteAny($user): bool { - return $user->hasPermissionTo('hapus jenis pengguna'); + return $user->hasPermissionTo('hapus peranan'); } /** @@ -54,6 +54,6 @@ class RolePolicy */ public function delete($user, ?Role $role = null): bool { - return $user->hasPermissionTo('hapus jenis pengguna'); + return $user->hasPermissionTo('hapus peranan'); } } diff --git a/be/Modules/Role/Routes/api.php b/be/Modules/Role/Routes/api.php index d75036b..8a9d6bf 100644 --- a/be/Modules/Role/Routes/api.php +++ b/be/Modules/Role/Routes/api.php @@ -3,11 +3,6 @@ use Illuminate\Support\Facades\Route; use Modules\Role\Http\Controllers\RoleController; -// Public route for registration form (no auth required) -Route::prefix('v1')->group(function () { - Route::get('public/roles', [RoleController::class, 'publicRole']); -}); - // Protected routes (auth required) Route::middleware(['auth:sanctum'])->prefix('v1')->group(function () { Route::apiResource('roles', RoleController::class)->names('role'); diff --git a/be/Modules/User/Database/Migrations/2026_06_09_025926_add_more_details_to_users_table.php b/be/Modules/User/Database/Migrations/2026_06_09_025926_add_more_details_to_users_table.php new file mode 100644 index 0000000..7c400dc --- /dev/null +++ b/be/Modules/User/Database/Migrations/2026_06_09_025926_add_more_details_to_users_table.php @@ -0,0 +1,61 @@ +string('marriage_status')->nullable() ->comment('Belum Berkahwin, Berkahwin, Bercerai, Balu, Duda'); + $table->integer('member_number')->nullable(); + $table->string('member_type')->nullable() ->comment('Anggota, Pesara'); + // join date + $table->date('join_date')->nullable(); + // birth date + $table->date('birth_date')->nullable(); + // birth place + $table->string('birth_place')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('gender'); + $table->dropColumn('marriage_status'); + $table->dropColumn('member_number'); + $table->dropColumn('member_type'); + $table->dropColumn('join_date'); + $table->dropColumn('birth_date'); + $table->dropColumn('birth_place'); + }); + + DB::statement("DROP TYPE IF EXISTS user_gender_enum"); + } +}; diff --git a/be/Modules/User/Database/Migrations/2026_06_09_081007_create_addresses_table.php b/be/Modules/User/Database/Migrations/2026_06_09_081007_create_addresses_table.php new file mode 100644 index 0000000..112319e --- /dev/null +++ b/be/Modules/User/Database/Migrations/2026_06_09_081007_create_addresses_table.php @@ -0,0 +1,35 @@ +uuid('id')->primary(); + $table->foreignUuid('user_id')->constrained('users')->onDelete('cascade'); + $table->string('address_type')->comment('home, office, billing')->nullable(); + $table->string('address_line_1')->nullable(); + $table->string('address_line_2')->nullable(); + $table->string('city')->nullable(); + $table->string('state')->nullable(); + $table->string('postcode')->nullable(); + $table->string('country')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('addresses'); + } +}; diff --git a/be/Modules/User/Database/Migrations/2026_06_10_022314_create_employments_table.php b/be/Modules/User/Database/Migrations/2026_06_10_022314_create_employments_table.php new file mode 100644 index 0000000..f216891 --- /dev/null +++ b/be/Modules/User/Database/Migrations/2026_06_10_022314_create_employments_table.php @@ -0,0 +1,35 @@ +uuid('id')->primary(); + $table->foreignUuid('user_id')->constrained('users')->onDelete('cascade'); + $table->string('company_name'); + $table->string('job_title'); + $table->string('employment_type')->comment('Permanent, Contract, Internship, Freelance'); + $table->decimal('salary', 10, 2); + $table->date('start_date'); + $table->date('end_date')->nullable(); + $table->boolean('is_current')->default(true); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('employments'); + } +}; diff --git a/be/Modules/User/Database/Migrations/2026_06_10_040351_create_banks_table.php b/be/Modules/User/Database/Migrations/2026_06_10_040351_create_banks_table.php new file mode 100644 index 0000000..85b4398 --- /dev/null +++ b/be/Modules/User/Database/Migrations/2026_06_10_040351_create_banks_table.php @@ -0,0 +1,31 @@ +uuid('id')->primary(); + $table->string('name'); + $table->string('code'); + $table->string('swift_code')->nullable(); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('banks'); + } +}; diff --git a/be/Modules/User/Database/Migrations/2026_06_10_040453_create_bank_details_table.php b/be/Modules/User/Database/Migrations/2026_06_10_040453_create_bank_details_table.php new file mode 100644 index 0000000..d9a5ae2 --- /dev/null +++ b/be/Modules/User/Database/Migrations/2026_06_10_040453_create_bank_details_table.php @@ -0,0 +1,32 @@ +uuid('id')->primary(); + $table->foreignUuid('user_id')->constrained('users')->onDelete('cascade'); + $table->foreignUuid('bank_id')->constrained('banks')->onDelete('cascade'); + $table->string('account_number'); + $table->string('account_name'); + $table->string('account_type')->comment('Saving, Current'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('bank_details'); + } +}; diff --git a/be/Modules/User/Database/Migrations/2026_06_10_072612_create_heirs_table.php b/be/Modules/User/Database/Migrations/2026_06_10_072612_create_heirs_table.php new file mode 100644 index 0000000..8761403 --- /dev/null +++ b/be/Modules/User/Database/Migrations/2026_06_10_072612_create_heirs_table.php @@ -0,0 +1,34 @@ +uuid('id')->primary(); + $table->foreignUuid('user_id')->constrained('users')->onDelete('cascade'); + $table->string('name'); + $table->string('ic_number'); + $table->string('relationship')->comment('Isteri, Anak, Orang Tua, Saudara, Lain-lain'); + $table->string('phone_number'); + $table->text('address')->nullable(); + $table->boolean('is_primary')->default(false)->comment('Primary heir'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('heirs'); + } +}; diff --git a/be/Modules/User/Entities/Address.php b/be/Modules/User/Entities/Address.php new file mode 100644 index 0000000..b102768 --- /dev/null +++ b/be/Modules/User/Entities/Address.php @@ -0,0 +1,63 @@ + */ + use HasUuids; + + protected $table = 'addresses'; + + /** + * The attributes that are mass assignable. + * + * @var list + */ + protected $fillable = [ + 'user_id', + 'address_type', + 'address_line_1', + 'address_line_2', + 'city', + 'state', + 'postcode', + 'country', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var list + */ + protected $hidden = [ + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'address_type' => 'string', + 'address_line_1' => 'string', + 'address_line_2' => 'string', + 'city' => 'string', + 'state' => 'string', + 'postcode' => 'string', + 'country' => 'string', + ]; + } + + public function user() + { + return $this->belongsTo(User::class); + } +} diff --git a/be/Modules/User/Entities/Bank.php b/be/Modules/User/Entities/Bank.php new file mode 100644 index 0000000..d13930e --- /dev/null +++ b/be/Modules/User/Entities/Bank.php @@ -0,0 +1,56 @@ + */ + use HasUuids; + + protected $table = 'banks'; + + /** + * The attributes that are mass assignable. + * + * @var list + */ + protected $fillable = [ + 'name', + 'code', + 'swift_code', + 'is_active', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var list + */ + protected $hidden = [ + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'name' => 'string', + 'code' => 'string', + 'swift_code' => 'string', + 'is_active' => 'boolean', + ]; + } + + public function bankDetails() + { + return $this->hasMany(BankDetail::class); + } +} diff --git a/be/Modules/User/Entities/BankDetail.php b/be/Modules/User/Entities/BankDetail.php new file mode 100644 index 0000000..8fa1c96 --- /dev/null +++ b/be/Modules/User/Entities/BankDetail.php @@ -0,0 +1,64 @@ + */ + use HasUuids; + + protected $table = 'bank_details'; + + /** + * The attributes that are mass assignable. + * + * @var list + */ + protected $fillable = [ + 'user_id', + 'bank_id', + 'account_name', + 'account_number', + 'account_type', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var list + */ + protected $hidden = [ + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'user_id' => 'string', + 'bank_id' => 'string', + 'account_name' => 'string', + 'account_number' => 'string', + 'account_type' => 'string', + ]; + } + + public function user() + { + return $this->belongsTo(User::class); + } + + public function bank() + { + return $this->belongsTo(Bank::class); + } +} diff --git a/be/Modules/User/Entities/Employment.php b/be/Modules/User/Entities/Employment.php new file mode 100644 index 0000000..330dc13 --- /dev/null +++ b/be/Modules/User/Entities/Employment.php @@ -0,0 +1,63 @@ + */ + use HasUuids; + + protected $table = 'employments'; + + /** + * The attributes that are mass assignable. + * + * @var list + */ + protected $fillable = [ + 'user_id', + 'company_name', + 'job_title', + 'employment_type', + 'salary', + 'start_date', + 'end_date', + 'is_current', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var list + */ + protected $hidden = [ + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'company_name' => 'string', + 'job_title' => 'string', + 'employment_type' => 'string', + 'salary' => 'decimal:2', + 'start_date' => 'date', + 'end_date' => 'date', + 'is_current' => 'boolean', + ]; + } + + public function user() + { + return $this->belongsTo(User::class); + } +} diff --git a/be/Modules/User/Entities/Heir.php b/be/Modules/User/Entities/Heir.php new file mode 100644 index 0000000..b60110a --- /dev/null +++ b/be/Modules/User/Entities/Heir.php @@ -0,0 +1,61 @@ + */ + use HasUuids; + + protected $table = 'heirs'; + + /** + * The attributes that are mass assignable. + * + * @var list + */ + protected $fillable = [ + 'user_id', + 'name', + 'ic_number', + 'relationship', + 'phone_number', + 'address', + 'is_primary', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var list + */ + protected $hidden = [ + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'name' => 'string', + 'ic_number' => 'string', + 'relationship' => 'string', + 'phone_number' => 'string', + 'address' => 'string', + 'is_primary' => 'boolean', + ]; + } + + public function user() + { + return $this->belongsTo(User::class); + } +} diff --git a/be/Modules/User/Http/Controllers/AddressController.php b/be/Modules/User/Http/Controllers/AddressController.php new file mode 100644 index 0000000..543dd02 --- /dev/null +++ b/be/Modules/User/Http/Controllers/AddressController.php @@ -0,0 +1,51 @@ + auth()->id(), + ]); + } + + protected function getItemName($item): string + { + return $item->address_type ?? $item->id; + } + + protected function prepareUpdateData(array $validated, $address): array + { + return $validated; + } + + protected function checkDependencies($address): ?JsonResponse + { + return null; + } +} \ No newline at end of file diff --git a/be/Modules/User/Http/Controllers/BankController.php b/be/Modules/User/Http/Controllers/BankController.php new file mode 100644 index 0000000..fb7ee02 --- /dev/null +++ b/be/Modules/User/Http/Controllers/BankController.php @@ -0,0 +1,59 @@ +name ?? $item->id; + } + + protected function prepareUpdateData(array $validated, $bank): array + { + return $validated; + } + + protected function checkDependencies($bank): ?JsonResponse + { + return null; + } + + public function active(): JsonResponse + { + $banks = $this->repository->active(); + + return response()->json([ + 'success' => true, + 'data' => $this->resourceClass::collection($banks), + ]); + } +} \ No newline at end of file diff --git a/be/Modules/User/Http/Controllers/BankDetailController.php b/be/Modules/User/Http/Controllers/BankDetailController.php new file mode 100644 index 0000000..edbf1e3 --- /dev/null +++ b/be/Modules/User/Http/Controllers/BankDetailController.php @@ -0,0 +1,51 @@ + auth()->id(), + ]); + } + + protected function getItemName($item): string + { + return $item->account_number ?? $item->id; + } + + protected function prepareUpdateData(array $validated, $bankDetail): array + { + return $validated; + } + + protected function checkDependencies($bankDetail): ?JsonResponse + { + return null; + } +} \ No newline at end of file diff --git a/be/Modules/User/Http/Controllers/EmploymentController.php b/be/Modules/User/Http/Controllers/EmploymentController.php new file mode 100644 index 0000000..87b5a41 --- /dev/null +++ b/be/Modules/User/Http/Controllers/EmploymentController.php @@ -0,0 +1,51 @@ + auth()->id(), + ]); + } + + protected function getItemName($item): string + { + return $item->company_name ?? $item->id; + } + + protected function prepareUpdateData(array $validated, $employment): array + { + return $validated; + } + + protected function checkDependencies($employment): ?JsonResponse + { + return null; + } +} \ No newline at end of file diff --git a/be/Modules/User/Http/Controllers/HeirController.php b/be/Modules/User/Http/Controllers/HeirController.php new file mode 100644 index 0000000..c830bed --- /dev/null +++ b/be/Modules/User/Http/Controllers/HeirController.php @@ -0,0 +1,51 @@ + auth()->id(), + ]); + } + + protected function getItemName($item): string + { + return $item->name ?? $item->id; + } + + protected function prepareUpdateData(array $validated, $heir): array + { + return $validated; + } + + protected function checkDependencies($heir): ?JsonResponse + { + return null; + } +} \ No newline at end of file diff --git a/be/Modules/User/Http/Controllers/UserController.php b/be/Modules/User/Http/Controllers/UserController.php index 78e9aa0..2526ff6 100644 --- a/be/Modules/User/Http/Controllers/UserController.php +++ b/be/Modules/User/Http/Controllers/UserController.php @@ -3,6 +3,7 @@ namespace Modules\User\Http\Controllers; use App\Http\Controllers\BaseCrudController; +use App\Services\ActivityLogger; use Exception; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -13,7 +14,7 @@ use Modules\Auth\Entities\User; use Modules\User\Http\Requests\UserRequest; use Modules\User\Repositories\Contracts\UserRepositoryInterface; use Modules\User\Services\UserService; -use Modules\User\Transformers\UserResource; +use Modules\Auth\Transformers\UserResource; use Modules\User\Transformers\UserListResource; class UserController extends BaseCrudController @@ -104,6 +105,13 @@ class UserController extends BaseCrudController 'phone_number' => 'sometimes|string|max:255', 'image' => 'sometimes|image|mimes:jpeg,png,jpg,gif|max:2048', 'image_url' => 'sometimes|string|max:255', + 'gender' => 'sometimes|string|max:255', + 'marriage_status' => 'sometimes|string|max:255', + 'member_number' => 'sometimes|integer', + 'member_type' => 'sometimes|string|max:255', + 'join_date' => 'sometimes|date', + 'birth_date' => 'sometimes|date', + 'birth_place' => 'sometimes|string|max:255', ]); $user = $this->userService->updateProfile( @@ -182,6 +190,69 @@ class UserController extends BaseCrudController } } + public function deletedIndex(Request $request): JsonResponse + { + $this->authorize('viewAny', $this->modelClass); + + try { + $perPage = min((int) $request->get('per_page', 10), 500); + $items = $this->userService->getDeletedPaginatedList( + $perPage, + $request->get('search', ''), + $request->get('status', ''), + $request->get('sort_by', 'deleted_at'), + $request->get('sort_order', 'desc') + ); + + return response()->json([ + 'success' => true, + 'data' => UserListResource::collection($items->items()), + 'pagination' => [ + 'current_page' => $items->currentPage(), + 'per_page' => $items->perPage(), + 'total' => $items->total(), + 'last_page' => $items->lastPage(), + 'from' => $items->firstItem(), + 'to' => $items->lastItem(), + 'has_more_pages' => $items->hasMorePages(), + ], + 'message' => 'Deleted users retrieved successfully.', + ]); + } catch (Exception $e) { + Log::error("Error fetching deleted {$this->resourceNamePlural}: ".$e->getMessage()); + + return $this->errorResponse('Failed to retrieve deleted users.', 500); + } + } + + public function restore(string $id): JsonResponse + { + $this->authorize('restore', $this->modelClass); + + try { + $user = $this->userService->restoreUser($id); + + if (! $user) { + return response()->json([ + 'success' => false, + 'message' => 'Deleted user not found.', + ], 404); + } + + ActivityLogger::log("Restored {$this->resourceName}: {$user->name}", $user); + + return response()->json([ + 'success' => true, + 'message' => 'User restored successfully.', + 'data' => (new UserListResource($user))->resolve(), + ]); + } catch (Exception $e) { + Log::error("Error restoring {$this->resourceName}: ".$e->getMessage()); + + return $this->errorResponse('Failed to restore user.', 500); + } + } + public function show($id): JsonResponse { $this->authorize('view', $this->modelClass); diff --git a/be/Modules/User/Http/Requests/AddressRequest.php b/be/Modules/User/Http/Requests/AddressRequest.php new file mode 100644 index 0000000..31a8d48 --- /dev/null +++ b/be/Modules/User/Http/Requests/AddressRequest.php @@ -0,0 +1,88 @@ +|string> + */ + public function rules(): array + { + // Get the address ID from the route (with null check) + $addressId = $this->route() ? $this->route('address') : null; + + // Check if this is a create or update operation + $isCreate = $this->isMethod('POST'); + $isUpdate = $this->isMethod('PUT') || $this->isMethod('PATCH'); + + // If this is not a POST, PUT, or PATCH request, return minimal rules + if (! $isCreate && ! $isUpdate) { + return [ + 'address_type' => 'nullable|string|max:255', + 'address_line_1' => 'nullable|string|max:255', + 'address_line_2' => 'nullable|string|max:255', + 'city' => 'nullable|string|max:255', + 'state' => 'nullable|string|max:255', + 'postcode' => 'nullable|string|max:255', + 'country' => 'nullable|string|max:255', + ]; + } + + // Build unique email validation rule + $addressUniqueRule = $addressId ? 'unique:addresses,address_type,'.$addressId : 'unique:addresses,address_type'; + + // If this is an update operation, make email and password optional + if ($isUpdate) { + return [ + 'address_type' => 'required|string|max:255', + 'address_line_1' => 'required|string|max:255', + 'address_line_2' => 'nullable|string|max:255', + 'city' => 'required|string|max:255', + 'state' => 'required|string|max:255', + 'postcode' => 'required|string|max:255', + 'country' => 'required|string|max:255', + ]; + } + + // Default rules for create operations + return [ + 'address_type' => 'required|string|max:255', + 'address_line_1' => 'required|string|max:255', + 'address_line_2' => 'nullable|string|max:255', + 'city' => 'required|string|max:255', + 'state' => 'required|string|max:255', + 'postcode' => 'required|string|max:255', + 'country' => 'required|string|max:255', + ]; + } + + /** + * Get custom messages for validator errors. + */ + public function messages(): array + { + return [ + 'address_type.required' => 'Jenis alamat diperlukan.', + 'address_type.max' => 'Jenis alamat tidak boleh melebihi 255 aksara.', + 'address_line_1.max' => 'Alamat 1 tidak boleh melebihi 255 aksara.', + 'address_line_2.max' => 'Alamat 2 tidak boleh melebihi 255 aksara.', + 'city.max' => 'Bandar tidak boleh melebihi 255 aksara.', + 'state.max' => 'Negeri tidak boleh melebihi 255 aksara.', + 'postcode.max' => 'Poskod tidak boleh melebihi 255 aksara.', + 'country.max' => 'Negara tidak boleh melebihi 255 aksara.', + ]; + } +} diff --git a/be/Modules/User/Http/Requests/BankDetailRequest.php b/be/Modules/User/Http/Requests/BankDetailRequest.php new file mode 100644 index 0000000..4fe2c80 --- /dev/null +++ b/be/Modules/User/Http/Requests/BankDetailRequest.php @@ -0,0 +1,67 @@ +|string> + */ + public function rules(): array + { + // Check if this is a create or update operation + $isCreate = $this->isMethod('POST'); + $isUpdate = $this->isMethod('PUT') || $this->isMethod('PATCH'); + + // If this is not a POST, PUT, or PATCH request, return minimal rules + if (! $isCreate && ! $isUpdate) { + return [ + 'bank_id' => 'nullable|string|max:255', + 'account_name' => 'nullable|string|max:255', + 'account_number' => 'nullable|string|max:255', + 'account_type' => 'nullable|string|max:255', + ]; + } + + // Default rules for create and update operations + return [ + 'bank_id' => 'required|string|max:255', + 'account_name' => 'required|string|max:255', + 'account_number' => 'required|string|max:255', + 'account_type' => 'required|string|max:255', + ]; + } + + /** + * Get custom messages for validator errors. + */ + public function messages(): array + { + return [ + 'bank_id.required' => 'ID bank diperlukan.', + 'bank_id.string' => 'ID bank tidak sah.', + 'bank_id.max' => 'ID bank tidak boleh melebihi 255 aksara.', + 'account_name.required' => 'Nama akaun diperlukan.', + 'account_name.string' => 'Nama akaun tidak sah.', + 'account_name.max' => 'Nama akaun tidak boleh melebihi 255 aksara.', + 'account_number.required' => 'Nombor akaun diperlukan.', + 'account_number.string' => 'Nombor akaun tidak sah.', + 'account_number.max' => 'Nombor akaun tidak boleh melebihi 255 aksara.', + 'account_type.required' => 'Jenis akaun diperlukan.', + 'account_type.string' => 'Jenis akaun tidak sah.', + 'account_type.max' => 'Jenis akaun tidak boleh melebihi 255 aksara.', + ]; + } +} diff --git a/be/Modules/User/Http/Requests/BankRequest.php b/be/Modules/User/Http/Requests/BankRequest.php new file mode 100644 index 0000000..242fa6d --- /dev/null +++ b/be/Modules/User/Http/Requests/BankRequest.php @@ -0,0 +1,72 @@ +|string> + */ + public function rules(): array + { + // Check if this is a create or update operation + $isCreate = $this->isMethod('POST'); + $isUpdate = $this->isMethod('PUT') || $this->isMethod('PATCH'); + + // If this is not a POST, PUT, or PATCH request, return minimal rules + if (! $isCreate && ! $isUpdate) { + return [ + 'name' => 'nullable|string|max:255', + 'code' => 'nullable|string|max:255', + 'swift_code' => 'nullable|string|max:255', + 'is_active' => 'nullable|boolean', + ]; + } + + // If this is an update operation, make email and password optional + if ($isUpdate) { + return [ + 'name' => 'required|string|max:255', + 'code' => 'required|string|max:255', + 'swift_code' => 'nullable|string|max:255', + 'is_active' => 'required|boolean', + ]; + } + + // Default rules for create operations + return [ + 'name' => 'required|string|max:255', + 'code' => 'required|string|max:255', + 'swift_code' => 'nullable|string|max:255', + 'is_active' => 'required|boolean', + ]; + } + + /** + * Get custom messages for validator errors. + */ + public function messages(): array + { + return [ + 'name.required' => 'Nama bank diperlukan.', + 'name.max' => 'Nama bank tidak boleh melebihi 255 aksara.', + 'code.required' => 'Kod bank diperlukan.', + 'code.max' => 'Kod bank tidak boleh melebihi 255 aksara.', + 'swift_code.max' => 'Swift code tidak boleh melebihi 255 aksara.', + 'is_active.required' => 'Status aktif diperlukan.', + 'is_active.boolean' => 'Status aktif tidak sah.', + ]; + } +} diff --git a/be/Modules/User/Http/Requests/EmploymentRequest.php b/be/Modules/User/Http/Requests/EmploymentRequest.php new file mode 100644 index 0000000..d02e12e --- /dev/null +++ b/be/Modules/User/Http/Requests/EmploymentRequest.php @@ -0,0 +1,88 @@ +|string> + */ + public function rules(): array + { + // Check if this is a create or update operation + $isCreate = $this->isMethod('POST'); + $isUpdate = $this->isMethod('PUT') || $this->isMethod('PATCH'); + + // If this is not a POST, PUT, or PATCH request, return minimal rules + if (! $isCreate && ! $isUpdate) { + return [ + 'company_name' => 'nullable|string|max:255', + 'job_title' => 'nullable|string|max:255', + 'employment_type' => 'nullable|string|max:255', + 'salary' => 'nullable|numeric|min:0', + 'start_date' => 'nullable|date', + 'end_date' => 'nullable|date', + 'is_current' => 'nullable|boolean', + ]; + } + + // If this is an update operation, make email and password optional + if ($isUpdate) { + return [ + 'company_name' => 'required|string|max:255', + 'job_title' => 'required|string|max:255', + 'employment_type' => 'required|string|max:255', + 'salary' => 'required|numeric|min:0', + 'start_date' => 'required|date', + 'end_date' => 'nullable|date', + 'is_current' => 'required|boolean', + ]; + } + + // Default rules for create operations + return [ + 'company_name' => 'required|string|max:255', + 'job_title' => 'required|string|max:255', + 'employment_type' => 'required|string|max:255', + 'salary' => 'required|numeric|min:0', + 'start_date' => 'required|date', + 'end_date' => 'nullable|date', + 'is_current' => 'required|boolean', + ]; + } + + /** + * Get custom messages for validator errors. + */ + public function messages(): array + { + return [ + 'company_name.required' => 'Nama syarikat diperlukan.', + 'company_name.max' => 'Nama syarikat tidak boleh melebihi 255 aksara.', + 'job_title.required' => 'Jawatan diperlukan.', + 'job_title.max' => 'Jawatan tidak boleh melebihi 255 aksara.', + 'employment_type.required' => 'Jenis kerja diperlukan.', + 'employment_type.max' => 'Jenis kerja tidak boleh melebihi 255 aksara.', + 'salary.required' => 'Gaji diperlukan.', + 'salary.numeric' => 'Gaji tidak sah.', + 'salary.min' => 'Gaji tidak boleh kurang dari 0.', + 'start_date.required' => 'Tarikh mulai kerja diperlukan.', + 'start_date.date' => 'Tarikh mulai kerja tidak sah.', + 'end_date.date' => 'Tarikh akhir kerja tidak sah.', + 'is_current.required' => 'Status kerja saat ini diperlukan.', + 'is_current.boolean' => 'Status kerja saat ini tidak sah.', + ]; + } +} diff --git a/be/Modules/User/Http/Requests/HeirRequest.php b/be/Modules/User/Http/Requests/HeirRequest.php new file mode 100644 index 0000000..bfc0362 --- /dev/null +++ b/be/Modules/User/Http/Requests/HeirRequest.php @@ -0,0 +1,83 @@ +|string> + */ + public function rules(): array + { + // Check if this is a create or update operation + $isCreate = $this->isMethod('POST'); + $isUpdate = $this->isMethod('PUT') || $this->isMethod('PATCH'); + + // If this is not a POST, PUT, or PATCH request, return minimal rules + if (! $isCreate && ! $isUpdate) { + return [ + 'name' => 'nullable|string|max:255', + 'ic_number' => 'nullable|string|max:255', + 'relationship' => 'nullable|string|max:255', + 'phone_number' => 'nullable|string|max:255', + 'address' => 'nullable|string', + 'is_primary' => 'nullable|boolean', + ]; + } + + // If this is an update operation, make email and password optional + if ($isUpdate) { + return [ + 'name' => 'required|string|max:255', + 'ic_number' => 'required|string|max:255', + 'relationship' => 'required|string|max:255', + 'phone_number' => 'required|string|max:255', + 'address' => 'required|string', + 'is_primary' => 'required|boolean', + ]; + } + + // Default rules for create operations + return [ + 'name' => 'required|string|max:255', + 'ic_number' => 'required|string|max:255', + 'relationship' => 'required|string|max:255', + 'phone_number' => 'required|string|max:255', + 'address' => 'required|string', + 'is_primary' => 'required|boolean', + ]; + } + + /** + * Get custom messages for validator errors. + */ + public function messages(): array + { + return [ + 'name.required' => 'Nama diperlukan.', + 'name.max' => 'Nama tidak boleh melebihi 255 aksara.', + 'ic_number.required' => 'Nombor Kad Pengenalan diperlukan.', + 'ic_number.max' => 'Nombor Kad Pengenalan tidak boleh melebihi 255 aksara.', + 'relationship.required' => 'Hubungan diperlukan.', + 'relationship.max' => 'Hubungan tidak boleh melebihi 255 aksara.', + 'phone_number.required' => 'Nombor telefon diperlukan.', + 'phone_number.max' => 'Nombor telefon tidak boleh melebihi 255 aksara.', + 'address.required' => 'Alamat diperlukan.', + 'address.string' => 'Alamat tidak sah.', + 'is_primary.required' => 'Status pewaris utama diperlukan.', + 'is_primary.boolean' => 'Status pewaris utama tidak sah.', + ]; + } +} diff --git a/be/Modules/User/Http/Requests/UserRequest.php b/be/Modules/User/Http/Requests/UserRequest.php index 939fdfd..1fb5207 100644 --- a/be/Modules/User/Http/Requests/UserRequest.php +++ b/be/Modules/User/Http/Requests/UserRequest.php @@ -39,6 +39,13 @@ class UserRequest extends FormRequest 'phone_number' => 'nullable|string|max:255', 'image_url' => 'nullable|string|max:255', 'status' => 'nullable|string', + 'gender' => 'nullable|string|max:255', + 'marriage_status' => 'nullable|string|max:255', + 'member_number' => 'required|integer', + 'member_type' => 'required|string|max:255', + 'join_date' => 'nullable|date', + 'birth_date' => 'nullable|date', + 'birth_place' => 'nullable|string|max:255', ]; } @@ -58,10 +65,17 @@ class UserRequest extends FormRequest // ], 'password' => 'nullable|string|min:8', 'ic_number' => 'required|string|max:255', - 'position' => 'required|string|max:255', + 'position' => 'nullable|string|max:255', 'phone_number' => 'nullable|string|max:255', 'image_url' => 'nullable|string|max:255', 'status' => 'nullable|string', + 'gender' => 'nullable|string|max:255', + 'marriage_status' => 'nullable|string|max:255', + 'member_number' => 'required|integer', + 'member_type' => 'required|string|max:255', + 'join_date' => 'nullable|date', + 'birth_date' => 'nullable|date', + 'birth_place' => 'nullable|string|max:255', ]; } @@ -81,6 +95,13 @@ class UserRequest extends FormRequest 'phone_number' => 'nullable|string|max:255', 'image_url' => 'nullable|string|max:255', 'status' => 'required|string', + 'gender' => 'required|string|max:255', + 'marriage_status' => 'required|string|max:255', + 'member_number' => 'required|integer', + 'member_type' => 'required|string|max:255', + 'join_date' => 'required|date', + 'birth_date' => 'required|date', + 'birth_place' => 'required|string|max:255', ]; } @@ -105,6 +126,20 @@ class UserRequest extends FormRequest 'image_url.max' => 'URL imej tidak boleh melebihi 255 aksara.', 'status.required' => 'Status diperlukan.', 'status.string' => 'Status aktif tidak sah.', + 'gender.required' => 'Jenis kelamin diperlukan.', + 'gender.max' => 'Jenis kelamin tidak boleh melebihi 255 aksara.', + 'marriage_status.required' => 'Status perkahwinan diperlukan.', + 'marriage_status.max' => 'Status perkahwinan tidak boleh melebihi 255 aksara.', + 'member_number.required' => 'Nombor anggota diperlukan.', + 'member_number.integer' => 'Nombor anggota tidak sah.', + 'member_type.required' => 'Jenis anggota diperlukan.', + 'member_type.max' => 'Jenis anggota tidak boleh melebihi 255 aksara.', + 'join_date.required' => 'Tarikh join diperlukan.', + 'join_date.date' => 'Tarikh join tidak sah.', + 'birth_date.required' => 'Tarikh lahir diperlukan.', + 'birth_date.date' => 'Tarikh lahir tidak sah.', + 'birth_place.required' => 'Tempat lahir diperlukan.', + 'birth_place.max' => 'Tempat lahir tidak boleh melebihi 255 aksara.', ]; } } diff --git a/be/Modules/User/Notifications/UserActivationNotification.php b/be/Modules/User/Notifications/UserActivationNotification.php index 62cee5d..e18e998 100644 --- a/be/Modules/User/Notifications/UserActivationNotification.php +++ b/be/Modules/User/Notifications/UserActivationNotification.php @@ -42,8 +42,6 @@ class UserActivationNotification extends Notification 'message' => $this->getNotificationMessage(), 'user_name' => $this->newUser->name ?? 'Unknown', 'user_email' => $this->newUser->email ?? 'Unknown', - 'user_army_number' => $this->newUser->army_number ?? 'Unknown', - 'user_unit_name' => $this->newUser->unit->name ?? 'Unknown', 'user_status' => $this->newUser->status ?? 'pending', ]; } @@ -55,10 +53,8 @@ class UserActivationNotification extends Notification { $userName = $this->newUser->name ?? 'Unknown'; $userEmail = $this->newUser->email ?? 'Unknown'; - $userArmyNumber = $this->newUser->army_number ?? 'Unknown'; - $unitName = $this->newUser->unit->name ?? 'Unknown'; - return "Pengguna baru memerlukan pengaktifan. Nama: {$userName}, Email: {$userEmail}, No. Tentera: {$userArmyNumber}, Unit: {$unitName}"; + return "Pengguna baru memerlukan pengaktifan. Nama: {$userName}, Email: {$userEmail}"; } } diff --git a/be/Modules/User/Policies/AddressPolicy.php b/be/Modules/User/Policies/AddressPolicy.php new file mode 100644 index 0000000..b33041d --- /dev/null +++ b/be/Modules/User/Policies/AddressPolicy.php @@ -0,0 +1,41 @@ +user_id === $user->id; + } + + public function create($user): bool + { + return true; + } + + public function update($user, ?Address $address = null): bool + { + return $address === null || $address->user_id === $user->id; + } + + public function deleteAny($user): bool + { + return true; + } + + public function delete($user, ?Address $address = null): bool + { + return $address === null || $address->user_id === $user->id; + } +} diff --git a/be/Modules/User/Policies/BankDetailPolicy.php b/be/Modules/User/Policies/BankDetailPolicy.php new file mode 100644 index 0000000..c52f6fa --- /dev/null +++ b/be/Modules/User/Policies/BankDetailPolicy.php @@ -0,0 +1,41 @@ +user_id === $user->id; + } + + public function create($user): bool + { + return true; + } + + public function update($user, ?BankDetail $bankDetail = null): bool + { + return $bankDetail === null || $bankDetail->user_id === $user->id; + } + + public function deleteAny($user): bool + { + return true; + } + + public function delete($user, ?BankDetail $bankDetail = null): bool + { + return $bankDetail === null || $bankDetail->user_id === $user->id; + } +} diff --git a/be/Modules/User/Policies/BankPolicy.php b/be/Modules/User/Policies/BankPolicy.php new file mode 100644 index 0000000..e0cc392 --- /dev/null +++ b/be/Modules/User/Policies/BankPolicy.php @@ -0,0 +1,41 @@ +hasPermissionTo('tambah bank'); + } + + public function update($user, ?Bank $bank = null): bool + { + return $user->hasPermissionTo('kemaskini bank'); + } + + public function deleteAny($user): bool + { + return $user->hasPermissionTo('hapus bank'); + } + + public function delete($user, ?Bank $bank = null): bool + { + return $user->hasPermissionTo('hapus bank'); + } +} diff --git a/be/Modules/User/Policies/EmploymentPolicy.php b/be/Modules/User/Policies/EmploymentPolicy.php new file mode 100644 index 0000000..1d03c84 --- /dev/null +++ b/be/Modules/User/Policies/EmploymentPolicy.php @@ -0,0 +1,41 @@ +user_id === $user->id; + } + + public function create($user): bool + { + return true; + } + + public function update($user, ?Employment $employement = null): bool + { + return $employement === null || $employement->user_id === $user->id; + } + + public function deleteAny($user): bool + { + return true; + } + + public function delete($user, ?Employment $employement = null): bool + { + return $employement === null || $employement->user_id === $user->id; + } +} diff --git a/be/Modules/User/Policies/HeirPolicy.php b/be/Modules/User/Policies/HeirPolicy.php new file mode 100644 index 0000000..ebb5680 --- /dev/null +++ b/be/Modules/User/Policies/HeirPolicy.php @@ -0,0 +1,41 @@ +user_id === $user->id; + } + + public function create($user): bool + { + return true; + } + + public function update($user, ?Heir $heir = null): bool + { + return $heir === null || $heir->user_id === $user->id; + } + + public function deleteAny($user): bool + { + return true; + } + + public function delete($user, ?Heir $heir = null): bool + { + return $heir === null || $heir->user_id === $user->id; + } +} diff --git a/be/Modules/User/Policies/UserPolicy.php b/be/Modules/User/Policies/UserPolicy.php index 559a272..22d6212 100644 --- a/be/Modules/User/Policies/UserPolicy.php +++ b/be/Modules/User/Policies/UserPolicy.php @@ -30,7 +30,7 @@ class UserPolicy */ public function create($user): bool { - return $user->hasPermissionTo('tambah pengguna'); + return $user->hasPermissionTo('daftar pengguna baru'); } /** @@ -46,7 +46,7 @@ class UserPolicy */ public function deleteAny($user): bool { - return $user->hasPermissionTo('hapus pengguna'); + return $user->hasPermissionTo('padam akaun pengguna'); } /** @@ -54,6 +54,14 @@ class UserPolicy */ public function delete($user, ?User $userModel = null): bool { - return $user->hasPermissionTo('hapus pengguna'); + return $user->hasPermissionTo('padam akaun pengguna'); + } + + /** + * Determine whether the user can restore a soft-deleted model. + */ + public function restore($user, ?User $userModel = null): bool + { + return $user->hasPermissionTo('padam akaun pengguna'); } } diff --git a/be/Modules/User/Providers/UserServiceProvider.php b/be/Modules/User/Providers/UserServiceProvider.php index e135007..9c28f9e 100644 --- a/be/Modules/User/Providers/UserServiceProvider.php +++ b/be/Modules/User/Providers/UserServiceProvider.php @@ -7,6 +7,16 @@ use Illuminate\Support\Facades\Gate; use Illuminate\Support\ServiceProvider; use Modules\Auth\Entities\User; use Modules\User\Policies\UserPolicy; +use Modules\User\Entities\Address; +use Modules\User\Policies\AddressPolicy; +use Modules\User\Entities\Employment; +use Modules\User\Policies\EmploymentPolicy; +use Modules\User\Entities\Bank; +use Modules\User\Policies\BankPolicy; +use Modules\User\Entities\BankDetail; +use Modules\User\Policies\BankDetailPolicy; +use Modules\User\Entities\Heir; +use Modules\User\Policies\HeirPolicy; use Nwidart\Modules\Traits\PathNamespace; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; @@ -41,11 +51,36 @@ class UserServiceProvider extends ServiceProvider $this->app->register(EventServiceProvider::class); $this->app->register(RouteServiceProvider::class); - // Register repository binding + // Register repository bindings $this->app->bind( \Modules\User\Repositories\Contracts\UserRepositoryInterface::class, \Modules\User\Repositories\UserRepository::class ); + + $this->app->bind( + \Modules\User\Repositories\Contracts\AddressRepositoryInterface::class, + \Modules\User\Repositories\AddressRepository::class + ); + + $this->app->bind( + \Modules\User\Repositories\Contracts\EmploymentRepositoryInterface::class, + \Modules\User\Repositories\EmploymentRepository::class + ); + + $this->app->bind( + \Modules\User\Repositories\Contracts\BankRepositoryInterface::class, + \Modules\User\Repositories\BankRepository::class + ); + + $this->app->bind( + \Modules\User\Repositories\Contracts\BankDetailRepositoryInterface::class, + \Modules\User\Repositories\BankDetailRepository::class + ); + + $this->app->bind( + \Modules\User\Repositories\Contracts\HeirRepositoryInterface::class, + \Modules\User\Repositories\HeirRepository::class + ); } /** @@ -54,6 +89,11 @@ class UserServiceProvider extends ServiceProvider protected function registerPolicies(): void { Gate::policy(User::class, UserPolicy::class); + Gate::policy(Address::class, AddressPolicy::class); + Gate::policy(Employment::class, EmploymentPolicy::class); + Gate::policy(Bank::class, BankPolicy::class); + Gate::policy(BankDetail::class, BankDetailPolicy::class); + Gate::policy(Heir::class, HeirPolicy::class); } /** diff --git a/be/Modules/User/Repositories/AddressRepository.php b/be/Modules/User/Repositories/AddressRepository.php new file mode 100644 index 0000000..ed197d0 --- /dev/null +++ b/be/Modules/User/Repositories/AddressRepository.php @@ -0,0 +1,89 @@ +where('user_id', auth()->id()) + ->orderBy('address_type'); + } + + public function getAllPaginated(int $perPage = 10, string $search = '') + { + $query = $this->baseQuery(); + + return $query->paginate($perPage); + } + + public function getAllWithRelationsPaginated( + int $perPage = 10, + string $search = '', + string $sortBy = 'address_type', + string $sortOrder = 'asc' + ) { + $allowedSortColumns = [ + 'id', + 'address_type', + 'address_line_1', + 'city', + 'state', + 'postcode', + 'country', + 'created_at', + ]; + $sortBy = in_array($sortBy, $allowedSortColumns, true) ? $sortBy : 'address_type'; + $sortOrder = strtolower($sortOrder) === 'desc' ? 'desc' : 'asc'; + + $query = Address::query() + ->where('user_id', auth()->id()) + ->with(['user:id,name,email']) + ->orderBy($sortBy, $sortOrder); + + return $query->paginate($perPage); + } + + public function getAllWithRelations(string $search = ''): Collection + { + $query = Address::query() + ->where('user_id', auth()->id()) + ->with(['user:id,name,email']) + ->orderBy('address_type'); + + return $query->get(); + } + + public function create(array $data): Address + { + return Address::create($data); + } + + public function findById(string $id): ?Address + { + return Address::query() + ->where('user_id', auth()->id()) + ->with(['user:id,name,email']) + ->find($id); + } + + public function delete(string $id): bool + { + $address = $this->findById($id); + if ($address) { + return $address->delete(); + } + + return false; + } + + public function all(string $search = ''): Collection + { + return $this->baseQuery()->get(); + } +} diff --git a/be/Modules/User/Repositories/BankDetailRepository.php b/be/Modules/User/Repositories/BankDetailRepository.php new file mode 100644 index 0000000..47a9fb6 --- /dev/null +++ b/be/Modules/User/Repositories/BankDetailRepository.php @@ -0,0 +1,59 @@ +where('user_id', auth()->id()); + } + + public function getAllPaginated(int $perPage = 10, string $search = '') + { + $query = $this->baseQuery(); + + return $query->paginate($perPage); + } + + public function getAllWithRelations(string $search = ''): Collection + { + $query = BankDetail::query() + ->where('user_id', auth()->id()) + ->with(['user:id,name,email', 'bank:id,name']) + ->orderBy('account_number'); + + return $query->get(); + } + + public function create(array $data): BankDetail + { + return BankDetail::create($data); + } + + public function findById(string $id): ?BankDetail + { + return BankDetail::query() + ->where('user_id', auth()->id()) + ->find($id); + } + + public function delete(string $id): bool + { + $bankDetail = $this->findById($id); + if ($bankDetail) { + return $bankDetail->delete(); + } + + return false; + } + + public function all(string $search = ''): Collection + { + return $this->baseQuery()->get(); + } +} diff --git a/be/Modules/User/Repositories/BankRepository.php b/be/Modules/User/Repositories/BankRepository.php new file mode 100644 index 0000000..a762a6e --- /dev/null +++ b/be/Modules/User/Repositories/BankRepository.php @@ -0,0 +1,81 @@ +orderBy('name'); + } + + public function getAllPaginated(int $perPage = 10, string $search = '') + { + $query = $this->baseQuery(); + + return $query->paginate($perPage); + } + + public function getAllWithRelationsPaginated( + int $perPage = 10, + string $search = '', + string $sortBy = 'name', + string $sortOrder = 'asc' + ) { + $allowedSortColumns = [ + 'name', + 'code', + 'swift_code', + 'is_active', + 'created_at', + ]; + $sortBy = in_array($sortBy, $allowedSortColumns, true) ? $sortBy : 'name'; + $sortOrder = strtolower($sortOrder) === 'desc' ? 'desc' : 'asc'; + + $query = Bank::query()->orderBy($sortBy, $sortOrder); + + return $query->paginate($perPage); + } + + public function getAllWithRelations(string $search = ''): Collection + { + $query = Bank::query()->orderBy('name'); + + return $query->get(); + } + + public function create(array $data): Bank + { + return Bank::create($data); + } + + public function findById(string $id): ?Bank + { + return Bank::query() + ->find($id); + } + + // public function delete(string $id): bool + // { + // $bank = $this->findById($id); + // if ($bank) { + // return $bank->delete(); + // } + + // return false; + // } + + public function all(string $search = ''): Collection + { + return $this->baseQuery()->get(); + } + + public function active(): Collection + { + return Bank::query()->where('is_active', true)->get(); + } +} diff --git a/be/Modules/User/Repositories/Contracts/AddressRepositoryInterface.php b/be/Modules/User/Repositories/Contracts/AddressRepositoryInterface.php new file mode 100644 index 0000000..ebad98c --- /dev/null +++ b/be/Modules/User/Repositories/Contracts/AddressRepositoryInterface.php @@ -0,0 +1,28 @@ +where('user_id', auth()->id()) + ->orderBy('company_name'); + } + + public function getAllPaginated(int $perPage = 10, string $search = '') + { + $query = $this->baseQuery(); + + return $query->paginate($perPage); + } + + public function getAllWithRelationsPaginated( + int $perPage = 10, + string $search = '', + string $sortBy = 'company_name', + string $sortOrder = 'asc' + ) { + $allowedSortColumns = [ + 'id', + 'company_name', + 'job_title', + 'employment_type', + 'salary', + 'start_date', + 'end_date', + 'is_current', + 'created_at', + ]; + $sortBy = in_array($sortBy, $allowedSortColumns, true) ? $sortBy : 'company_name'; + $sortOrder = strtolower($sortOrder) === 'desc' ? 'desc' : 'asc'; + + $query = Employment::query() + ->where('user_id', auth()->id()) + ->with(['user:id,name,email']) + ->orderBy($sortBy, $sortOrder); + + return $query->paginate($perPage); + } + + public function getAllWithRelations(string $search = ''): Collection + { + $query = Employment::query() + ->where('user_id', auth()->id()) + ->with(['user:id,name,email']) + ->orderBy('company_name'); + + return $query->get(); + } + + public function create(array $data): Employment + { + return Employment::create($data); + } + + public function findById(string $id): ?Employment + { + return Employment::query() + ->where('user_id', auth()->id()) + ->with(['user:id,name,email']) + ->find($id); + } + + public function delete(string $id): bool + { + $employement = $this->findById($id); + if ($employement) { + return $employement->delete(); + } + + return false; + } + + public function all(string $search = ''): Collection + { + return $this->baseQuery()->get(); + } +} diff --git a/be/Modules/User/Repositories/HeirRepository.php b/be/Modules/User/Repositories/HeirRepository.php new file mode 100644 index 0000000..7d70282 --- /dev/null +++ b/be/Modules/User/Repositories/HeirRepository.php @@ -0,0 +1,89 @@ +where('user_id', auth()->id()) + ->orderBy('name'); + } + + public function getAllPaginated(int $perPage = 10, string $search = '') + { + $query = $this->baseQuery(); + + return $query->paginate($perPage); + } + + // public function getAllWithRelationsPaginated( + // int $perPage = 10, + // string $search = '', + // string $sortBy = 'name', + // string $sortOrder = 'asc' + // ) { + // $allowedSortColumns = [ + // 'id', + // 'name', + // 'job_title', + // 'relationship', + // 'phone_number', + // 'address', + // 'is_primary', + // 'created_at', + // ]; + // $sortBy = in_array($sortBy, $allowedSortColumns, true) ? $sortBy : 'company_name'; + // $sortOrder = strtolower($sortOrder) === 'desc' ? 'desc' : 'asc'; + + // $query = Employment::query() + // ->where('user_id', auth()->id()) + // ->with(['user:id,name,email']) + // ->orderBy($sortBy, $sortOrder); + + // return $query->paginate($perPage); + // } + + public function getAllWithRelations(string $search = ''): Collection + { + $query = Heir::query() + ->where('user_id', auth()->id()) + ->with(['user:id,name,email']) + ->orderBy('name'); + + return $query->get(); + } + + public function create(array $data): Heir + { + return Heir::create($data); + } + + public function findById(string $id): ?Heir + { + return Heir::query() + ->where('user_id', auth()->id()) + ->with(['user:id,name,email']) + ->find($id); + } + + public function delete(string $id): bool + { + $heir = $this->findById($id); + if ($heir) { + return $heir->delete(); + } + + return false; + } + + public function all(string $search = ''): Collection + { + return $this->baseQuery()->get(); + } +} diff --git a/be/Modules/User/Repositories/UserRepository.php b/be/Modules/User/Repositories/UserRepository.php index 5010a32..33ce614 100644 --- a/be/Modules/User/Repositories/UserRepository.php +++ b/be/Modules/User/Repositories/UserRepository.php @@ -8,24 +8,32 @@ use Modules\User\Repositories\Contracts\UserRepositoryInterface; class UserRepository implements UserRepositoryInterface { + private function applySearch($query, string $search): void + { + if (empty($search)) { + return; + } + + $query->where(function ($q) use ($search) { + $q->where('name', 'ILIKE', "%{$search}%") + ->orWhere('ic_number', 'ILIKE', "%{$search}%") + ->orWhere('email', 'ILIKE', "%{$search}%") + ->orWhere('phone_number', 'ILIKE', "%{$search}%") + ->orWhere('member_number', 'ILIKE', "%{$search}%") + ->orWhereHas('roles', function ($roleQuery) use ($search) { + $roleQuery->whereRaw('LOWER(name) ILIKE ?', ['%' . strtolower($search) . '%']); + }); + }); + } + /** * Get all Users with pagination and search */ public function getAllPaginated(int $perPage = 10, string $search = '') { - $query = User::visibleTo(auth()->user())->excludeDevelopersUnlessDeveloper()->orderBy('name'); + $query = User::excludeDevelopersUnlessDeveloper()->orderBy('name'); - if (! empty($search)) { - $query->where(function ($q) use ($search) { - $q->where('name', 'ILIKE', "%{$search}%"); - $q->orWhere('ic_number', 'ILIKE', "%{$search}%"); - $q->orWhere('email', 'ILIKE', "%{$search}%"); - $q->orWhere('phone_number', 'ILIKE', "%{$search}%"); - $q->orWhereHas('roles', function ($roleQuery) use ($search) { - $roleQuery->whereRaw('LOWER(name) ILIKE ?', ['%' . strtolower($search) . '%']); - }); - }); - } + $this->applySearch($query, $search); return $query->paginate($perPage); } @@ -46,38 +54,60 @@ class UserRepository implements UserRepositoryInterface 'email', 'position', 'status', - 'ic_number', - 'phone_number', + 'member_number', 'created_at', + 'deleted_at', ]; $sortBy = in_array($sortBy, $allowedSortColumns, true) ? $sortBy : 'name'; $sortOrder = strtolower($sortOrder) === 'desc' ? 'desc' : 'asc'; - $query = User::visibleTo(auth()->user())->excludeDevelopersUnlessDeveloper() + $query = User::excludeDevelopersUnlessDeveloper() ->with([ 'roles:id,name,guard_name' ]) ->orderBy($sortBy, $sortOrder); - if (! empty($search)) { - $query->where(function ($q) use ($search) { - $q->where('name', 'ILIKE', "%{$search}%") - ->orWhere('ic_number', 'ILIKE', "%{$search}%") - ->orWhere('email', 'ILIKE', "%{$search}%") - ->orWhere('phone_number', 'ILIKE', "%{$search}%") - ->orWhereHas('roles', function ($roleQuery) use ($search) { - $roleQuery->whereRaw('LOWER(name) ILIKE ?', ['%' . strtolower($search) . '%']); - }); - $q->orWhere('ic_number', 'ILIKE', "%{$search}%"); - $q->orWhere('email', 'ILIKE', "%{$search}%"); - $q->orWhere('phone_number', 'ILIKE', "%{$search}%"); - $q->orWhereHas('roles', function ($roleQuery) use ($search) { - $roleQuery->whereRaw('LOWER(name) ILIKE ?', ['%' . strtolower($search) . '%']); - }); - }); + $this->applySearch($query, $search); + + if (! empty($status)) { + $query->where('status', $status); } - // Add status filter + return $query->paginate($perPage); + } + + /** + * Get soft-deleted Users with their relationships and pagination + */ + public function getDeletedWithRelationsPaginated( + int $perPage = 10, + string $search = '', + string $status = '', + string $sortBy = 'deleted_at', + string $sortOrder = 'desc' + ) { + $allowedSortColumns = [ + 'id', + 'name', + 'email', + 'position', + 'status', + 'member_number', + 'created_at', + 'deleted_at', + ]; + $sortBy = in_array($sortBy, $allowedSortColumns, true) ? $sortBy : 'deleted_at'; + $sortOrder = strtolower($sortOrder) === 'desc' ? 'desc' : 'asc'; + + $query = User::onlyTrashed() + ->excludeDevelopersUnlessDeveloper() + ->with([ + 'roles:id,name,guard_name', + ]) + ->orderBy($sortBy, $sortOrder); + + $this->applySearch($query, $search); + if (! empty($status)) { $query->where('status', $status); } @@ -90,19 +120,9 @@ class UserRepository implements UserRepositoryInterface */ public function getAllWithRelations(string $search = ''): Collection { - $query = User::visibleTo(auth()->user())->excludeDevelopersUnlessDeveloper()->orderBy('name'); + $query = User::excludeDevelopersUnlessDeveloper()->orderBy('name'); - if (! empty($search)) { - $query->where(function ($q) use ($search) { - $q->where('name', 'ILIKE', "%{$search}%") - ->orWhere('ic_number', 'ILIKE', "%{$search}%") - ->orWhere('email', 'ILIKE', "%{$search}%") - ->orWhere('phone_number', 'ILIKE', "%{$search}%") - ->orWhereHas('roles', function ($roleQuery) use ($search) { - $roleQuery->whereRaw('LOWER(name) ILIKE ?', ['%' . strtolower($search) . '%']); - }); - }); - } + $this->applySearch($query, $search); return $query->get(); } @@ -136,24 +156,36 @@ class UserRepository implements UserRepositoryInterface return false; } + /** + * Find soft-deleted User by ID + */ + public function findTrashedById(string $id): ?User + { + return User::onlyTrashed()->with(['roles'])->find($id); + } + + /** + * Restore a soft-deleted User + */ + public function restore(string $id): bool + { + $user = User::onlyTrashed()->find($id); + + if ($user) { + return (bool) $user->restore(); + } + + return false; + } + /** * Get all Ranks */ public function all(string $search = ''): Collection { - $query = User::visibleTo(auth()->user())->excludeDevelopersUnlessDeveloper()->orderBy('name'); + $query = User::excludeDevelopersUnlessDeveloper()->orderBy('name'); - if (! empty($search)) { - $query->where(function ($q) use ($search) { - $q->where('name', 'ILIKE', "%{$search}%") - ->orWhere('ic_number', 'ILIKE', "%{$search}%") - ->orWhere('email', 'ILIKE', "%{$search}%") - ->orWhere('phone_number', 'ILIKE', "%{$search}%") - ->orWhereHas('roles', function ($roleQuery) use ($search) { - $roleQuery->whereRaw('LOWER(name) ILIKE ?', ['%' . strtolower($search) . '%']); - }); - }); - } + $this->applySearch($query, $search); return $query->get(); } diff --git a/be/Modules/User/Routes/api.php b/be/Modules/User/Routes/api.php index d09e4f1..28c0793 100644 --- a/be/Modules/User/Routes/api.php +++ b/be/Modules/User/Routes/api.php @@ -3,10 +3,25 @@ use Illuminate\Support\Facades\Route; use Modules\User\Http\Controllers\UserController; use App\Http\Controllers\ImpersonateController; +use Modules\User\Http\Controllers\AddressController; +use Modules\User\Http\Controllers\EmploymentController; +use Modules\User\Http\Controllers\BankController; +use Modules\User\Http\Controllers\BankDetailController; +use Modules\User\Http\Controllers\HeirController; Route::middleware(['auth:sanctum', 'single.session'])->prefix('v1')->group(function () { + Route::get('users/deleted', [UserController::class, 'deletedIndex'])->name('users.deleted.index'); + Route::post('users/{id}/restore', [UserController::class, 'restore'])->name('users.restore'); Route::apiResource('users', UserController::class)->names('user'); - + Route::apiResource('addresses', AddressController::class)->names('address'); + // employement + Route::apiResource('employments', EmploymentController::class)->names('employment'); + // bank and bank detail + Route::get('banks/active', [BankController::class, 'active'])->name('bank.active'); + Route::apiResource('banks', BankController::class)->names('bank'); + Route::apiResource('bank_details', BankDetailController::class)->names('bank_detail'); + // heir + Route::apiResource('heirs', HeirController::class)->names('heir'); // User role management routes Route::post('users/{user}/roles', [UserController::class, 'assignRoles'])->name('users.roles.assign'); diff --git a/be/Modules/User/Services/UserService.php b/be/Modules/User/Services/UserService.php index 0a9d9bf..90f20fb 100644 --- a/be/Modules/User/Services/UserService.php +++ b/be/Modules/User/Services/UserService.php @@ -32,12 +32,43 @@ class UserService ); } + public function getDeletedPaginatedList( + int $perPage, + string $search, + string $status, + string $sortBy, + string $sortOrder + ): LengthAwarePaginator { + return $this->repository->getDeletedWithRelationsPaginated( + $perPage, + $search, + $status, + $sortBy, + $sortOrder + ); + } + + public function restoreUser(string $id): ?User + { + if (! $this->repository->restore($id)) { + return null; + } + + return $this->repository->findById($id); + } + public function getUserWithRelations(string $id): ?User { $user = $this->repository->findById($id); if ($user) { - $user->load(['roles.permissions']); + $user->load([ + 'roles.permissions', + 'addresses', + 'employments', + 'bankDetails.bank', + 'heirs', + ]); } return $user; diff --git a/be/Modules/User/Transformers/AddressResource.php b/be/Modules/User/Transformers/AddressResource.php new file mode 100644 index 0000000..ffbf6ae --- /dev/null +++ b/be/Modules/User/Transformers/AddressResource.php @@ -0,0 +1,26 @@ + $this->id, + 'user_id' => $this->user_id, + 'address_type' => $this->address_type, + 'address_line_1' => $this->address_line_1, + 'address_line_2' => $this->address_line_2, + 'city' => $this->city, + 'state' => $this->state, + 'postcode' => $this->postcode, + 'country' => $this->country, + 'created_at' => $this->created_at?->toISOString(), + 'updated_at' => $this->updated_at?->toISOString(), + ]; + } +} diff --git a/be/Modules/User/Transformers/BankDetailResource.php b/be/Modules/User/Transformers/BankDetailResource.php new file mode 100644 index 0000000..9acd024 --- /dev/null +++ b/be/Modules/User/Transformers/BankDetailResource.php @@ -0,0 +1,25 @@ + $this->id, + 'user_id' => $this->user_id, + 'bank_id' => $this->bank_id, + 'account_number' => $this->account_number, + 'account_name' => $this->account_name, + 'account_type' => $this->account_type, + 'bank' => new BankResource($this->whenLoaded('bank')), + 'created_at' => $this->created_at?->toISOString(), + 'updated_at' => $this->updated_at?->toISOString(), + ]; + } +} diff --git a/be/Modules/User/Transformers/BankResource.php b/be/Modules/User/Transformers/BankResource.php new file mode 100644 index 0000000..065dc31 --- /dev/null +++ b/be/Modules/User/Transformers/BankResource.php @@ -0,0 +1,22 @@ + $this->id, + 'name' => $this->name, + 'code' => $this->code, + 'swift_code' => $this->swift_code, + 'is_active' => $this->is_active, + 'created_at' => $this->created_at?->toISOString(), + 'updated_at' => $this->updated_at?->toISOString(), + ]; + } +} diff --git a/be/Modules/User/Transformers/EmploymentResource.php b/be/Modules/User/Transformers/EmploymentResource.php new file mode 100644 index 0000000..f734dbc --- /dev/null +++ b/be/Modules/User/Transformers/EmploymentResource.php @@ -0,0 +1,26 @@ + $this->id, + 'user_id' => $this->user_id, + 'company_name' => $this->company_name, + 'job_title' => $this->job_title, + 'employment_type' => $this->employment_type, + 'salary' => $this->salary, + 'start_date' => $this->start_date, + 'end_date' => $this->end_date, + 'is_current' => $this->is_current, + 'created_at' => $this->created_at?->toISOString(), + 'updated_at' => $this->updated_at?->toISOString(), + ]; + } +} diff --git a/be/Modules/User/Transformers/HeirResource.php b/be/Modules/User/Transformers/HeirResource.php new file mode 100644 index 0000000..f32c38a --- /dev/null +++ b/be/Modules/User/Transformers/HeirResource.php @@ -0,0 +1,25 @@ + $this->id, + 'user_id' => $this->user_id, + 'name' => $this->name, + 'ic_number' => $this->ic_number, + 'relationship' => $this->relationship, + 'phone_number' => $this->phone_number, + 'address' => $this->address, + 'is_primary' => $this->is_primary, + 'created_at' => $this->created_at?->toISOString(), + 'updated_at' => $this->updated_at?->toISOString(), + ]; + } +} diff --git a/be/Modules/User/Transformers/UserListResource.php b/be/Modules/User/Transformers/UserListResource.php index 5426fee..136aef2 100644 --- a/be/Modules/User/Transformers/UserListResource.php +++ b/be/Modules/User/Transformers/UserListResource.php @@ -20,6 +20,13 @@ class UserListResource extends JsonResource 'image_url' => $this->image_url ? Storage::disk('public')->url($this->image_url) : null, 'status' => $this->status, 'email_verified_at' => $this->email_verified_at, + 'gender' => $this->gender, + 'marriage_status' => $this->marriage_status, + 'member_number' => $this->member_number, + 'member_type' => $this->member_type, + 'join_date' => $this->join_date, + 'birth_date' => $this->birth_date, + 'birth_place' => $this->birth_place, 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, 'deleted_at' => $this->deleted_at, diff --git a/be/Modules/User/Transformers/UserResource.php b/be/Modules/User/Transformers/UserResource.php deleted file mode 100644 index b7021e4..0000000 --- a/be/Modules/User/Transformers/UserResource.php +++ /dev/null @@ -1,54 +0,0 @@ - $this->id, - 'name' => $this->name, - 'email' => $this->email, - 'ic_number' => $this->ic_number, - 'position' => $this->position, - 'phone_number' => $this->phone_number, - 'image_url' => $this->image_url ? Storage::disk('public')->url($this->image_url) : null, - 'status' => $this->status, - 'two_factor_secret' => $this->two_factor_secret, - 'two_factor_recovery_codes' => $this->two_factor_recovery_codes, - 'two_factor_confirmed_at' => $this->two_factor_confirmed_at, - 'email_verified_at' => $this->email_verified_at, - 'created_at' => $this->created_at, - 'updated_at' => $this->updated_at, - 'deleted_at' => $this->deleted_at, - 'roles' => $this->whenLoaded('roles', function () { - return $this->roles->map(function ($role) { - return [ - 'id' => $role->id, - 'name' => $role->name, - 'guard_name' => $role->guard_name, - 'permissions' => $this->when($role->relationLoaded('permissions'), function () use ($role) { - return $role->permissions->map(function ($permission) { - return [ - 'id' => $permission->id, - 'name' => $permission->name, - 'guard_name' => $permission->guard_name, - 'route_name' => $permission->route_name, - 'created_at' => $permission->created_at, - 'updated_at' => $permission->updated_at, - ]; - }); - }), - 'created_at' => $role->created_at, - 'updated_at' => $role->updated_at, - ]; - }); - }), - ]; - } -} diff --git a/be/app/Http/Controllers/ContactController.php b/be/app/Http/Controllers/ContactController.php deleted file mode 100644 index 5fae078..0000000 --- a/be/app/Http/Controllers/ContactController.php +++ /dev/null @@ -1,209 +0,0 @@ -contactService = $contactService; - } - - /** - * Get all active contact persons - */ - public function getContacts(): JsonResponse - { - try { - $contacts = $this->contactService->getActiveContacts(); - - return response()->json([ - 'success' => true, - 'data' => $contacts, - 'message' => 'Contact persons retrieved successfully' - ]); - } catch (\Exception $e) { - return response()->json([ - 'success' => false, - 'message' => 'Failed to retrieve contact persons', - 'error' => $e->getMessage() - ], 500); - } - } - - /** - * Get contact settings for admin (including inactive) - */ - public function getContactSettings(): JsonResponse - { - try { - $settings = $this->contactService->getAllContactSettings(); - - return response()->json([ - 'success' => true, - 'data' => $settings, - 'message' => 'Contact settings retrieved successfully' - ]); - } catch (\Exception $e) { - return response()->json([ - 'success' => false, - 'message' => 'Failed to retrieve contact settings', - 'error' => $e->getMessage() - ], 500); - } - } - - /** - * Update contact settings (Admin only) - */ - public function updateContacts(Request $request): JsonResponse - { - $request->validate([ - 'contacts' => 'required|array', - 'contacts.*.name' => 'required|string|max:100', - 'contacts.*.position' => 'nullable|string|max:100', - 'contacts.*.email' => 'nullable|email|max:255', - 'contacts.*.phone' => 'nullable|string|max:50', - 'contacts.*.department' => 'nullable|string|max:100', - 'contacts.*.is_active' => 'boolean', - 'contacts.*.sort_order' => 'integer|min:0' - ]); - - try { - DB::beginTransaction(); - - $result = $this->contactService->updateContactSettings($request->contacts); - - DB::commit(); - - return response()->json([ - 'success' => true, - 'data' => $result, - 'message' => 'Contact settings updated successfully' - ]); - } catch (\Exception $e) { - DB::rollBack(); - - return response()->json([ - 'success' => false, - 'message' => 'Failed to update contact settings', - 'error' => $e->getMessage() - ], 500); - } - } - - /** - * Add a new contact person (Admin only) - */ - public function addContact(Request $request): JsonResponse - { - $request->validate([ - 'name' => 'required|string|max:100', - 'position' => 'nullable|string|max:100', - 'email' => 'nullable|email|max:255', - 'phone' => 'nullable|string|max:50', - 'department' => 'nullable|string|max:100', - 'is_active' => 'boolean', - 'sort_order' => 'integer|min:0' - ]); - - try { - $result = $this->contactService->addContactPerson($request->all()); - - return response()->json([ - 'success' => true, - 'data' => $result, - 'message' => 'Contact person added successfully' - ], 201); - } catch (\Exception $e) { - return response()->json([ - 'success' => false, - 'message' => 'Failed to add contact person', - 'error' => $e->getMessage() - ], 500); - } - } - - /** - * Update a specific contact person (Admin only) - */ - public function updateContact(Request $request, int $id): JsonResponse - { - $request->validate([ - 'name' => 'required|string|max:100', - 'position' => 'nullable|string|max:100', - 'email' => 'nullable|email|max:255', - 'phone' => 'nullable|string|max:50', - 'department' => 'nullable|string|max:100', - 'is_active' => 'boolean', - 'sort_order' => 'integer|min:0' - ]); - - try { - $result = $this->contactService->updateContactPerson($id, $request->all()); - - return response()->json([ - 'success' => true, - 'data' => $result, - 'message' => 'Contact person updated successfully' - ]); - } catch (\Exception $e) { - return response()->json([ - 'success' => false, - 'message' => 'Failed to update contact person', - 'error' => $e->getMessage() - ], 500); - } - } - - /** - * Delete a contact person (Admin only) - */ - public function deleteContact(int $id): JsonResponse - { - try { - $result = $this->contactService->deleteContactPerson($id); - - return response()->json([ - 'success' => true, - 'data' => $result, - 'message' => 'Contact person deleted successfully' - ]); - } catch (\Exception $e) { - return response()->json([ - 'success' => false, - 'message' => 'Failed to delete contact person', - 'error' => $e->getMessage() - ], 500); - } - } - - /** - * Toggle active status of a contact person (Admin only) - */ - public function toggleContact(int $id): JsonResponse - { - try { - $result = $this->contactService->toggleContactPerson($id); - - return response()->json([ - 'success' => true, - 'data' => $result, - 'message' => 'Contact person status toggled successfully' - ]); - } catch (\Exception $e) { - return response()->json([ - 'success' => false, - 'message' => 'Failed to toggle contact person status', - 'error' => $e->getMessage() - ], 500); - } - } -} diff --git a/be/app/Http/Controllers/CountryController.php b/be/app/Http/Controllers/CountryController.php deleted file mode 100644 index 1beeaf8..0000000 --- a/be/app/Http/Controllers/CountryController.php +++ /dev/null @@ -1,24 +0,0 @@ -json([ - 'success' => true, - 'data' => $countries, - 'message' => 'Countries fetched successfully', - ]); - return response()->json([ - 'success' => true, - 'data' => $countries, - 'message' => 'Countries fetched successfully', - ]); - } -} \ No newline at end of file diff --git a/be/app/Http/Controllers/LetterPreviewController.php b/be/app/Http/Controllers/LetterPreviewController.php new file mode 100644 index 0000000..32d79d4 --- /dev/null +++ b/be/app/Http/Controllers/LetterPreviewController.php @@ -0,0 +1,49 @@ +environment('local', 'development')) { + abort(404); + } + + $view = 'pdf.preview'; + $format = $request->query('format', 'html'); + + if ($format === 'pdf') { + return $this->letterService->pdfResponse($view, [], 'letter-preview.pdf'); + } + + return $this->letterService->htmlResponse($view); + } + + public function showMembershipApprovedLetter(Request $request): Response + { + if (! app()->environment('local', 'development')) { + abort(404); + } + + $view = 'pdf.preview-approved-membership'; + $format = $request->query('format', 'html'); + + if ($format === 'pdf') { + return $this->letterService->pdfResponse($view, [], 'letter-preview-approved-membership.pdf'); + } + + return $this->letterService->htmlResponse($view); + } +} \ No newline at end of file diff --git a/be/app/Http/Controllers/OnlineUsersController.php b/be/app/Http/Controllers/OnlineUsersController.php deleted file mode 100644 index 3e53297..0000000 --- a/be/app/Http/Controllers/OnlineUsersController.php +++ /dev/null @@ -1,183 +0,0 @@ -onlineUsersService = $onlineUsersService; - } - - /** - * Get list of online users - * - * @param Request $request - * @return JsonResponse - */ - public function index(Request $request): JsonResponse - { - try { - $timeoutMinutes = 5; // Fixed to 5 minutes - - $onlineUsers = $this->onlineUsersService->getOnlineUsers($timeoutMinutes); - - return response()->json([ - 'success' => true, - 'data' => $onlineUsers, - 'meta' => [ - 'timeout_minutes' => $timeoutMinutes, - 'total_online' => $onlineUsers->count(), - 'timestamp' => now()->toISOString() - ], - 'message' => 'Online users retrieved successfully.' - ]); - - } catch (\Exception $e) { - Log::error('Error fetching online users: ' . $e->getMessage()); - - return response()->json([ - 'success' => false, - 'message' => 'Failed to retrieve online users.', - 'error' => config('app.debug') ? $e->getMessage() : 'Internal server error' - ], 500); - } - } - - /** - * Get online users count - * - * @param Request $request - * @return JsonResponse - */ - public function count(Request $request): JsonResponse - { - try { - $timeoutMinutes = 5; // Fixed to 5 minutes - - $count = $this->onlineUsersService->getOnlineUsersCount($timeoutMinutes); - - return response()->json([ - 'success' => true, - 'data' => [ - 'count' => $count, - 'timeout_minutes' => $timeoutMinutes - ], - 'message' => 'Online users count retrieved successfully.' - ]); - - } catch (\Exception $e) { - Log::error('Error fetching online users count: ' . $e->getMessage()); - - return response()->json([ - 'success' => false, - 'message' => 'Failed to retrieve online users count.', - 'error' => config('app.debug') ? $e->getMessage() : 'Internal server error' - ], 500); - } - } - - /** - * Get online users statistics - * - * @return JsonResponse - */ - public function stats(): JsonResponse - { - try { - $stats = $this->onlineUsersService->getOnlineUsersStats(); - - return response()->json([ - 'success' => true, - 'data' => $stats, - 'message' => 'Online users statistics retrieved successfully.' - ]); - - } catch (\Exception $e) { - Log::error('Error fetching online users stats: ' . $e->getMessage()); - - return response()->json([ - 'success' => false, - 'message' => 'Failed to retrieve online users statistics.', - 'error' => config('app.debug') ? $e->getMessage() : 'Internal server error' - ], 500); - } - } - - /** - * Get current user's session information - * - * @param Request $request - * @return JsonResponse - */ - public function mySession(Request $request): JsonResponse - { - try { - $user = $request->user(); - - if (!$user) { - return response()->json([ - 'success' => false, - 'message' => 'User not authenticated.' - ], 401); - } - - $sessionInfo = $this->onlineUsersService->getUserSessionInfo($user->id); - - if (!$sessionInfo) { - return response()->json([ - 'success' => false, - 'message' => 'No active session found.' - ], 404); - } - - return response()->json([ - 'success' => true, - 'data' => $sessionInfo, - 'message' => 'Session information retrieved successfully.' - ]); - - } catch (\Exception $e) { - Log::error('Error fetching user session info: ' . $e->getMessage()); - - return response()->json([ - 'success' => false, - 'message' => 'Failed to retrieve session information.', - 'error' => config('app.debug') ? $e->getMessage() : 'Internal server error' - ], 500); - } - } - - /** - * Clear online users cache (admin only) - * - * @return JsonResponse - */ - public function clearCache(): JsonResponse - { - try { - $this->onlineUsersService->clearCache(); - - return response()->json([ - 'success' => true, - 'message' => 'Online users cache cleared successfully.' - ]); - - } catch (\Exception $e) { - Log::error('Error clearing online users cache: ' . $e->getMessage()); - - return response()->json([ - 'success' => false, - 'message' => 'Failed to clear cache.', - 'error' => config('app.debug') ? $e->getMessage() : 'Internal server error' - ], 500); - } - } -} diff --git a/be/app/Http/Controllers/SocialMediaController.php b/be/app/Http/Controllers/SocialMediaController.php deleted file mode 100644 index 3421812..0000000 --- a/be/app/Http/Controllers/SocialMediaController.php +++ /dev/null @@ -1,206 +0,0 @@ -socialMediaService = $socialMediaService; - } - - /** - * Get all active social media platforms - */ - public function getSocialMedia(): JsonResponse - { - try { - $socialMedia = $this->socialMediaService->getActiveSocialMedia(); - - return response()->json([ - 'success' => true, - 'data' => $socialMedia, - 'message' => 'Social media platforms retrieved successfully' - ]); - } catch (\Exception $e) { - return response()->json([ - 'success' => false, - 'message' => 'Failed to retrieve social media platforms', - 'error' => $e->getMessage() - ], 500); - } - } - - /** - * Update social media settings (Admin only) - */ - public function updateSocialMedia(Request $request): JsonResponse - { - $request->validate([ - 'social_media' => 'required|array', - 'social_media.*.platform' => 'required|string|max:50', - 'social_media.*.name' => 'required|string|max:100', - 'social_media.*.url' => 'required|url|max:255', - 'social_media.*.icon' => 'nullable|string|max:100', - 'social_media.*.is_active' => 'boolean', - 'social_media.*.sort_order' => 'integer|min:0' - ]); - - try { - DB::beginTransaction(); - - $result = $this->socialMediaService->updateSocialMediaSettings($request->social_media); - - DB::commit(); - - return response()->json([ - 'success' => true, - 'data' => $result, - 'message' => 'Social media settings updated successfully' - ]); - } catch (\Exception $e) { - DB::rollBack(); - - return response()->json([ - 'success' => false, - 'message' => 'Failed to update social media settings', - 'error' => $e->getMessage() - ], 500); - } - } - - /** - * Get social media settings for admin (including inactive) - */ - public function getSocialMediaSettings(): JsonResponse - { - try { - $settings = $this->socialMediaService->getAllSocialMediaSettings(); - - return response()->json([ - 'success' => true, - 'data' => $settings, - 'message' => 'Social media settings retrieved successfully' - ]); - } catch (\Exception $e) { - return response()->json([ - 'success' => false, - 'message' => 'Failed to retrieve social media settings', - 'error' => $e->getMessage() - ], 500); - } - } - - /** - * Add a new social media platform (Admin only) - */ - public function addSocialMedia(Request $request): JsonResponse - { - $request->validate([ - 'platform' => 'required|string|max:50', - 'name' => 'required|string|max:100', - 'url' => 'required|url|max:255', - 'icon' => 'nullable|string|max:100', - 'is_active' => 'boolean', - 'sort_order' => 'integer|min:0' - ]); - - try { - $result = $this->socialMediaService->addSocialMediaPlatform($request->all()); - - return response()->json([ - 'success' => true, - 'data' => $result, - 'message' => 'Social media platform added successfully' - ], 201); - } catch (\Exception $e) { - return response()->json([ - 'success' => false, - 'message' => 'Failed to add social media platform', - 'error' => $e->getMessage() - ], 500); - } - } - - /** - * Update a specific social media platform (Admin only) - */ - public function updateSocialMediaPlatform(Request $request, int $id): JsonResponse - { - $request->validate([ - 'platform' => 'required|string|max:50', - 'name' => 'required|string|max:100', - 'url' => 'required|url|max:255', - 'icon' => 'nullable|string|max:100', - 'is_active' => 'boolean', - 'sort_order' => 'integer|min:0' - ]); - - try { - $result = $this->socialMediaService->updateSocialMediaPlatform($id, $request->all()); - - return response()->json([ - 'success' => true, - 'data' => $result, - 'message' => 'Social media platform updated successfully' - ]); - } catch (\Exception $e) { - return response()->json([ - 'success' => false, - 'message' => 'Failed to update social media platform', - 'error' => $e->getMessage() - ], 500); - } - } - - /** - * Delete a social media platform (Admin only) - */ - public function deleteSocialMediaPlatform(int $id): JsonResponse - { - try { - $result = $this->socialMediaService->deleteSocialMediaPlatform($id); - - return response()->json([ - 'success' => true, - 'data' => $result, - 'message' => 'Social media platform deleted successfully' - ]); - } catch (\Exception $e) { - return response()->json([ - 'success' => false, - 'message' => 'Failed to delete social media platform', - 'error' => $e->getMessage() - ], 500); - } - } - - /** - * Toggle active status of a social media platform (Admin only) - */ - public function toggleSocialMediaPlatform(int $id): JsonResponse - { - try { - $result = $this->socialMediaService->toggleSocialMediaPlatform($id); - - return response()->json([ - 'success' => true, - 'data' => $result, - 'message' => 'Social media platform status toggled successfully' - ]); - } catch (\Exception $e) { - return response()->json([ - 'success' => false, - 'message' => 'Failed to toggle social media platform status', - 'error' => $e->getMessage() - ], 500); - } - } -} \ No newline at end of file diff --git a/be/app/Models/Country.php b/be/app/Models/Country.php deleted file mode 100644 index 431c4bf..0000000 --- a/be/app/Models/Country.php +++ /dev/null @@ -1,12 +0,0 @@ - + */ + protected $fillable = [ + 'documentable_type', + 'documentable_id', + 'name', + 'path', + 'file_size', + 'mime_type', + 'type', + 'description', + 'uploaded_by', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var list + */ + protected $hidden = [ + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'name' => 'string', + 'path' => 'string', + 'file_size' => 'integer', + 'mime_type' => 'string', + 'type' => 'string', + 'description' => 'string', + ]; + } + + public function documentable(): MorphTo + { + return $this->morphTo(); + } + + public function uploadedBy(): BelongsTo + { + return $this->belongsTo(User::class, 'uploaded_by'); + } +} diff --git a/be/app/Notifications/Concerns/BuildsMailMessage.php b/be/app/Notifications/Concerns/BuildsMailMessage.php new file mode 100644 index 0000000..6516731 --- /dev/null +++ b/be/app/Notifications/Concerns/BuildsMailMessage.php @@ -0,0 +1,17 @@ +subject($subject) + ->markdown($view, array_merge([ + 'logoPath' => config('mail.logo_path'), + ], $data)); + } +} diff --git a/be/app/Providers/FortifyServiceProvider.php b/be/app/Providers/FortifyServiceProvider.php index f9abbaa..0077392 100644 --- a/be/app/Providers/FortifyServiceProvider.php +++ b/be/app/Providers/FortifyServiceProvider.php @@ -68,6 +68,18 @@ class FortifyServiceProvider extends ServiceProvider return Limit::perMinute(1)->by($throttleKey); }); + RateLimiter::for('password-reset-request', function (Request $request) { + $throttleKey = Str::transliterate(Str::lower($request->input('email', '')).'|'.$request->ip()); + + return Limit::perMinute(1)->by($throttleKey); + }); + + RateLimiter::for('password-reset', function (Request $request) { + $throttleKey = Str::transliterate(Str::lower($request->input('email', '')).'|'.$request->ip()); + + return Limit::perMinute(5)->by($throttleKey); + }); + Fortify::authenticateUsing(function (Request $request) { $user = User::where('email', $request->email)->first(); diff --git a/be/app/Services/ContactService.php b/be/app/Services/ContactService.php deleted file mode 100644 index e98d14a..0000000 --- a/be/app/Services/ContactService.php +++ /dev/null @@ -1,198 +0,0 @@ -where('is_active', true) - ->orderBy('sort_order', 'asc') - ->orderBy('name', 'asc') - ->get() - ->toArray(); - - return array_map(function ($item) { - return [ - 'id' => $item->id, - 'name' => $item->name, - 'position' => $item->position, - 'email' => $item->email, - 'phone' => $item->phone, - 'department' => $item->department, - 'sort_order' => $item->sort_order, - ]; - }, $contacts); - } catch (\Exception $e) { - Log::error('Error fetching active contacts: ' . $e->getMessage()); - throw $e; - } - } - - /** - * Get all contact settings (including inactive) for admin - */ - public function getAllContactSettings(): array - { - try { - $settings = DB::table('contact_settings') - ->orderBy('sort_order', 'asc') - ->orderBy('name', 'asc') - ->get() - ->toArray(); - - return array_map(function ($item) { - return [ - 'id' => $item->id, - 'name' => $item->name, - 'position' => $item->position, - 'email' => $item->email, - 'phone' => $item->phone, - 'department' => $item->department, - 'is_active' => (bool) $item->is_active, - 'sort_order' => $item->sort_order, - 'created_at' => $item->created_at, - 'updated_at' => $item->updated_at, - ]; - }, $settings); - } catch (\Exception $e) { - Log::error('Error fetching all contact settings: ' . $e->getMessage()); - throw $e; - } - } - - /** - * Update contact settings (bulk update) - */ - public function updateContactSettings(array $contactData): array - { - try { - // Clear existing settings - DB::table('contact_settings')->truncate(); - - // Insert new settings - $insertData = []; - foreach ($contactData as $index => $item) { - $insertData[] = [ - 'name' => $item['name'], - 'position' => $item['position'] ?? null, - 'email' => $item['email'] ?? null, - 'phone' => $item['phone'] ?? null, - 'department' => $item['department'] ?? null, - 'is_active' => $item['is_active'] ?? true, - 'sort_order' => $item['sort_order'] ?? $index, - 'created_at' => now(), - 'updated_at' => now(), - ]; - } - - DB::table('contact_settings')->insert($insertData); - - // Return updated settings - return $this->getAllContactSettings(); - } catch (\Exception $e) { - Log::error('Error updating contact settings: ' . $e->getMessage()); - throw $e; - } - } - - /** - * Add a single contact person - */ - public function addContactPerson(array $data): array - { - try { - $id = DB::table('contact_settings')->insertGetId([ - 'name' => $data['name'], - 'position' => $data['position'] ?? null, - 'email' => $data['email'] ?? null, - 'phone' => $data['phone'] ?? null, - 'department' => $data['department'] ?? null, - 'is_active' => $data['is_active'] ?? true, - 'sort_order' => $data['sort_order'] ?? 0, - 'created_at' => now(), - 'updated_at' => now(), - ]); - - return $this->getAllContactSettings(); - } catch (\Exception $e) { - Log::error('Error adding contact person: ' . $e->getMessage()); - throw $e; - } - } - - /** - * Update a single contact person - */ - public function updateContactPerson(int $id, array $data): array - { - try { - DB::table('contact_settings') - ->where('id', $id) - ->update([ - 'name' => $data['name'], - 'position' => $data['position'] ?? null, - 'email' => $data['email'] ?? null, - 'phone' => $data['phone'] ?? null, - 'department' => $data['department'] ?? null, - 'is_active' => $data['is_active'] ?? true, - 'sort_order' => $data['sort_order'] ?? 0, - 'updated_at' => now(), - ]); - - return $this->getAllContactSettings(); - } catch (\Exception $e) { - Log::error('Error updating contact person: ' . $e->getMessage()); - throw $e; - } - } - - /** - * Delete a contact person - */ - public function deleteContactPerson(int $id): array - { - try { - DB::table('contact_settings')->where('id', $id)->delete(); - - return $this->getAllContactSettings(); - } catch (\Exception $e) { - Log::error('Error deleting contact person: ' . $e->getMessage()); - throw $e; - } - } - - /** - * Toggle active status of a contact person - */ - public function toggleContactPerson(int $id): array - { - try { - $contact = DB::table('contact_settings')->where('id', $id)->first(); - - if (!$contact) { - throw new \Exception('Contact person not found'); - } - - DB::table('contact_settings') - ->where('id', $id) - ->update([ - 'is_active' => !$contact->is_active, - 'updated_at' => now(), - ]); - - return $this->getAllContactSettings(); - } catch (\Exception $e) { - Log::error('Error toggling contact person: ' . $e->getMessage()); - throw $e; - } - } -} diff --git a/be/app/Services/DocumentService.php b/be/app/Services/DocumentService.php index 4aaaee8..4b78240 100644 --- a/be/app/Services/DocumentService.php +++ b/be/app/Services/DocumentService.php @@ -19,22 +19,16 @@ class DocumentService string $documentType = 'general', ?string $description = null ): Document { - // Generate unique filename $fileName = time().'_'.$file->getClientOriginalName(); - - // Store file in a folder named after the model $folderName = strtolower(class_basename($model)); - $filePath = $file->storeAs("documents/{$folderName}", $fileName, 'public'); + $filePath = $file->storeAs("documents/{$folderName}", $fileName, Document::STORAGE_DISK); - // Create document record - return Document::create([ - 'documentable_type' => get_class($model), - 'documentable_id' => $model->id, - 'document_name' => $file->getClientOriginalName(), - 'document_path' => $filePath, + return $model->documents()->create([ + 'name' => $file->getClientOriginalName(), + 'path' => $filePath, 'file_size' => $file->getSize(), 'mime_type' => $file->getClientMimeType(), - 'document_type' => $documentType, + 'type' => $documentType, 'description' => $description, 'uploaded_by' => auth()->id(), ]); @@ -43,7 +37,7 @@ class DocumentService /** * Delete a document. */ - public function deleteDocument(int $documentId): bool + public function deleteDocument(string $documentId): bool { $document = Document::findOrFail($documentId); @@ -58,7 +52,7 @@ class DocumentService $query = $model->documents(); if ($documentType) { - $query->where('document_type', $documentType); + $query->where('type', $documentType); } return $query->with('uploadedBy')->get(); @@ -67,7 +61,7 @@ class DocumentService /** * Get document by ID with validation. */ - public function getDocument(int $documentId): Document + public function getDocument(string $documentId): Document { return Document::with('uploadedBy')->findOrFail($documentId); } @@ -75,15 +69,17 @@ class DocumentService /** * Download a document. */ - public function downloadDocument(int $documentId) + public function downloadDocument(string $documentId) { $document = $this->getDocument($documentId); - if (! Storage::exists($document->document_path)) { + $disk = Storage::disk(Document::STORAGE_DISK); + + if (! $disk->exists($document->path)) { throw new Exception('File not found'); } - return Storage::download($document->document_path, $document->document_name); + return $disk->download($document->path, $document->name); } /** @@ -94,7 +90,7 @@ class DocumentService $query = $model->documents(); if ($documentType) { - $query->where('document_type', $documentType); + $query->where('type', $documentType); } return $query->count(); @@ -140,12 +136,10 @@ class DocumentService $supportedTypes = $this->getSupportedFileTypes(); $maxSize = $this->getMaxFileSize() * 1024; // Convert to bytes - // Check file size if ($file->getSize() > $maxSize) { throw new Exception('File size exceeds maximum limit of '.$this->getMaxFileSize().'KB'); } - // Check mime type if (! in_array($file->getClientMimeType(), $supportedTypes)) { throw new Exception('File type not supported. Supported types: '.implode(', ', array_keys($supportedTypes))); } diff --git a/be/app/Services/LetterService.php b/be/app/Services/LetterService.php new file mode 100644 index 0000000..efc6931 --- /dev/null +++ b/be/app/Services/LetterService.php @@ -0,0 +1,102 @@ + $data + */ + public function renderHtml(string $view, array $data = []): string + { + return View::make($view, $this->prepareViewData($data))->render(); + } + + /** + * @param array $data + */ + public function htmlResponse(string $view, array $data = []): Response + { + return response($this->renderHtml($view, $data), SymfonyResponse::HTTP_OK, [ + 'Content-Type' => 'text/html; charset=UTF-8', + ]); + } + + /** + * @param array $data + */ + public function pdfResponse(string $view, array $data = [], string $filename = 'letter.pdf'): Response + { + $pdf = $this->makeBrowsershot($this->renderHtml($view, $data))->pdf(); + + return response($pdf, SymfonyResponse::HTTP_OK, [ + 'Content-Type' => 'application/pdf', + 'Content-Disposition' => 'inline; filename="'.$filename.'"', + ]); + } + + /** + * @param array $overrides + * @return array + */ + public function prepareViewData(array $overrides = []): array + { + $logoPath = $overrides['logoPath'] ?? config('mail.logo_path'); + + return array_merge([ + 'letterLayout' => LetterLayout::resolve(), + 'logoPath' => $logoPath, + 'logoUrl' => $this->resolveLogoDataUri($logoPath), + 'organizationAddress' => $overrides['organizationAddress'] ?? null, + 'organizationContact' => $overrides['organizationContact'] ?? null, + ], $overrides); + } + + protected function makeBrowsershot(string $html): Browsershot + { + $layout = LetterLayout::resolve(); + [$top, $right, $bottom, $left] = $layout['margins_mm']; + + $browsershot = Browsershot::html($html) + ->setNodeModulePath(config('browsershot.node_module_path')) + ->timeout(config('browsershot.timeout')) + ->format($layout['page_format']) + ->margins($top, $right, $bottom, $left) + ->showBackground(); + + if ($nodeBinary = config('browsershot.node_binary')) { + $browsershot->setNodeBinary($nodeBinary); + } + + if ($npmBinary = config('browsershot.npm_binary')) { + $browsershot->setNpmBinary($npmBinary); + } + + if ($chromePath = config('browsershot.chrome_path')) { + $browsershot->setChromePath($chromePath); + } + + if (config('browsershot.no_sandbox')) { + $browsershot->noSandbox(); + } + + return $browsershot; + } + + protected function resolveLogoDataUri(?string $logoPath): ?string + { + if (empty($logoPath) || ! file_exists($logoPath)) { + return null; + } + + $mime = mime_content_type($logoPath) ?: 'image/svg+xml'; + + return 'data:'.$mime.';base64,'.base64_encode(file_get_contents($logoPath)); + } +} diff --git a/be/app/Services/OnlineUsersService.php b/be/app/Services/OnlineUsersService.php deleted file mode 100644 index 8d6a3a4..0000000 --- a/be/app/Services/OnlineUsersService.php +++ /dev/null @@ -1,212 +0,0 @@ -subSeconds($timeoutSeconds); - - // Get active Sanctum tokens with user information - $onlineTokens = PersonalAccessToken::with(['tokenable.unit', 'tokenable.rank', 'tokenable.position']) - ->where('tokenable_type', User::class) - ->where('last_used_at', '>', $cutoffTime) - ->where(function ($query) { - $query->whereNull('expires_at') - ->orWhere('expires_at', '>', now()); - }) - ->whereHas('tokenable', function ($query) { - $query->whereNull('deleted_at'); - }) - ->orderBy('last_used_at', 'desc') - ->get(); - - // Group by user to handle multiple tokens - $onlineUsers = $onlineTokens->groupBy('tokenable_id')->map(function ($tokens) { - $token = $tokens->first(); - $user = $token->tokenable; - $latestToken = $tokens->sortByDesc('last_used_at')->first(); - - return [ - 'id' => $user->id, - 'name' => $user->name, - 'email' => $user->email, - 'army_number' => $user->army_number, - 'image_url' => $user->image_url ? Storage::disk('public')->url($user->image_url) : null, - 'status' => $user->status, - 'unit_name' => $user->unit?->name, - 'rank_name' => $user->rank?->name, - 'position_name' => $user->position?->name, - 'ip_address' => 'N/A', // Sanctum doesn't store IP by default - 'user_agent' => $this->parseUserAgent('Sanctum Token'), - 'last_activity' => $latestToken->last_used_at?->timestamp ?? $latestToken->created_at->timestamp, - 'last_activity_human' => $latestToken->last_used_at?->diffForHumans() ?? $latestToken->created_at->diffForHumans(), - 'session_count' => $tokens->count(), - 'is_online' => true - ]; - }); - - return $onlineUsers->values(); - }); - } - - /** - * Get online users count - * - * @param int $timeoutMinutes Minutes of inactivity to consider user offline - * @return int - */ - public function getOnlineUsersCount(int $timeoutMinutes = 5): int - { - $cacheKey = "online_users_count_{$timeoutMinutes}"; - - return Cache::remember($cacheKey, 30, function () use ($timeoutMinutes) { - $timeoutSeconds = $timeoutMinutes * 60; - $cutoffTime = now()->subSeconds($timeoutSeconds); - - return PersonalAccessToken::where('tokenable_type', User::class) - ->where('last_used_at', '>', $cutoffTime) - ->where(function ($query) { - $query->whereNull('expires_at') - ->orWhere('expires_at', '>', now()); - }) - ->whereHas('tokenable', function ($query) { - $query->whereNull('deleted_at'); - }) - ->distinct('tokenable_id') - ->count('tokenable_id'); - }); - } - - /** - * Get user's current session information - * - * @param int $userId - * @return array|null - */ - public function getUserSessionInfo(int $userId): ?array - { - $token = PersonalAccessToken::where('tokenable_type', User::class) - ->where('tokenable_id', $userId) - ->where(function ($query) { - $query->whereNull('expires_at') - ->orWhere('expires_at', '>', now()); - }) - ->orderBy('last_used_at', 'desc') - ->first(); - - if (!$token) { - return null; - } - - return [ - 'session_id' => $token->id, - 'ip_address' => 'N/A', // Sanctum doesn't store IP by default - 'user_agent' => $this->parseUserAgent('Sanctum Token'), - 'last_activity' => $token->last_used_at?->timestamp ?? $token->created_at->timestamp, - 'last_activity_human' => $token->last_used_at?->diffForHumans() ?? $token->created_at->diffForHumans(), - 'is_online' => $token->last_used_at && $token->last_used_at->gt(now()->subMinutes(5)) - ]; - } - - /** - * Parse user agent string to extract browser and OS info - * - * @param string $userAgent - * @return array - */ - private function parseUserAgent(string $userAgent): array - { - $browser = 'Unknown'; - $os = 'Unknown'; - - // Simple browser detection - if (strpos($userAgent, 'Chrome') !== false) { - $browser = 'Chrome'; - } elseif (strpos($userAgent, 'Firefox') !== false) { - $browser = 'Firefox'; - } elseif (strpos($userAgent, 'Safari') !== false) { - $browser = 'Safari'; - } elseif (strpos($userAgent, 'Edge') !== false) { - $browser = 'Edge'; - } - - // Simple OS detection - if (strpos($userAgent, 'Windows') !== false) { - $os = 'Windows'; - } elseif (strpos($userAgent, 'Mac') !== false) { - $os = 'macOS'; - } elseif (strpos($userAgent, 'Linux') !== false) { - $os = 'Linux'; - } elseif (strpos($userAgent, 'Android') !== false) { - $os = 'Android'; - } elseif (strpos($userAgent, 'iOS') !== false) { - $os = 'iOS'; - } - - return [ - 'browser' => $browser, - 'os' => $os, - 'raw' => $userAgent - ]; - } - - /** - * Clear online users cache - */ - public function clearCache(): void - { - Cache::forget('online_users_5'); - Cache::forget('online_users_10'); - Cache::forget('online_users_15'); - Cache::forget('online_users_30'); - Cache::forget('online_users_count_5'); - Cache::forget('online_users_count_10'); - Cache::forget('online_users_count_15'); - Cache::forget('online_users_count_30'); - } - - /** - * Get online users statistics - * - * @return array - */ - public function getOnlineUsersStats(): array - { - $stats = []; - - // Get stats for different timeout periods - $timeouts = [5, 10, 15, 30]; - - foreach ($timeouts as $timeout) { - $stats["online_{$timeout}min"] = $this->getOnlineUsersCount($timeout); - } - - // Get total registered users - $stats['total_users'] = User::count(); - - // Get users by status - $stats['active_users'] = User::where('status', 'active')->count(); - $stats['inactive_users'] = User::where('status', 'inactive')->count(); - - return $stats; - } -} diff --git a/be/app/Services/SocialMediaService.php b/be/app/Services/SocialMediaService.php deleted file mode 100644 index 433dc8b..0000000 --- a/be/app/Services/SocialMediaService.php +++ /dev/null @@ -1,193 +0,0 @@ -where('is_active', true) - ->orderBy('sort_order', 'asc') - ->orderBy('name', 'asc') - ->get() - ->toArray(); - - return array_map(function ($item) { - return [ - 'id' => $item->id, - 'platform' => $item->platform, - 'name' => $item->name, - 'url' => $item->url, - 'icon' => $item->icon, - 'sort_order' => $item->sort_order, - ]; - }, $socialMedia); - } catch (\Exception $e) { - Log::error('Error fetching active social media: ' . $e->getMessage()); - throw $e; - } - } - - /** - * Get all social media settings (including inactive) for admin - */ - public function getAllSocialMediaSettings(): array - { - try { - $settings = DB::table('social_media_settings') - ->orderBy('sort_order', 'asc') - ->orderBy('name', 'asc') - ->get() - ->toArray(); - - return array_map(function ($item) { - return [ - 'id' => $item->id, - 'platform' => $item->platform, - 'name' => $item->name, - 'url' => $item->url, - 'icon' => $item->icon, - 'is_active' => (bool) $item->is_active, - 'sort_order' => $item->sort_order, - 'created_at' => $item->created_at, - 'updated_at' => $item->updated_at, - ]; - }, $settings); - } catch (\Exception $e) { - Log::error('Error fetching all social media settings: ' . $e->getMessage()); - throw $e; - } - } - - /** - * Update social media settings (bulk update) - */ - public function updateSocialMediaSettings(array $socialMediaData): array - { - try { - // Clear existing settings - DB::table('social_media_settings')->truncate(); - - // Insert new settings - $insertData = []; - foreach ($socialMediaData as $index => $item) { - $insertData[] = [ - 'platform' => $item['platform'], - 'name' => $item['name'], - 'url' => $item['url'], - 'icon' => $item['icon'] ?? null, - 'is_active' => $item['is_active'] ?? true, - 'sort_order' => $item['sort_order'] ?? $index, - 'created_at' => now(), - 'updated_at' => now(), - ]; - } - - DB::table('social_media_settings')->insert($insertData); - - // Return updated settings - return $this->getAllSocialMediaSettings(); - } catch (\Exception $e) { - Log::error('Error updating social media settings: ' . $e->getMessage()); - throw $e; - } - } - - /** - * Add a single social media platform - */ - public function addSocialMediaPlatform(array $data): array - { - try { - $id = DB::table('social_media_settings')->insertGetId([ - 'platform' => $data['platform'], - 'name' => $data['name'], - 'url' => $data['url'], - 'icon' => $data['icon'] ?? null, - 'is_active' => $data['is_active'] ?? true, - 'sort_order' => $data['sort_order'] ?? 0, - 'created_at' => now(), - 'updated_at' => now(), - ]); - - return $this->getAllSocialMediaSettings(); - } catch (\Exception $e) { - Log::error('Error adding social media platform: ' . $e->getMessage()); - throw $e; - } - } - - /** - * Update a single social media platform - */ - public function updateSocialMediaPlatform(int $id, array $data): array - { - try { - DB::table('social_media_settings') - ->where('id', $id) - ->update([ - 'platform' => $data['platform'], - 'name' => $data['name'], - 'url' => $data['url'], - 'icon' => $data['icon'] ?? null, - 'is_active' => $data['is_active'] ?? true, - 'sort_order' => $data['sort_order'] ?? 0, - 'updated_at' => now(), - ]); - - return $this->getAllSocialMediaSettings(); - } catch (\Exception $e) { - Log::error('Error updating social media platform: ' . $e->getMessage()); - throw $e; - } - } - - /** - * Delete a social media platform - */ - public function deleteSocialMediaPlatform(int $id): array - { - try { - DB::table('social_media_settings')->where('id', $id)->delete(); - - return $this->getAllSocialMediaSettings(); - } catch (\Exception $e) { - Log::error('Error deleting social media platform: ' . $e->getMessage()); - throw $e; - } - } - - /** - * Toggle active status of a social media platform - */ - public function toggleSocialMediaPlatform(int $id): array - { - try { - $platform = DB::table('social_media_settings')->where('id', $id)->first(); - - if (!$platform) { - throw new \Exception('Social media platform not found'); - } - - DB::table('social_media_settings') - ->where('id', $id) - ->update([ - 'is_active' => !$platform->is_active, - 'updated_at' => now(), - ]); - - return $this->getAllSocialMediaSettings(); - } catch (\Exception $e) { - Log::error('Error toggling social media platform: ' . $e->getMessage()); - throw $e; - } - } -} diff --git a/be/app/Services/VisibilityService.php b/be/app/Services/VisibilityService.php deleted file mode 100644 index dad6050..0000000 --- a/be/app/Services/VisibilityService.php +++ /dev/null @@ -1,304 +0,0 @@ -can('akses peringkat keseluruhan', Unit::class)) { - return $query; - } - - // Get user's unit - $userUnit = Unit::find($user->unit_id); - if (!$userUnit) { - return $query->whereRaw('1 = 0'); - } - - // Government permission: Can view all units under their government - // Government users do NOT see units without formations - // SEL PERO 91 REJ (process models): ONLY see units without formation - not their gov/formation units - if ($user->can('akses peringkat formasi')) { - if ($includeUnitsWithoutFormation && $user->hasRole('SEL PERO 91 REJ')) { - $unitIds = Unit::whereNull('formation_id') - ->whereNull('repair_formation_id') - ->pluck('id') - ->toArray(); - } else { - $unitIds = $this->getUnitsUnderGovernment($userUnit); - $unitIds = $this->excludeUnitsWithoutFormation($unitIds); - } - - if (!empty($unitIds)) { - return $query->whereIn($unitColumn, $unitIds); - } - return $query->whereRaw('1 = 0'); - } - - // DIV permission: Can view all units under their formation (including repair formations) - if ($user->can('akses peringkat divisyen')) { - $unitIds = $this->getVisibleUnitIdsForFormationUser($user, $userUnit, $includeUnitsWithoutFormation); - - if (!empty($unitIds)) { - return $query->whereIn($unitColumn, $unitIds); - } - return $query->whereRaw('1 = 0'); - } - - // Unit permission: Can only view data within their unit - return $query->where($unitColumn, $user->unit_id); - } - - /** - * Apply visibility scoping for models with indirect unit relationships - * - * @param bool $includeUnitsWithoutFormation When true (process models), SEL PERO 91 REJ - * users can see units without formations. Government users never see units without formations. - */ - public function applyVisibilityToIndirectQuery(Builder $query, $user, array $unitRelationship, bool $includeUnitsWithoutFormation = false): Builder - { - // Check if user has akses peringkat keseluruhan permission (super admin) - if ($user->can('akses peringkat keseluruhan', Unit::class)) { - return $query; - } - - // Get user's unit - $userUnit = Unit::find($user->unit_id); - if (!$userUnit) { - return $query->whereRaw('1 = 0'); - } - - // Government permission: Can view all units under their government - // SEL PERO 91 REJ (process models): ONLY see units without formation - not their gov/formation units - if ($user->can('akses peringkat formasi')) { - if ($includeUnitsWithoutFormation && $user->hasRole('SEL PERO 91 REJ')) { - $unitIds = Unit::whereNull('formation_id') - ->whereNull('repair_formation_id') - ->pluck('id') - ->toArray(); - } else { - $unitIds = $this->getUnitsUnderGovernment($userUnit); - $unitIds = $this->excludeUnitsWithoutFormation($unitIds); - } - - if (!empty($unitIds)) { - return $query->whereHas($unitRelationship['relationship'], function ($subQuery) use ($unitIds, $unitRelationship) { - $subQuery->whereIn($unitRelationship['unitColumn'], $unitIds); - }); - } - return $query->whereRaw('1 = 0'); - } - - // DIV permission: Can view all units under their formation (including repair formations) - if ($user->can('akses peringkat divisyen')) { - $unitIds = $this->getVisibleUnitIdsForFormationUser($user, $userUnit, $includeUnitsWithoutFormation); - - if (!empty($unitIds)) { - return $query->whereHas($unitRelationship['relationship'], function ($subQuery) use ($unitIds, $unitRelationship) { - $subQuery->whereIn($unitRelationship['unitColumn'], $unitIds); - }); - } - return $query->whereRaw('1 = 0'); - } - - // Unit permission: Can only view data within their unit - return $query->whereHas($unitRelationship['relationship'], function ($subQuery) use ($user, $unitRelationship) { - $subQuery->where($unitRelationship['unitColumn'], $user->unit_id); - }); - } - - /** - * Get all units under the same government as the user's unit - * Handles both direct government relationships and formation-government relationships - * Priority: Direct government_id takes precedence over formation government - * - * @param Unit $userUnit - * @return array - */ - public function getUnitsUnderGovernment(Unit $userUnit): array - { - $unitIds = collect(); - - // Case 1: User's unit has direct government relationship (HIGHEST PRIORITY) - if ($userUnit->government_id) { - $directUnits = Unit::where('government_id', $userUnit->government_id) - ->pluck('id'); - $unitIds = $unitIds->merge($directUnits); - - // Also get units in formations under the same direct government - $formationUnits = Unit::whereHas('formation', function ($query) use ($userUnit) { - $query->where('government_id', $userUnit->government_id); - })->pluck('id'); - $unitIds = $unitIds->merge($formationUnits); - - return $unitIds->unique()->toArray(); - } - - // Case 2: User's unit belongs to a formation that has a government (FALLBACK) - if ($userUnit->formation_id) { - $formation = Formation::find($userUnit->formation_id); - if ($formation && $formation->government_id) { - // Get all units in formations under the same government - $formationUnits = Unit::whereHas('formation', function ($query) use ($formation) { - $query->where('government_id', $formation->government_id); - })->pluck('id'); - $unitIds = $unitIds->merge($formationUnits); - - // Also get direct government units under the same government - $directGovUnits = Unit::where('government_id', $formation->government_id) - ->pluck('id'); - $unitIds = $unitIds->merge($directGovUnits); - } - } - - return $unitIds->unique()->toArray(); - } - - /** - * Get the repair formation ID for a unit - * Priority: repair_formation_id > formation_id > null - * - * @param Unit $unit - * @return int|null - */ - public function getRepairFormationId(Unit $unit): ?int - { - // Priority 1: Check repair_formation_id (custom repair formation) - if ($unit->repair_formation_id) { - return $unit->repair_formation_id; - } - - // Priority 2: Fall back to regular formation_id - if ($unit->formation_id) { - return $unit->formation_id; - } - - // Priority 3: No formation (government-level only) - return null; - } - - /** - * Get visible unit IDs for a user - * - * @param bool $includeUnitsWithoutFormation When true (process models), SEL PERO 91 REJ - * users can see units without formations. Government users never see units without formations. - */ - public function getVisibleUnitIds($user, bool $includeUnitsWithoutFormation = false): array - { - // Check if user has akses peringkat keseluruhan permission (super admin) - if ($user->can('akses peringkat keseluruhan', Unit::class)) { - return Unit::pluck('id')->toArray(); - } - - // Get user's unit - $userUnit = Unit::find($user->unit_id); - if (!$userUnit) { - return []; - } - - // Government permission: Can view all units under their government - // SEL PERO 91 REJ (process models): ONLY see units without formation - not their gov/formation units - if ($user->can('akses peringkat formasi')) { - if ($includeUnitsWithoutFormation && $user->hasRole('SEL PERO 91 REJ')) { - return Unit::whereNull('formation_id') - ->whereNull('repair_formation_id') - ->pluck('id') - ->toArray(); - } - $unitIds = $this->getUnitsUnderGovernment($userUnit); - return $this->excludeUnitsWithoutFormation($unitIds); - } - - // DIV permission: Can view all units under their formation (including repair formations) - if ($user->can('akses peringkat divisyen')) { - return $this->getVisibleUnitIdsForFormationUser($user, $userUnit, $includeUnitsWithoutFormation); - } - - // Unit permission: Can only view data within their unit - return [$user->unit_id]; - } - - /** - * Exclude units that have no formation (both formation_id and repair_formation_id are null). - * Government users do not see these units. - */ - protected function excludeUnitsWithoutFormation(array $unitIds): array - { - if (empty($unitIds)) { - return []; - } - - return Unit::whereIn('id', $unitIds) - ->where(function ($q) { - $q->whereNotNull('formation_id') - ->orWhereNotNull('repair_formation_id'); - }) - ->pluck('id') - ->toArray(); - } - - /** - * Get visible unit IDs for a user with akses peringkat divisyen permission. - * - * Asset view (includeUnitsWithoutFormation=false): Only units where formation_id matches. - * Excludes units that have repair_formation_id but formation_id null (e.g. 10 SKN RAJD uses - * 3 DIV for repair only - 3 DIV users see their repairs but NOT their asset data). - * - * Process view (includeUnitsWithoutFormation=true): Units where repair_formation_id OR - * formation_id matches. SEL PERO 91 REJ also sees units without any formation. - */ - protected function getVisibleUnitIdsForFormationUser($user, Unit $userUnit, bool $includeUnitsWithoutFormation): array - { - $userRepairFormationId = $this->getRepairFormationId($userUnit); - - if ($userRepairFormationId) { - if (!$includeUnitsWithoutFormation) { - // Asset view: Only units that belong to the formation (formation_id matches). - // Exclude units that use formation only for repair (formation_id null, repair_formation_id set). - return Unit::where('formation_id', $userRepairFormationId) - ->pluck('id') - ->toArray(); - } - - // SEL PERO 91 REJ: ONLY see units without formation - not their formation's units - // (formation units are handled by SEL PERO DIV) - if ($user->hasRole('SEL PERO 91 REJ')) { - return Unit::whereNull('formation_id') - ->whereNull('repair_formation_id') - ->pluck('id') - ->toArray(); - } - - // Process view: Units in user's formation (repair_formation_id or formation_id) - return Unit::where(function ($q) use ($userRepairFormationId) { - $q->where('repair_formation_id', $userRepairFormationId) - ->orWhere(function ($q2) use ($userRepairFormationId) { - $q2->whereNull('repair_formation_id') - ->where('formation_id', $userRepairFormationId); - }); - })->pluck('id')->toArray(); - } - - // User's unit has no formation - check if they have SEL PERO 91 REJ (process models only) - if ($includeUnitsWithoutFormation && $user->hasRole('SEL PERO 91 REJ')) { - return Unit::whereNull('formation_id') - ->whereNull('repair_formation_id') - ->pluck('id') - ->toArray(); - } - - return []; - } -} - diff --git a/be/app/Support/LetterLayout.php b/be/app/Support/LetterLayout.php new file mode 100644 index 0000000..c8543fa --- /dev/null +++ b/be/app/Support/LetterLayout.php @@ -0,0 +1,83 @@ + + * } + */ + public static function resolve(): array + { + $page = config('letter.page'); + $content = config('letter.content'); + $typography = config('letter.typography'); + $spacing = config('letter.spacing'); + $colors = config('letter.colors'); + + $marginTop = self::length($page['margin_top_mm']); + $marginRight = self::length($page['margin_right_mm']); + $marginBottom = self::length($page['margin_bottom_mm']); + $marginLeft = self::length($page['margin_left_mm']); + $contentPaddingX = self::length($content['padding_x_mm']); + $pageHeight = $page['height_mm'].'mm'; + + return [ + 'page_format' => $page['format'], + 'margins_mm' => [ + $page['margin_top_mm'], + $page['margin_right_mm'], + $page['margin_bottom_mm'], + $page['margin_left_mm'], + ], + 'css' => [ + 'margin_top' => $marginTop, + 'margin_right' => $marginRight, + 'margin_bottom' => $marginBottom, + 'margin_left' => $marginLeft, + 'content_padding_x' => $contentPaddingX, + 'page_height' => $pageHeight, + 'sheet_min_height' => sprintf( + 'calc(%s - %s - %s)', + $pageHeight, + $marginTop, + $marginBottom, + ), + 'font_family' => $typography['font_family'], + 'font_size' => $typography['font_size'], + 'line_height' => (string) $typography['line_height'], + 'body_color' => $typography['body_color'], + 'signatory_title_size' => $typography['signatory_title_size'], + 'signatory_title_color' => $typography['signatory_title_color'], + 'footer_font_size' => $typography['footer_font_size'], + 'footer_line_height' => (string) $typography['footer_line_height'], + 'letterhead_registration_size' => $typography['letterhead_registration_size'], + 'letterhead_margin_bottom' => $spacing['letterhead_margin_bottom'].'px', + 'meta_margin_bottom' => $spacing['meta_margin_bottom'].'px', + 'recipient_margin_bottom' => $spacing['recipient_margin_bottom'].'px', + 'subject_margin_bottom' => $spacing['subject_margin_bottom'].'px', + 'body_margin_bottom' => $spacing['body_margin_bottom'].'px', + 'body_paragraph_margin_bottom' => $spacing['body_paragraph_margin_bottom'].'px', + 'signature_block_margin_bottom' => $spacing['signature_block_margin_bottom'].'px', + 'signature_company_margin_bottom' => $spacing['signature_company_margin_bottom'].'px', + 'signature_space_height' => $spacing['signature_space_height'].'px', + 'logo_height' => $spacing['logo_height'].'px', + 'color_blue' => $colors['blue'], + 'color_orange' => $colors['orange'], + 'color_white' => $colors['white'], + 'color_black' => $colors['black'], + ], + ]; + } + + private static function length(int $millimetres): string + { + $centimetres = $millimetres / 10; + + return rtrim(rtrim(number_format($centimetres, 2, '.', ''), '0'), '.').'cm'; + } +} diff --git a/be/app/Traits/HasDocuments.php b/be/app/Traits/HasDocuments.php index e4580a3..ffd6cd5 100644 --- a/be/app/Traits/HasDocuments.php +++ b/be/app/Traits/HasDocuments.php @@ -20,7 +20,7 @@ trait HasDocuments */ public function documentsOfType($type) { - return $this->documents()->where('document_type', $type); + return $this->documents()->where('type', $type); } /** @@ -33,15 +33,15 @@ trait HasDocuments // Store file in a folder named after the model $folderName = strtolower(class_basename($this)); - $filePath = $file->storeAs("documents/{$folderName}", $fileName); + $filePath = $file->storeAs("documents/{$folderName}", $fileName, Document::STORAGE_DISK); // Create document record return $this->documents()->create([ - 'document_name' => $file->getClientOriginalName(), - 'document_path' => $filePath, + 'name' => $file->getClientOriginalName(), + 'path' => $filePath, 'file_size' => $file->getSize(), 'mime_type' => $file->getClientMimeType(), - 'document_type' => $documentType, + 'type' => $documentType, 'description' => $description, 'uploaded_by' => auth()->id(), ]); diff --git a/be/app/Traits/HasVisibility.php b/be/app/Traits/HasVisibility.php deleted file mode 100644 index 1d42fd0..0000000 --- a/be/app/Traits/HasVisibility.php +++ /dev/null @@ -1,58 +0,0 @@ -includeUnitsWithoutFormation - : false; - - // Check if model has a direct unit column - $unitColumn = property_exists($this, 'unitColumn') ? $this->unitColumn : 'unit_id'; - - // Check if model has an indirect unit relationship - $unitRelationship = property_exists($this, 'unitRelationship') ? $this->unitRelationship : null; - - if ($unitRelationship) { - return $this->getVisibilityService()->applyVisibilityToIndirectQuery( - $query, - $user, - $unitRelationship, - $includeUnitsWithoutFormation - ); - } - - return $this->getVisibilityService()->applyVisibilityToQuery( - $query, - $user, - $unitColumn, - $includeUnitsWithoutFormation - ); - } - - /** - * Get all units under the same government as the user's unit - * Handles both direct government relationships and formation-government relationships - * Priority: Direct government_id takes precedence over formation government - * - * @deprecated Use VisibilityService::getUnitsUnderGovernment() instead - */ - protected function getUnitsUnderGovernment($userUnit) - { - return $this->getVisibilityService()->getUnitsUnderGovernment($userUnit); - } -} \ No newline at end of file diff --git a/be/composer.json b/be/composer.json index 5fda7bc..93d2c6f 100644 --- a/be/composer.json +++ b/be/composer.json @@ -19,6 +19,7 @@ "laravel/tinker": "^2.10.1", "maatwebsite/excel": "^3.1", "nwidart/laravel-modules": "^12.0", + "spatie/browsershot": "*", "spatie/laravel-activitylog": "^4.10", "spatie/laravel-permission": "^6.21" }, diff --git a/be/composer.lock b/be/composer.lock index 18879a6..2f61a0a 100644 --- a/be/composer.lock +++ b/be/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "74a39a84834cfbce955691dc2137f5b4", + "content-hash": "d542997c95e6b07d8e6db6e796b81d4f", "packages": [ { "name": "bacon/bacon-qr-code", - "version": "v3.0.1", + "version": "v3.1.1", "source": { "type": "git", "url": "https://github.com/Bacon/BaconQrCode.git", - "reference": "f9cc1f52b5a463062251d666761178dbdb6b544f" + "reference": "4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/f9cc1f52b5a463062251d666761178dbdb6b544f", - "reference": "f9cc1f52b5a463062251d666761178dbdb6b544f", + "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2", + "reference": "4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2", "shasum": "" }, "require": { @@ -27,8 +27,9 @@ }, "require-dev": { "phly/keep-a-changelog": "^2.12", - "phpunit/phpunit": "^10.5.11 || 11.0.4", + "phpunit/phpunit": "^10.5.11 || ^11.0.4", "spatie/phpunit-snapshot-assertions": "^5.1.5", + "spatie/pixelmatch-php": "^1.2.0", "squizlabs/php_codesniffer": "^3.9" }, "suggest": { @@ -56,22 +57,22 @@ "homepage": "https://github.com/Bacon/BaconQrCode", "support": { "issues": "https://github.com/Bacon/BaconQrCode/issues", - "source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.1" + "source": "https://github.com/Bacon/BaconQrCode/tree/v3.1.1" }, - "time": "2024-10-01T13:55:55+00:00" + "time": "2026-04-05T21:06:35+00:00" }, { "name": "brick/math", - "version": "0.14.0", + "version": "0.14.8", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2" + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", - "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", + "url": "https://api.github.com/repos/brick/math/zipball/63422359a44b7f06cae63c3b429b59e8efcc0629", + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629", "shasum": "" }, "require": { @@ -110,7 +111,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.14.0" + "source": "https://github.com/brick/math/tree/0.14.8" }, "funding": [ { @@ -118,7 +119,7 @@ "type": "github" } ], - "time": "2025-08-29T12:40:03+00:00" + "time": "2026-02-10T14:33:43+00:00" }, { "name": "carbonphp/carbon-doctrine-types", @@ -191,28 +192,29 @@ }, { "name": "composer/pcre", - "version": "3.3.2", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", "shasum": "" }, "require": { "php": "^7.4 || ^8.0" }, "conflict": { - "phpstan/phpstan": "<1.11.10" + "phpstan/phpstan": "<2.2.2" }, "require-dev": { - "phpstan/phpstan": "^1.12 || ^2", - "phpstan/phpstan-strict-rules": "^1 || ^2", - "phpunit/phpunit": "^8 || ^9" + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9" }, "type": "library", "extra": { @@ -250,7 +252,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.3.2" + "source": "https://github.com/composer/pcre/tree/3.4.0" }, "funding": [ { @@ -260,13 +262,9 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2024-11-12T16:29:46+00:00" + "time": "2026-06-07T11:47:49+00:00" }, { "name": "composer/semver", @@ -470,6 +468,54 @@ }, "time": "2024-07-08T12:26:09+00:00" }, + { + "name": "doctrine/deprecations", + "version": "1.1.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=14" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" + }, + "time": "2026-02-07T07:09:04+00:00" + }, { "name": "doctrine/inflector", "version": "2.1.0", @@ -639,16 +685,16 @@ }, { "name": "dompdf/dompdf", - "version": "v3.1.2", + "version": "v3.1.5", "source": { "type": "git", "url": "https://github.com/dompdf/dompdf.git", - "reference": "b3493e35d31a5e76ec24c3b64a29b0034b2f32a6" + "reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dompdf/dompdf/zipball/b3493e35d31a5e76ec24c3b64a29b0034b2f32a6", - "reference": "b3493e35d31a5e76ec24c3b64a29b0034b2f32a6", + "url": "https://api.github.com/repos/dompdf/dompdf/zipball/f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496", + "reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496", "shasum": "" }, "require": { @@ -697,22 +743,22 @@ "homepage": "https://github.com/dompdf/dompdf", "support": { "issues": "https://github.com/dompdf/dompdf/issues", - "source": "https://github.com/dompdf/dompdf/tree/v3.1.2" + "source": "https://github.com/dompdf/dompdf/tree/v3.1.5" }, - "time": "2025-09-23T03:06:41+00:00" + "time": "2026-03-03T13:54:37+00:00" }, { "name": "dompdf/php-font-lib", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/dompdf/php-font-lib.git", - "reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d" + "reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d", - "reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d", + "url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/a6e9a688a2a80016ac080b97be73d3e10c444c9a", + "reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a", "shasum": "" }, "require": { @@ -720,7 +766,7 @@ "php": "^7.1 || ^8.0" }, "require-dev": { - "symfony/phpunit-bridge": "^3 || ^4 || ^5 || ^6" + "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11 || ^12" }, "type": "library", "autoload": { @@ -742,31 +788,31 @@ "homepage": "https://github.com/dompdf/php-font-lib", "support": { "issues": "https://github.com/dompdf/php-font-lib/issues", - "source": "https://github.com/dompdf/php-font-lib/tree/1.0.1" + "source": "https://github.com/dompdf/php-font-lib/tree/1.0.2" }, - "time": "2024-12-02T14:37:59+00:00" + "time": "2026-01-20T14:10:26+00:00" }, { "name": "dompdf/php-svg-lib", - "version": "1.0.0", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/dompdf/php-svg-lib.git", - "reference": "eb045e518185298eb6ff8d80d0d0c6b17aecd9af" + "reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/eb045e518185298eb6ff8d80d0d0c6b17aecd9af", - "reference": "eb045e518185298eb6ff8d80d0d0c6b17aecd9af", + "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/8259ffb930817e72b1ff1caef5d226501f3dfeb1", + "reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1", "shasum": "" }, "require": { "ext-mbstring": "*", "php": "^7.1 || ^8.0", - "sabberworm/php-css-parser": "^8.4" + "sabberworm/php-css-parser": "^8.4 || ^9.0" }, "require-dev": { - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5" + "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11" }, "type": "library", "autoload": { @@ -788,35 +834,34 @@ "homepage": "https://github.com/dompdf/php-svg-lib", "support": { "issues": "https://github.com/dompdf/php-svg-lib/issues", - "source": "https://github.com/dompdf/php-svg-lib/tree/1.0.0" + "source": "https://github.com/dompdf/php-svg-lib/tree/1.0.2" }, - "time": "2024-04-29T13:26:35+00:00" + "time": "2026-01-02T16:01:13+00:00" }, { "name": "dragonmantank/cron-expression", - "version": "v3.4.0", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/dragonmantank/cron-expression.git", - "reference": "8c784d071debd117328803d86b2097615b457500" + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/8c784d071debd117328803d86b2097615b457500", - "reference": "8c784d071debd117328803d86b2097615b457500", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013", "shasum": "" }, "require": { - "php": "^7.2|^8.0", - "webmozart/assert": "^1.0" + "php": "^8.2|^8.3|^8.4|^8.5" }, "replace": { "mtdowling/cron-expression": "^1.0" }, "require-dev": { - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^1.0", - "phpunit/phpunit": "^7.0|^8.0|^9.0" + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.32|^2.1.31", + "phpunit/phpunit": "^8.5.48|^9.0" }, "type": "library", "extra": { @@ -847,7 +892,7 @@ ], "support": { "issues": "https://github.com/dragonmantank/cron-expression/issues", - "source": "https://github.com/dragonmantank/cron-expression/tree/v3.4.0" + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.6.0" }, "funding": [ { @@ -855,7 +900,7 @@ "type": "github" } ], - "time": "2024-10-09T13:47:03+00:00" + "time": "2025-10-31T18:51:33+00:00" }, { "name": "egulias/email-validator", @@ -926,20 +971,20 @@ }, { "name": "ezyang/htmlpurifier", - "version": "v4.18.0", + "version": "v4.19.0", "source": { "type": "git", "url": "https://github.com/ezyang/htmlpurifier.git", - "reference": "cb56001e54359df7ae76dc522d08845dc741621b" + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/cb56001e54359df7ae76dc522d08845dc741621b", - "reference": "cb56001e54359df7ae76dc522d08845dc741621b", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/b287d2a16aceffbf6e0295559b39662612b77fcf", + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf", "shasum": "" }, "require": { - "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" }, "require-dev": { "cerdic/css-tidy": "^1.7 || ^2.0", @@ -981,37 +1026,37 @@ ], "support": { "issues": "https://github.com/ezyang/htmlpurifier/issues", - "source": "https://github.com/ezyang/htmlpurifier/tree/v4.18.0" + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.19.0" }, - "time": "2024-11-01T03:51:45+00:00" + "time": "2025-10-17T16:34:55+00:00" }, { "name": "fruitcake/php-cors", - "version": "v1.3.0", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/fruitcake/php-cors.git", - "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b" + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/3d158f36e7875e2f040f37bc0573956240a5a38b", - "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", "shasum": "" }, "require": { - "php": "^7.4|^8.0", - "symfony/http-foundation": "^4.4|^5.4|^6|^7" + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" }, "require-dev": { - "phpstan/phpstan": "^1.4", + "phpstan/phpstan": "^2", "phpunit/phpunit": "^9", - "squizlabs/php_codesniffer": "^3.5" + "squizlabs/php_codesniffer": "^4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2-dev" + "dev-master": "1.3-dev" } }, "autoload": { @@ -1042,7 +1087,7 @@ ], "support": { "issues": "https://github.com/fruitcake/php-cors/issues", - "source": "https://github.com/fruitcake/php-cors/tree/v1.3.0" + "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0" }, "funding": [ { @@ -1054,28 +1099,28 @@ "type": "github" } ], - "time": "2023-10-12T05:21:21+00:00" + "time": "2025-12-03T09:33:47+00:00" }, { "name": "graham-campbell/result-type", - "version": "v1.1.3", + "version": "v1.1.4", "source": { "type": "git", "url": "https://github.com/GrahamCampbell/Result-Type.git", - "reference": "3ba905c11371512af9d9bdd27d99b782216b6945" + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945", - "reference": "3ba905c11371512af9d9bdd27d99b782216b6945", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.3" + "phpoption/phpoption": "^1.9.5" }, "require-dev": { - "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" }, "type": "library", "autoload": { @@ -1104,7 +1149,7 @@ ], "support": { "issues": "https://github.com/GrahamCampbell/Result-Type/issues", - "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3" + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" }, "funding": [ { @@ -1116,29 +1161,30 @@ "type": "tidelift" } ], - "time": "2024-07-20T21:45:45+00:00" + "time": "2025-12-27T19:43:20+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "7.10.0", + "version": "7.12.3", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" + "reference": "9aa17bcdd777ee31df9fc83c337ca4ca2340def3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", - "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/9aa17bcdd777ee31df9fc83c337ca4ca2340def3", + "reference": "9aa17bcdd777ee31df9fc83c337ca4ca2340def3", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^2.3", - "guzzlehttp/psr7": "^2.8", + "guzzlehttp/promises": "^2.5", + "guzzlehttp/psr7": "^2.12.3", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", - "symfony/deprecation-contracts": "^2.2 || ^3.0" + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.25" }, "provide": { "psr/http-client-implementation": "1.0" @@ -1147,8 +1193,9 @@ "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", "guzzle/client-integration-tests": "3.0.2", + "guzzlehttp/test-server": "^0.5.1", "php-http/message-factory": "^1.1", - "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { @@ -1226,7 +1273,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.10.0" + "source": "https://github.com/guzzle/guzzle/tree/7.12.3" }, "funding": [ { @@ -1242,28 +1289,29 @@ "type": "tidelift" } ], - "time": "2025-08-23T22:36:01+00:00" + "time": "2026-06-23T15:29:02+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.3.0", + "version": "2.5.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "481557b130ef3790cf82b713667b43030dc9c957" + "reference": "4360e982f87f5f258bf872d094647791db2f4c8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", - "reference": "481557b130ef3790cf82b713667b43030dc9c957", + "url": "https://api.github.com/repos/guzzle/promises/zipball/4360e982f87f5f258bf872d094647791db2f4c8e", + "reference": "4360e982f87f5f258bf872d094647791db2f4c8e", "shasum": "" }, "require": { - "php": "^7.2.5 || ^8.0" + "php": "^7.2.5 || ^8.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.44 || ^9.6.25" + "phpunit/phpunit": "^8.5.52 || ^9.6.34" }, "type": "library", "extra": { @@ -1309,7 +1357,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.3.0" + "source": "https://github.com/guzzle/promises/tree/2.5.0" }, "funding": [ { @@ -1325,27 +1373,29 @@ "type": "tidelift" } ], - "time": "2025-08-22T14:34:08+00:00" + "time": "2026-06-02T12:23:43+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.8.0", + "version": "2.12.3", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "21dc724a0583619cd1652f673303492272778051" + "reference": "7ec62dc3f44aa218487dbed81a9bf9bc647be55d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", - "reference": "21dc724a0583619cd1652f673303492272778051", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7ec62dc3f44aa218487dbed81a9bf9bc647be55d", + "reference": "7ec62dc3f44aa218487dbed81a9bf9bc647be55d", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", "psr/http-factory": "^1.0", "psr/http-message": "^1.1 || ^2.0", - "ralouphie/getallheaders": "^3.0" + "ralouphie/getallheaders": "^3.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.25" }, "provide": { "psr/http-factory-implementation": "1.0", @@ -1353,8 +1403,9 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "http-interop/http-factory-tests": "0.9.0", - "phpunit/phpunit": "^8.5.44 || ^9.6.25" + "http-interop/http-factory-tests": "1.1.0", + "jshttp/mime-db": "1.54.0.1", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -1425,7 +1476,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.8.0" + "source": "https://github.com/guzzle/psr7/tree/2.12.3" }, "funding": [ { @@ -1441,29 +1492,29 @@ "type": "tidelift" } ], - "time": "2025-08-23T21:21:41+00:00" + "time": "2026-06-23T15:21:08+00:00" }, { "name": "guzzlehttp/uri-template", - "version": "v1.0.5", + "version": "v1.0.8", "source": { "type": "git", "url": "https://github.com/guzzle/uri-template.git", - "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1" + "reference": "9c19128923b05a5d7355e5d2318d7808b7e33bbd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/uri-template/zipball/4f4bbd4e7172148801e76e3decc1e559bdee34e1", - "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/9c19128923b05a5d7355e5d2318d7808b7e33bbd", + "reference": "9c19128923b05a5d7355e5d2318d7808b7e33bbd", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", - "symfony/polyfill-php80": "^1.24" + "symfony/polyfill-php80": "^1.25" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.44 || ^9.6.25", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", "uri-template/tests": "1.0.0" }, "type": "library", @@ -1511,7 +1562,7 @@ ], "support": { "issues": "https://github.com/guzzle/uri-template/issues", - "source": "https://github.com/guzzle/uri-template/tree/v1.0.5" + "source": "https://github.com/guzzle/uri-template/tree/v1.0.8" }, "funding": [ { @@ -1527,29 +1578,29 @@ "type": "tidelift" } ], - "time": "2025-08-22T14:27:06+00:00" + "time": "2026-06-23T13:02:23+00:00" }, { "name": "lab404/laravel-impersonate", - "version": "1.7.7", + "version": "1.7.8", "source": { "type": "git", "url": "https://github.com/404labfr/laravel-impersonate.git", - "reference": "5033f3433a55ca8bb2cc3e4a018a39dd8a327a9f" + "reference": "0008a39da8914cc946b6a5ed211230708ee736b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/404labfr/laravel-impersonate/zipball/5033f3433a55ca8bb2cc3e4a018a39dd8a327a9f", - "reference": "5033f3433a55ca8bb2cc3e4a018a39dd8a327a9f", + "url": "https://api.github.com/repos/404labfr/laravel-impersonate/zipball/0008a39da8914cc946b6a5ed211230708ee736b3", + "reference": "0008a39da8914cc946b6a5ed211230708ee736b3", "shasum": "" }, "require": { - "laravel/framework": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0 | ^12.0", + "laravel/framework": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0 | ^12.0 | ^13.0", "php": "^7.2 | ^8.0" }, "require-dev": { "mockery/mockery": "^1.3.3", - "orchestra/testbench": "^4.0 | ^5.0 | ^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0", + "orchestra/testbench": "^4.0 | ^5.0 | ^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0", "phpunit/phpunit": "^7.5 | ^8.0 | ^9.0 | ^10.0 | ^11.0" }, "type": "library", @@ -1592,37 +1643,36 @@ ], "support": { "issues": "https://github.com/404labfr/laravel-impersonate/issues", - "source": "https://github.com/404labfr/laravel-impersonate/tree/1.7.7" + "source": "https://github.com/404labfr/laravel-impersonate/tree/1.7.8" }, - "time": "2025-02-24T16:18:38+00:00" + "time": "2026-03-17T15:24:14+00:00" }, { "name": "laravel/fortify", - "version": "v1.30.0", + "version": "v1.37.2", "source": { "type": "git", "url": "https://github.com/laravel/fortify.git", - "reference": "005f4d535ae671312d267d942b964807fc0ef6f8" + "reference": "5d4b6a53527edd19ecc4f13e8e74ec91bdefab0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/fortify/zipball/005f4d535ae671312d267d942b964807fc0ef6f8", - "reference": "005f4d535ae671312d267d942b964807fc0ef6f8", + "url": "https://api.github.com/repos/laravel/fortify/zipball/5d4b6a53527edd19ecc4f13e8e74ec91bdefab0c", + "reference": "5d4b6a53527edd19ecc4f13e8e74ec91bdefab0c", "shasum": "" }, "require": { "bacon/bacon-qr-code": "^3.0", "ext-json": "*", - "illuminate/support": "^10.0|^11.0|^12.0", - "php": "^8.1", - "pragmarx/google2fa": "^8.0", - "symfony/console": "^6.0|^7.0" + "illuminate/console": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "laravel/passkeys": "^0.2.0", + "php": "^8.2", + "pragmarx/google2fa": "^9.0" }, "require-dev": { - "mockery/mockery": "^1.0", - "orchestra/testbench": "^8.16|^9.0|^10.0", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^10.4|^11.3" + "orchestra/testbench": "^9.15|^10.8|^11.0", + "phpstan/phpstan": "^1.10" }, "type": "library", "extra": { @@ -1659,20 +1709,20 @@ "issues": "https://github.com/laravel/fortify/issues", "source": "https://github.com/laravel/fortify" }, - "time": "2025-08-29T20:15:47+00:00" + "time": "2026-05-15T22:59:10+00:00" }, { "name": "laravel/framework", - "version": "v12.29.0", + "version": "v12.62.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "a9e4c73086f5ba38383e9c1d74b84fe46aac730b" + "reference": "f7e61eb1e0e06a38996802b769bce9127aec227c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/a9e4c73086f5ba38383e9c1d74b84fe46aac730b", - "reference": "a9e4c73086f5ba38383e9c1d74b84fe46aac730b", + "url": "https://api.github.com/repos/laravel/framework/zipball/f7e61eb1e0e06a38996802b769bce9127aec227c", + "reference": "f7e61eb1e0e06a38996802b769bce9127aec227c", "shasum": "" }, "require": { @@ -1693,14 +1743,13 @@ "guzzlehttp/uri-template": "^1.0", "laravel/prompts": "^0.3.0", "laravel/serializable-closure": "^1.3|^2.0", - "league/commonmark": "^2.7", + "league/commonmark": "^2.8.1", "league/flysystem": "^3.25.1", "league/flysystem-local": "^3.25.1", "league/uri": "^7.5.1", "monolog/monolog": "^3.0", "nesbot/carbon": "^3.8.4", "nunomaduro/termwind": "^2.0", - "phiki/phiki": "v2.0.0", "php": "^8.2", "psr/container": "^1.1.1|^2.0.1", "psr/log": "^1.0|^2.0|^3.0", @@ -1714,8 +1763,8 @@ "symfony/mailer": "^7.2.0", "symfony/mime": "^7.2.0", "symfony/polyfill-php83": "^1.33", - "symfony/polyfill-php84": "^1.33", - "symfony/polyfill-php85": "^1.33", + "symfony/polyfill-php84": "^1.34", + "symfony/polyfill-php85": "^1.34", "symfony/process": "^7.2.0", "symfony/routing": "^7.2.0", "symfony/uid": "^7.2.0", @@ -1761,6 +1810,7 @@ "illuminate/process": "self.version", "illuminate/queue": "self.version", "illuminate/redis": "self.version", + "illuminate/reflection": "self.version", "illuminate/routing": "self.version", "illuminate/session": "self.version", "illuminate/support": "self.version", @@ -1785,13 +1835,13 @@ "league/flysystem-sftp-v3": "^3.25.1", "mockery/mockery": "^1.6.10", "opis/json-schema": "^2.4.1", - "orchestra/testbench-core": "^10.6.5", + "orchestra/testbench-core": "^10.9.0", "pda/pheanstalk": "^5.0.6|^7.0.0", "php-http/discovery": "^1.15", - "phpstan/phpstan": "^2.0", + "phpstan/phpstan": "^2.1.41", "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", "predis/predis": "^2.3|^3.0", - "resend/resend-php": "^0.10.0", + "resend/resend-php": "^0.10.0|^1.0", "symfony/cache": "^7.2.0", "symfony/http-client": "^7.2.0", "symfony/psr-http-message-bridge": "^7.2.0", @@ -1825,7 +1875,7 @@ "predis/predis": "Required to use the predis connector (^2.3|^3.0).", "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", - "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0).", + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0|^1.0).", "symfony/cache": "Required to PSR-6 cache bridge (^7.2).", "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.2).", @@ -1847,6 +1897,7 @@ "src/Illuminate/Filesystem/functions.php", "src/Illuminate/Foundation/helpers.php", "src/Illuminate/Log/functions.php", + "src/Illuminate/Reflection/helpers.php", "src/Illuminate/Support/functions.php", "src/Illuminate/Support/helpers.php" ], @@ -1855,7 +1906,8 @@ "Illuminate\\Support\\": [ "src/Illuminate/Macroable/", "src/Illuminate/Collections/", - "src/Illuminate/Conditionable/" + "src/Illuminate/Conditionable/", + "src/Illuminate/Reflection/" ] } }, @@ -1879,42 +1931,42 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-09-16T14:15:03+00:00" + "time": "2026-06-09T13:50:13+00:00" }, { "name": "laravel/horizon", - "version": "v5.36.0", + "version": "v5.47.2", "source": { "type": "git", "url": "https://github.com/laravel/horizon.git", - "reference": "eccc804c9da78064c97a8f506bb148f05c816409" + "reference": "a6ac142293ad02db4d7cccb961dd32f56ef1462d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/horizon/zipball/eccc804c9da78064c97a8f506bb148f05c816409", - "reference": "eccc804c9da78064c97a8f506bb148f05c816409", + "url": "https://api.github.com/repos/laravel/horizon/zipball/a6ac142293ad02db4d7cccb961dd32f56ef1462d", + "reference": "a6ac142293ad02db4d7cccb961dd32f56ef1462d", "shasum": "" }, "require": { "ext-json": "*", "ext-pcntl": "*", "ext-posix": "*", - "illuminate/contracts": "^9.21|^10.0|^11.0|^12.0", - "illuminate/queue": "^9.21|^10.0|^11.0|^12.0", - "illuminate/support": "^9.21|^10.0|^11.0|^12.0", + "illuminate/contracts": "^9.21|^10.0|^11.0|^12.0|^13.0", + "illuminate/queue": "^9.21|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^9.21|^10.0|^11.0|^12.0|^13.0", + "laravel/sentinel": "^1.0", "nesbot/carbon": "^2.17|^3.0", "php": "^8.0", "ramsey/uuid": "^4.0", - "symfony/console": "^6.0|^7.0", - "symfony/error-handler": "^6.0|^7.0", + "symfony/console": "^6.0|^7.0|^8.0", + "symfony/error-handler": "^6.0|^7.0|^8.0", "symfony/polyfill-php83": "^1.28", - "symfony/process": "^6.0|^7.0" + "symfony/process": "^6.0|^7.0|^8.0" }, "require-dev": { "mockery/mockery": "^1.0", - "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", + "orchestra/testbench": "^7.56|^8.37|^9.16|^10.9|^11.0", "phpstan/phpstan": "^1.10|^2.0", - "phpunit/phpunit": "^9.0|^10.4|^11.5|^12.0", "predis/predis": "^1.1|^2.0|^3.0" }, "suggest": { @@ -1957,40 +2009,108 @@ ], "support": { "issues": "https://github.com/laravel/horizon/issues", - "source": "https://github.com/laravel/horizon/tree/v5.36.0" + "source": "https://github.com/laravel/horizon/tree/v5.47.2" }, - "time": "2025-10-10T13:44:39+00:00" + "time": "2026-06-03T15:11:37+00:00" }, { - "name": "laravel/prompts", - "version": "v0.3.6", + "name": "laravel/passkeys", + "version": "v0.2.1", "source": { "type": "git", - "url": "https://github.com/laravel/prompts.git", - "reference": "86a8b692e8661d0fb308cec64f3d176821323077" + "url": "https://github.com/laravel/passkeys-server.git", + "reference": "a76656ada41b2b4a591f075eddae5ddc67e8ab9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/86a8b692e8661d0fb308cec64f3d176821323077", - "reference": "86a8b692e8661d0fb308cec64f3d176821323077", + "url": "https://api.github.com/repos/laravel/passkeys-server/zipball/a76656ada41b2b4a591f075eddae5ddc67e8ab9c", + "reference": "a76656ada41b2b4a591f075eddae5ddc67e8ab9c", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^11.0|^12.0|^13.0", + "illuminate/database": "^11.0|^12.0|^13.0", + "illuminate/http": "^11.0|^12.0|^13.0", + "illuminate/routing": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "php": "^8.2", + "web-auth/webauthn-lib": "5.3.x" + }, + "require-dev": { + "laravel/pint": "^1.28.0", + "orchestra/testbench": "^9.0|^10.0|^11.0", + "pestphp/pest": "^3.0|^4.0", + "phpstan/phpstan": "^2.0", + "rector/rector": "^2.3" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Passkeys\\PasskeysServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Passkeys\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Passwordless authentication using WebAuthn/passkeys for Laravel", + "homepage": "https://github.com/laravel/passkeys-server", + "keywords": [ + "Authentication", + "Passwordless", + "laravel", + "passkeys", + "webauthn" + ], + "support": { + "issues": "https://github.com/laravel/passkeys-server/issues", + "source": "https://github.com/laravel/passkeys-server" + }, + "time": "2026-05-18T16:26:00+00:00" + }, + { + "name": "laravel/prompts", + "version": "v0.3.18", + "source": { + "type": "git", + "url": "https://github.com/laravel/prompts.git", + "reference": "a19af51bb144bf87f08397921fa619f85c7d4e72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/prompts/zipball/a19af51bb144bf87f08397921fa619f85c7d4e72", + "reference": "a19af51bb144bf87f08397921fa619f85c7d4e72", "shasum": "" }, "require": { "composer-runtime-api": "^2.2", "ext-mbstring": "*", "php": "^8.1", - "symfony/console": "^6.2|^7.0" + "symfony/console": "^6.2|^7.0|^8.0" }, "conflict": { "illuminate/console": ">=10.17.0 <10.25.0", "laravel/framework": ">=10.17.0 <10.25.0" }, "require-dev": { - "illuminate/collections": "^10.0|^11.0|^12.0", + "illuminate/collections": "^10.0|^11.0|^12.0|^13.0", "mockery/mockery": "^1.5", - "pestphp/pest": "^2.3|^3.4", - "phpstan/phpstan": "^1.11", - "phpstan/phpstan-mockery": "^1.1" + "pestphp/pest": "^2.3|^3.4|^4.0", + "phpstan/phpstan": "^1.12.28", + "phpstan/phpstan-mockery": "^1.1.3" }, "suggest": { "ext-pcntl": "Required for the spinner to be animated." @@ -2016,38 +2136,37 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.6" + "source": "https://github.com/laravel/prompts/tree/v0.3.18" }, - "time": "2025-07-07T14:17:42+00:00" + "time": "2026-05-19T00:47:18+00:00" }, { "name": "laravel/sanctum", - "version": "v4.2.0", + "version": "v4.3.2", "source": { "type": "git", "url": "https://github.com/laravel/sanctum.git", - "reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677" + "reference": "2a9bccc18e9907808e0018dd15fa643937886b1e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sanctum/zipball/fd6df4f79f48a72992e8d29a9c0ee25422a0d677", - "reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/2a9bccc18e9907808e0018dd15fa643937886b1e", + "reference": "2a9bccc18e9907808e0018dd15fa643937886b1e", "shasum": "" }, "require": { "ext-json": "*", - "illuminate/console": "^11.0|^12.0", - "illuminate/contracts": "^11.0|^12.0", - "illuminate/database": "^11.0|^12.0", - "illuminate/support": "^11.0|^12.0", + "illuminate/console": "^11.0|^12.0|^13.0", + "illuminate/contracts": "^11.0|^12.0|^13.0", + "illuminate/database": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", "php": "^8.2", - "symfony/console": "^7.0" + "symfony/console": "^7.0|^8.0" }, "require-dev": { "mockery/mockery": "^1.6", - "orchestra/testbench": "^9.0|^10.0", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^11.3" + "orchestra/testbench": "^9.15|^10.8|^11.0", + "phpstan/phpstan": "^1.10" }, "type": "library", "extra": { @@ -2082,31 +2201,87 @@ "issues": "https://github.com/laravel/sanctum/issues", "source": "https://github.com/laravel/sanctum" }, - "time": "2025-07-09T19:45:24+00:00" + "time": "2026-04-30T11:46:25+00:00" }, { - "name": "laravel/serializable-closure", - "version": "v2.0.4", + "name": "laravel/sentinel", + "version": "v1.1.0", "source": { "type": "git", - "url": "https://github.com/laravel/serializable-closure.git", - "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841" + "url": "https://github.com/laravel/sentinel.git", + "reference": "972d9885d9d14312a118e9565c4e6ecc5e751ea1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b352cf0534aa1ae6b4d825d1e762e35d43f8a841", - "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841", + "url": "https://api.github.com/repos/laravel/sentinel/zipball/972d9885d9d14312a118e9565c4e6ecc5e751ea1", + "reference": "972d9885d9d14312a118e9565c4e6ecc5e751ea1", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/container": "^8.37|^9.0|^10.0|^11.0|^12.0|^13.0", + "php": "^8.0" + }, + "require-dev": { + "laravel/pint": "^1.27", + "orchestra/testbench": "^6.47.1|^7.56|^8.37|^9.16|^10.9|^11.0", + "phpstan/phpstan": "^2.1.33" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sentinel\\SentinelServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sentinel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Mior Muhammad Zaki", + "email": "mior@laravel.com" + } + ], + "support": { + "source": "https://github.com/laravel/sentinel/tree/v1.1.0" + }, + "time": "2026-03-24T14:03:38+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.13", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b566ee0dd251f3c4078bed003a7ce015f5ea6dce", + "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce", "shasum": "" }, "require": { "php": "^8.1" }, "require-dev": { - "illuminate/support": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", "nesbot/carbon": "^2.67|^3.0", - "pestphp/pest": "^2.36|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", "phpstan/phpstan": "^2.0", - "symfony/var-dumper": "^6.2.0|^7.0.0" + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" }, "type": "library", "extra": { @@ -2143,20 +2318,20 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2025-03-19T13:51:03+00:00" + "time": "2026-04-16T14:03:50+00:00" }, { "name": "laravel/tinker", - "version": "v2.10.1", + "version": "v2.11.1", "source": { "type": "git", "url": "https://github.com/laravel/tinker.git", - "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3" + "reference": "c9f80cc835649b5c1842898fb043f8cc098dd741" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/tinker/zipball/22177cc71807d38f2810c6204d8f7183d88a57d3", - "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3", + "url": "https://api.github.com/repos/laravel/tinker/zipball/c9f80cc835649b5c1842898fb043f8cc098dd741", + "reference": "c9f80cc835649b5c1842898fb043f8cc098dd741", "shasum": "" }, "require": { @@ -2165,7 +2340,7 @@ "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", "php": "^7.2.5|^8.0", "psy/psysh": "^0.11.1|^0.12.0", - "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0" + "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0|^8.0" }, "require-dev": { "mockery/mockery": "~1.3.3|^1.4.2", @@ -2207,22 +2382,22 @@ ], "support": { "issues": "https://github.com/laravel/tinker/issues", - "source": "https://github.com/laravel/tinker/tree/v2.10.1" + "source": "https://github.com/laravel/tinker/tree/v2.11.1" }, - "time": "2025-01-27T14:24:01+00:00" + "time": "2026-02-06T14:12:35+00:00" }, { "name": "league/commonmark", - "version": "2.7.1", + "version": "2.8.2", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "10732241927d3971d28e7ea7b5712721fa2296ca" + "reference": "59fb075d2101740c337c7216e3f32b36c204218b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/10732241927d3971d28e7ea7b5712721fa2296ca", - "reference": "10732241927d3971d28e7ea7b5712721fa2296ca", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/59fb075d2101740c337c7216e3f32b36c204218b", + "reference": "59fb075d2101740c337c7216e3f32b36c204218b", "shasum": "" }, "require": { @@ -2247,9 +2422,9 @@ "phpstan/phpstan": "^1.8.2", "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", "scrutinizer/ocular": "^1.8.1", - "symfony/finder": "^5.3 | ^6.0 | ^7.0", - "symfony/process": "^5.4 | ^6.0 | ^7.0", - "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0", + "symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0 || ^8.0", "unleashedtech/php-coding-standard": "^3.1.1", "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" }, @@ -2259,7 +2434,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.8-dev" + "dev-main": "2.9-dev" } }, "autoload": { @@ -2316,7 +2491,7 @@ "type": "tidelift" } ], - "time": "2025-07-20T12:47:49+00:00" + "time": "2026-03-19T13:16:38+00:00" }, { "name": "league/config", @@ -2402,16 +2577,16 @@ }, { "name": "league/flysystem", - "version": "3.30.0", + "version": "3.35.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "2203e3151755d874bb2943649dae1eb8533ac93e" + "reference": "d0a405f03d980461e4b6e08cfed2c162650f9f6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/2203e3151755d874bb2943649dae1eb8533ac93e", - "reference": "2203e3151755d874bb2943649dae1eb8533ac93e", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/d0a405f03d980461e4b6e08cfed2c162650f9f6b", + "reference": "d0a405f03d980461e4b6e08cfed2c162650f9f6b", "shasum": "" }, "require": { @@ -2479,22 +2654,22 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.30.0" + "source": "https://github.com/thephpleague/flysystem/tree/3.35.0" }, - "time": "2025-06-25T13:29:59+00:00" + "time": "2026-06-22T20:12:06+00:00" }, { "name": "league/flysystem-local", - "version": "3.30.0", + "version": "3.31.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-local.git", - "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10" + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/6691915f77c7fb69adfb87dcd550052dc184ee10", - "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079", "shasum": "" }, "require": { @@ -2528,9 +2703,9 @@ "local" ], "support": { - "source": "https://github.com/thephpleague/flysystem-local/tree/3.30.0" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.31.0" }, - "time": "2025-05-21T10:34:19+00:00" + "time": "2026-01-23T15:30:45+00:00" }, { "name": "league/mime-type-detection", @@ -2590,33 +2765,38 @@ }, { "name": "league/uri", - "version": "7.5.1", + "version": "7.8.1", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "81fb5145d2644324614cc532b28efd0215bda430" + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", - "reference": "81fb5145d2644324614cc532b28efd0215bda430", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.5", - "php": "^8.1" + "league/uri-interfaces": "^7.8.1", + "php": "^8.1", + "psr/http-factory": "^1" }, "conflict": { "league/uri-schemes": "^1.0" }, "suggest": { "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", "ext-fileinfo": "to create Data URI from file contennts", "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", - "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", - "league/uri-components": "Needed to easily manipulate URI objects components", + "ext-uri": "to use the PHP native URI class", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-components": "to provide additional tools to manipulate URI objects components", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -2644,6 +2824,7 @@ "description": "URI manipulation library", "homepage": "https://uri.thephpleague.com", "keywords": [ + "URN", "data-uri", "file-uri", "ftp", @@ -2656,9 +2837,11 @@ "psr-7", "query-string", "querystring", + "rfc2141", "rfc3986", "rfc3987", "rfc6570", + "rfc8141", "uri", "uri-template", "url", @@ -2668,7 +2851,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.5.1" + "source": "https://github.com/thephpleague/uri/tree/7.8.1" }, "funding": [ { @@ -2676,26 +2859,25 @@ "type": "github" } ], - "time": "2024-12-08T08:40:02+00:00" + "time": "2026-03-15T20:22:25+00:00" }, { "name": "league/uri-interfaces", - "version": "7.5.0", + "version": "7.8.1", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", "shasum": "" }, "require": { "ext-filter": "*", "php": "^8.1", - "psr/http-factory": "^1", "psr/http-message": "^1.1 || ^2.0" }, "suggest": { @@ -2703,6 +2885,7 @@ "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -2727,7 +2910,7 @@ "homepage": "https://nyamsprod.com" } ], - "description": "Common interfaces and classes for URI representation and interaction", + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", "homepage": "https://uri.thephpleague.com", "keywords": [ "data-uri", @@ -2752,7 +2935,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.5.0" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" }, "funding": [ { @@ -2760,33 +2943,33 @@ "type": "github" } ], - "time": "2024-12-08T08:18:47+00:00" + "time": "2026-03-08T20:05:35+00:00" }, { "name": "maatwebsite/excel", - "version": "3.1.67", + "version": "3.1.69", "source": { "type": "git", "url": "https://github.com/SpartnerNL/Laravel-Excel.git", - "reference": "e508e34a502a3acc3329b464dad257378a7edb4d" + "reference": "ae5d65b7c9a2fac43bff4d44f796ac95d7a8e760" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/SpartnerNL/Laravel-Excel/zipball/e508e34a502a3acc3329b464dad257378a7edb4d", - "reference": "e508e34a502a3acc3329b464dad257378a7edb4d", + "url": "https://api.github.com/repos/SpartnerNL/Laravel-Excel/zipball/ae5d65b7c9a2fac43bff4d44f796ac95d7a8e760", + "reference": "ae5d65b7c9a2fac43bff4d44f796ac95d7a8e760", "shasum": "" }, "require": { "composer/semver": "^3.3", "ext-json": "*", - "illuminate/support": "5.8.*||^6.0||^7.0||^8.0||^9.0||^10.0||^11.0||^12.0", + "illuminate/support": "5.8.*||^6.0||^7.0||^8.0||^9.0||^10.0||^11.0||^12.0||^13.0", "php": "^7.0||^8.0", - "phpoffice/phpspreadsheet": "^1.30.0", + "phpoffice/phpspreadsheet": "^1.30.4", "psr/simple-cache": "^1.0||^2.0||^3.0" }, "require-dev": { - "laravel/scout": "^7.0||^8.0||^9.0||^10.0", - "orchestra/testbench": "^6.0||^7.0||^8.0||^9.0||^10.0", + "laravel/scout": "^7.0||^8.0||^9.0||^10.0||^11.0", + "orchestra/testbench": "^6.0||^7.0||^8.0||^9.0||^10.0||^11.0", "predis/predis": "^1.1" }, "type": "library", @@ -2829,7 +3012,7 @@ ], "support": { "issues": "https://github.com/SpartnerNL/Laravel-Excel/issues", - "source": "https://github.com/SpartnerNL/Laravel-Excel/tree/3.1.67" + "source": "https://github.com/SpartnerNL/Laravel-Excel/tree/3.1.69" }, "funding": [ { @@ -2841,20 +3024,20 @@ "type": "github" } ], - "time": "2025-08-26T09:13:16+00:00" + "time": "2026-04-30T20:03:58+00:00" }, { "name": "maennchen/zipstream-php", - "version": "3.2.0", + "version": "3.2.2", "source": { "type": "git", "url": "https://github.com/maennchen/ZipStream-PHP.git", - "reference": "9712d8fa4cdf9240380b01eb4be55ad8dcf71416" + "reference": "77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/9712d8fa4cdf9240380b01eb4be55ad8dcf71416", - "reference": "9712d8fa4cdf9240380b01eb4be55ad8dcf71416", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e", + "reference": "77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e", "shasum": "" }, "require": { @@ -2865,7 +3048,7 @@ "require-dev": { "brianium/paratest": "^7.7", "ext-zip": "*", - "friendsofphp/php-cs-fixer": "^3.16", + "friendsofphp/php-cs-fixer": "^3.86", "guzzlehttp/guzzle": "^7.5", "mikey179/vfsstream": "^1.6", "php-coveralls/php-coveralls": "^2.5", @@ -2911,7 +3094,7 @@ ], "support": { "issues": "https://github.com/maennchen/ZipStream-PHP/issues", - "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.0" + "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.2" }, "funding": [ { @@ -2919,7 +3102,7 @@ "type": "github" } ], - "time": "2025-07-17T11:15:13+00:00" + "time": "2026-04-11T18:38:28+00:00" }, { "name": "markbaker/complex", @@ -3030,16 +3213,16 @@ }, { "name": "masterminds/html5", - "version": "2.10.0", + "version": "2.10.1", "source": { "type": "git", "url": "https://github.com/Masterminds/html5-php.git", - "reference": "fcf91eb64359852f00d921887b219479b4f21251" + "reference": "fd5018f6815fff903946d0564977b44ce8010e29" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251", - "reference": "fcf91eb64359852f00d921887b219479b4f21251", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fd5018f6815fff903946d0564977b44ce8010e29", + "reference": "fd5018f6815fff903946d0564977b44ce8010e29", "shasum": "" }, "require": { @@ -3047,7 +3230,7 @@ "php": ">=5.3.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9" + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9 || ^10" }, "type": "library", "extra": { @@ -3091,22 +3274,22 @@ ], "support": { "issues": "https://github.com/Masterminds/html5-php/issues", - "source": "https://github.com/Masterminds/html5-php/tree/2.10.0" + "source": "https://github.com/Masterminds/html5-php/tree/2.10.1" }, - "time": "2025-07-25T09:04:22+00:00" + "time": "2026-06-23T18:43:15+00:00" }, { "name": "monolog/monolog", - "version": "3.9.0", + "version": "3.10.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", - "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", "shasum": "" }, "require": { @@ -3124,7 +3307,7 @@ "graylog2/gelf-php": "^1.4.2 || ^2.0", "guzzlehttp/guzzle": "^7.4.5", "guzzlehttp/psr7": "^2.2", - "mongodb/mongodb": "^1.8", + "mongodb/mongodb": "^1.8 || ^2.0", "php-amqplib/php-amqplib": "~2.4 || ^3", "php-console/php-console": "^3.1.8", "phpstan/phpstan": "^2", @@ -3184,7 +3367,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/3.9.0" + "source": "https://github.com/Seldaek/monolog/tree/3.10.0" }, "funding": [ { @@ -3196,20 +3379,20 @@ "type": "tidelift" } ], - "time": "2025-03-24T10:02:05+00:00" + "time": "2026-01-02T08:56:05+00:00" }, { "name": "nesbot/carbon", - "version": "3.10.3", + "version": "3.13.0", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f" + "reference": "40f6618f052df16b545f626fbf9a878e6497d16a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f", - "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/40f6618f052df16b545f626fbf9a878e6497d16a", + "reference": "40f6618f052df16b545f626fbf9a878e6497d16a", "shasum": "" }, "require": { @@ -3217,9 +3400,9 @@ "ext-json": "*", "php": "^8.1", "psr/clock": "^1.0", - "symfony/clock": "^6.3.12 || ^7.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", "symfony/polyfill-mbstring": "^1.0", - "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0" + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" }, "provide": { "psr/clock-implementation": "1.0" @@ -3233,7 +3416,7 @@ "phpstan/extension-installer": "^1.4.3", "phpstan/phpstan": "^2.1.22", "phpunit/phpunit": "^10.5.53", - "squizlabs/php_codesniffer": "^3.13.4" + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0" }, "bin": [ "bin/carbon" @@ -3276,14 +3459,14 @@ } ], "description": "An API extension for DateTime that supports 281 different languages.", - "homepage": "https://carbon.nesbot.com", + "homepage": "https://carbonphp.github.io/carbon/", "keywords": [ "date", "datetime", "time" ], "support": { - "docs": "https://carbon.nesbot.com/docs", + "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html", "issues": "https://github.com/CarbonPHP/carbon/issues", "source": "https://github.com/CarbonPHP/carbon" }, @@ -3301,29 +3484,31 @@ "type": "tidelift" } ], - "time": "2025-09-06T13:39:36+00:00" + "time": "2026-06-18T13:49:15+00:00" }, { "name": "nette/schema", - "version": "v1.3.2", + "version": "v1.3.5", "source": { "type": "git", "url": "https://github.com/nette/schema.git", - "reference": "da801d52f0354f70a638673c4a0f04e16529431d" + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/schema/zipball/da801d52f0354f70a638673c4a0f04e16529431d", - "reference": "da801d52f0354f70a638673c4a0f04e16529431d", + "url": "https://api.github.com/repos/nette/schema/zipball/f0ab1a3cda782dbc5da270d28545236aa80c4002", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002", "shasum": "" }, "require": { "nette/utils": "^4.0", - "php": "8.1 - 8.4" + "php": "8.1 - 8.5" }, "require-dev": { - "nette/tester": "^2.5.2", - "phpstan/phpstan-nette": "^1.0", + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.6", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1.39@stable", "tracy/tracy": "^2.8" }, "type": "library", @@ -3333,6 +3518,9 @@ } }, "autoload": { + "psr-4": { + "Nette\\": "src" + }, "classmap": [ "src/" ] @@ -3361,26 +3549,26 @@ ], "support": { "issues": "https://github.com/nette/schema/issues", - "source": "https://github.com/nette/schema/tree/v1.3.2" + "source": "https://github.com/nette/schema/tree/v1.3.5" }, - "time": "2024-10-06T23:10:23+00:00" + "time": "2026-02-23T03:47:12+00:00" }, { "name": "nette/utils", - "version": "v4.0.8", + "version": "v4.1.4", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede" + "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/c930ca4e3cf4f17dcfb03037703679d2396d2ede", - "reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede", + "url": "https://api.github.com/repos/nette/utils/zipball/7da6c396d7ebe142bc857c20479d5e70a5e1aac7", + "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7", "shasum": "" }, "require": { - "php": "8.0 - 8.5" + "php": "8.2 - 8.5" }, "conflict": { "nette/finder": "<3", @@ -3388,8 +3576,10 @@ }, "require-dev": { "jetbrains/phpstorm-attributes": "^1.2", + "nette/phpstan-rules": "^1.0", "nette/tester": "^2.5", - "phpstan/phpstan-nette": "^2.0@stable", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1@stable", "tracy/tracy": "^2.9" }, "suggest": { @@ -3403,7 +3593,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "4.1-dev" } }, "autoload": { @@ -3450,22 +3640,22 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.0.8" + "source": "https://github.com/nette/utils/tree/v4.1.4" }, - "time": "2025-08-06T21:43:34+00:00" + "time": "2026-05-11T20:49:54+00:00" }, { "name": "nikic/php-parser", - "version": "v5.6.1", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", - "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -3508,37 +3698,37 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2025-08-13T20:13:15+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "nunomaduro/termwind", - "version": "v2.3.1", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/nunomaduro/termwind.git", - "reference": "dfa08f390e509967a15c22493dc0bac5733d9123" + "reference": "712a31b768f5daea284c2169a7d227031001b9a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/dfa08f390e509967a15c22493dc0bac5733d9123", - "reference": "dfa08f390e509967a15c22493dc0bac5733d9123", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/712a31b768f5daea284c2169a7d227031001b9a8", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8", "shasum": "" }, "require": { "ext-mbstring": "*", "php": "^8.2", - "symfony/console": "^7.2.6" + "symfony/console": "^7.4.4 || ^8.0.4" }, "require-dev": { - "illuminate/console": "^11.44.7", - "laravel/pint": "^1.22.0", + "illuminate/console": "^11.47.0", + "laravel/pint": "^1.27.1", "mockery/mockery": "^1.6.12", - "pestphp/pest": "^2.36.0 || ^3.8.2", - "phpstan/phpstan": "^1.12.25", + "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.3.2", + "phpstan/phpstan": "^1.12.32", "phpstan/phpstan-strict-rules": "^1.6.2", - "symfony/var-dumper": "^7.2.6", + "symfony/var-dumper": "^7.3.5 || ^8.0.4", "thecodingmachine/phpstan-strict-rules": "^1.0.0" }, "type": "library", @@ -3570,7 +3760,7 @@ "email": "enunomaduro@gmail.com" } ], - "description": "Its like Tailwind CSS, but for the console.", + "description": "It's like Tailwind CSS, but for the console.", "keywords": [ "cli", "console", @@ -3581,7 +3771,7 @@ ], "support": { "issues": "https://github.com/nunomaduro/termwind/issues", - "source": "https://github.com/nunomaduro/termwind/tree/v2.3.1" + "source": "https://github.com/nunomaduro/termwind/tree/v2.4.0" }, "funding": [ { @@ -3597,20 +3787,20 @@ "type": "github" } ], - "time": "2025-05-08T08:14:37+00:00" + "time": "2026-02-16T23:10:27+00:00" }, { "name": "nwidart/laravel-modules", - "version": "v12.0.4", + "version": "v12.0.5", "source": { "type": "git", "url": "https://github.com/nWidart/laravel-modules.git", - "reference": "6e1f50de63366206b06ec53bbc823282977ddd06" + "reference": "5fe38e88b66394debeac785278a7e40eb81df51a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nWidart/laravel-modules/zipball/6e1f50de63366206b06ec53bbc823282977ddd06", - "reference": "6e1f50de63366206b06ec53bbc823282977ddd06", + "url": "https://api.github.com/repos/nWidart/laravel-modules/zipball/5fe38e88b66394debeac785278a7e40eb81df51a", + "reference": "5fe38e88b66394debeac785278a7e40eb81df51a", "shasum": "" }, "require": { @@ -3674,7 +3864,7 @@ ], "support": { "issues": "https://github.com/nWidart/laravel-modules/issues", - "source": "https://github.com/nWidart/laravel-modules/tree/v12.0.4" + "source": "https://github.com/nWidart/laravel-modules/tree/v12.0.5" }, "funding": [ { @@ -3686,28 +3876,30 @@ "type": "github" } ], - "time": "2025-06-29T09:23:53+00:00" + "time": "2026-03-19T18:16:17+00:00" }, { "name": "paragonie/constant_time_encoding", - "version": "v3.0.0", + "version": "v3.1.3", "source": { "type": "git", "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "df1e7fde177501eee2037dd159cf04f5f301a512" + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512", - "reference": "df1e7fde177501eee2037dd159cf04f5f301a512", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", "shasum": "" }, "require": { "php": "^8" }, "require-dev": { - "phpunit/phpunit": "^9", - "vimeo/psalm": "^4|^5" + "infection/infection": "^0", + "nikic/php-fuzzer": "^0", + "phpunit/phpunit": "^9|^10|^11", + "vimeo/psalm": "^4|^5|^6" }, "type": "library", "autoload": { @@ -3753,48 +3945,34 @@ "issues": "https://github.com/paragonie/constant_time_encoding/issues", "source": "https://github.com/paragonie/constant_time_encoding" }, - "time": "2024-05-08T12:36:18+00:00" + "time": "2025-09-24T15:06:41+00:00" }, { - "name": "phiki/phiki", - "version": "v2.0.0", + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", "source": { "type": "git", - "url": "https://github.com/phikiphp/phiki.git", - "reference": "461f6dd7e91dc3a95463b42f549ac7d0aab4702f" + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phikiphp/phiki/zipball/461f6dd7e91dc3a95463b42f549ac7d0aab4702f", - "reference": "461f6dd7e91dc3a95463b42f549ac7d0aab4702f", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", "shasum": "" }, "require": { - "ext-mbstring": "*", - "league/commonmark": "^2.5.3", - "php": "^8.2", - "psr/simple-cache": "^3.0" - }, - "require-dev": { - "illuminate/support": "^11.45", - "laravel/pint": "^1.18.1", - "orchestra/testbench": "^9.15", - "pestphp/pest": "^3.5.1", - "phpstan/extension-installer": "^1.4.3", - "phpstan/phpstan": "^2.0", - "symfony/var-dumper": "^7.1.6" + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { - "laravel": { - "providers": [ - "Phiki\\Adapters\\Laravel\\PhikiServiceProvider" - ] + "branch-alias": { + "dev-2.x": "2.x-dev" } }, "autoload": { "psr-4": { - "Phiki\\": "src/" + "phpDocumentor\\Reflection\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -3803,41 +3981,160 @@ ], "authors": [ { - "name": "Ryan Chandler", - "email": "support@ryangjchandler.co.uk", - "homepage": "https://ryangjchandler.co.uk", - "role": "Developer" + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" } ], - "description": "Syntax highlighting using TextMate grammars in PHP.", + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], "support": { - "issues": "https://github.com/phikiphp/phiki/issues", - "source": "https://github.com/phikiphp/phiki/tree/v2.0.0" + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" }, - "funding": [ - { - "url": "https://github.com/sponsors/ryangjchandler", - "type": "github" - }, - { - "url": "https://buymeacoffee.com/ryangjchandler", - "type": "other" - } - ], - "time": "2025-08-28T18:20:27+00:00" + "time": "2020-06-27T09:03:43+00:00" }, { - "name": "phpoffice/phpspreadsheet", - "version": "1.30.0", + "name": "phpdocumentor/reflection-docblock", + "version": "6.0.3", "source": { "type": "git", - "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", - "reference": "2f39286e0136673778b7a142b3f0d141e43d1714" + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/2f39286e0136673778b7a142b3f0d141e43d1714", - "reference": "2f39286e0136673778b7a142b3f0d141e43d1714", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/7bae67520aa9f5ecc506d646810bd40d9da54582", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^2.0", + "phpstan/phpdoc-parser": "^2.0", + "webmozart/assert": "^1.9.1 || ^2" + }, + "require-dev": { + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26", + "shipmonk/dead-code-detector": "^0.5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.3" + }, + "time": "2026-03-18T20:49:53+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/327a05bbee54120d4786a0dc67aad30226ad4cf9", + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev", + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/2.0.0" + }, + "time": "2026-01-06T21:53:42+00:00" + }, + { + "name": "phpoffice/phpspreadsheet", + "version": "1.30.5", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", + "reference": "97bcabd32a64924688487dcd64aceaf158affb5c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/97bcabd32a64924688487dcd64aceaf158affb5c", + "reference": "97bcabd32a64924688487dcd64aceaf158affb5c", "shasum": "" }, "require": { @@ -3859,13 +4156,12 @@ "maennchen/zipstream-php": "^2.1 || ^3.0", "markbaker/complex": "^3.0", "markbaker/matrix": "^3.0", - "php": "^7.4 || ^8.0", - "psr/http-client": "^1.0", - "psr/http-factory": "^1.0", + "php": ">=7.4.0 <8.5.0", "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "dev-main", + "doctrine/instantiator": "^1.5", "dompdf/dompdf": "^1.0 || ^2.0 || ^3.0", "friendsofphp/php-cs-fixer": "^3.2", "mitoteam/jpgraph": "^10.3", @@ -3912,6 +4208,9 @@ }, { "name": "Adrien Crivelli" + }, + { + "name": "Owen Leibman" } ], "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", @@ -3928,22 +4227,22 @@ ], "support": { "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", - "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.0" + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.5" }, - "time": "2025-08-10T06:28:02+00:00" + "time": "2026-05-31T05:13:11+00:00" }, { "name": "phpoption/phpoption", - "version": "1.9.4", + "version": "1.9.5", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d" + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", - "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", "shasum": "" }, "require": { @@ -3993,7 +4292,7 @@ ], "support": { "issues": "https://github.com/schmittjoh/php-option/issues", - "source": "https://github.com/schmittjoh/php-option/tree/1.9.4" + "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" }, "funding": [ { @@ -4005,20 +4304,67 @@ "type": "tidelift" } ], - "time": "2025-08-21T11:53:16+00:00" + "time": "2025-12-27T19:41:33+00:00" }, { - "name": "pragmarx/google2fa", - "version": "v8.0.3", + "name": "phpstan/phpdoc-parser", + "version": "2.3.2", "source": { "type": "git", - "url": "https://github.com/antonioribeiro/google2fa.git", - "reference": "6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad" + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad", - "reference": "6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2" + }, + "time": "2026-01-25T14:56:51+00:00" + }, + { + "name": "pragmarx/google2fa", + "version": "v9.0.0", + "source": { + "type": "git", + "url": "https://github.com/antonioribeiro/google2fa.git", + "reference": "e6bc62dd6ae83acc475f57912e27466019a1f2cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/e6bc62dd6ae83acc475f57912e27466019a1f2cf", + "reference": "e6bc62dd6ae83acc475f57912e27466019a1f2cf", "shasum": "" }, "require": { @@ -4055,9 +4401,9 @@ ], "support": { "issues": "https://github.com/antonioribeiro/google2fa/issues", - "source": "https://github.com/antonioribeiro/google2fa/tree/v8.0.3" + "source": "https://github.com/antonioribeiro/google2fa/tree/v9.0.0" }, - "time": "2024-09-05T11:56:40+00:00" + "time": "2025-09-19T22:51:08+00:00" }, { "name": "psr/clock", @@ -4473,16 +4819,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.10", + "version": "v0.12.23", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "6e80abe6f2257121f1eb9a4c55bf29d921025b22" + "reference": "4dcc0f08047d52bbde475eda481146fd8e27e1a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/6e80abe6f2257121f1eb9a4c55bf29d921025b22", - "reference": "6e80abe6f2257121f1eb9a4c55bf29d921025b22", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/4dcc0f08047d52bbde475eda481146fd8e27e1a4", + "reference": "4dcc0f08047d52bbde475eda481146fd8e27e1a4", "shasum": "" }, "require": { @@ -4490,18 +4836,19 @@ "ext-tokenizer": "*", "nikic/php-parser": "^5.0 || ^4.0", "php": "^8.0 || ^7.4", - "symfony/console": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", - "symfony/var-dumper": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + "symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" }, "conflict": { "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.2" + "bamarni/composer-bin-plugin": "^1.2", + "composer/class-map-generator": "^1.6" }, "suggest": { + "composer/class-map-generator": "Improved tab completion performance with better class discovery.", "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", - "ext-pdo-sqlite": "The doc command requires SQLite to work.", "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." }, "bin": [ @@ -4545,9 +4892,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.10" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.23" }, - "time": "2025-08-04T12:39:37+00:00" + "time": "2026-05-23T13:41:31+00:00" }, { "name": "ralouphie/getallheaders", @@ -4671,20 +5018,20 @@ }, { "name": "ramsey/uuid", - "version": "4.9.1", + "version": "4.9.3", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440" + "reference": "1df15849d00943a67d677dc9cfd80795f038c9f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/81f941f6f729b1e3ceea61d9d014f8b6c6800440", - "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/1df15849d00943a67d677dc9cfd80795f038c9f8", + "reference": "1df15849d00943a67d677dc9cfd80795f038c9f8", "shasum": "" }, "require": { - "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "brick/math": ">=0.8.16 <=0.18", "php": "^8.0", "ramsey/collection": "^1.2 || ^2.0" }, @@ -4743,31 +5090,41 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.9.1" + "source": "https://github.com/ramsey/uuid/tree/4.9.3" }, - "time": "2025-09-04T20:59:21+00:00" + "time": "2026-06-18T03:57:49+00:00" }, { "name": "sabberworm/php-css-parser", - "version": "v8.9.0", + "version": "v9.4.0", "source": { "type": "git", "url": "https://github.com/MyIntervals/PHP-CSS-Parser.git", - "reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9" + "reference": "fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/d8e916507b88e389e26d4ab03c904a082aa66bb9", - "reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9", + "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f", + "reference": "fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f", "shasum": "" }, "require": { "ext-iconv": "*", - "php": "^5.6.20 || ^7.0.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" + "php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "thecodingmachine/safe": "^1.3 || ^2.5 || ^3.4" }, "require-dev": { - "phpunit/phpunit": "5.7.27 || 6.5.14 || 7.5.20 || 8.5.41", - "rawr/cross-data-providers": "^2.0.0" + "php-parallel-lint/php-parallel-lint": "1.4.0", + "phpstan/extension-installer": "1.4.3", + "phpstan/phpstan": "1.12.33 || 2.2.2", + "phpstan/phpstan-phpunit": "1.4.2 || 2.0.16", + "phpstan/phpstan-strict-rules": "1.6.2 || 2.0.11", + "phpunit/phpunit": "8.5.52", + "rawr/phpunit-data-provider": "3.3.1", + "rector/rector": "1.2.10 || 2.4.6", + "rector/type-perfect": "1.0.0 || 2.1.3", + "squizlabs/php_codesniffer": "4.0.1", + "thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.3" }, "suggest": { "ext-mbstring": "for parsing UTF-8 CSS" @@ -4775,10 +5132,14 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "9.0.x-dev" + "dev-main": "9.5.x-dev" } }, "autoload": { + "files": [ + "src/Rule/Rule.php", + "src/RuleSet/RuleContainer.php" + ], "psr-4": { "Sabberworm\\CSS\\": "src/" } @@ -4809,35 +5170,103 @@ ], "support": { "issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues", - "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v8.9.0" + "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.4.0" }, - "time": "2025-07-11T13:20:48+00:00" + "time": "2026-06-18T15:10:53+00:00" }, { - "name": "spatie/laravel-activitylog", - "version": "4.10.2", + "name": "spatie/browsershot", + "version": "5.4.0", "source": { "type": "git", - "url": "https://github.com/spatie/laravel-activitylog.git", - "reference": "bb879775d487438ed9a99e64f09086b608990c10" + "url": "https://github.com/spatie/browsershot.git", + "reference": "dcf7a65fd1d0fc8fd113739b84982377728d0b2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-activitylog/zipball/bb879775d487438ed9a99e64f09086b608990c10", - "reference": "bb879775d487438ed9a99e64f09086b608990c10", + "url": "https://api.github.com/repos/spatie/browsershot/zipball/dcf7a65fd1d0fc8fd113739b84982377728d0b2f", + "reference": "dcf7a65fd1d0fc8fd113739b84982377728d0b2f", "shasum": "" }, "require": { - "illuminate/config": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0", - "illuminate/database": "^8.69 || ^9.27 || ^10.0 || ^11.0 || ^12.0", - "illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0", + "ext-fileinfo": "*", + "ext-json": "*", + "php": "^8.2", + "spatie/temporary-directory": "^2.0", + "symfony/process": "^6.0|^7.0|^8.0" + }, + "require-dev": { + "pestphp/pest": "^3.0|^4.0", + "spatie/image": "^3.6", + "spatie/pdf-to-text": "^1.52", + "spatie/phpunit-snapshot-assertions": "^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\Browsershot\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://github.com/freekmurze", + "role": "Developer" + } + ], + "description": "Convert a webpage to an image or pdf using headless Chrome", + "homepage": "https://github.com/spatie/browsershot", + "keywords": [ + "chrome", + "convert", + "headless", + "image", + "pdf", + "puppeteer", + "screenshot", + "webpage" + ], + "support": { + "source": "https://github.com/spatie/browsershot/tree/5.4.0" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2026-05-26T13:13:33+00:00" + }, + { + "name": "spatie/laravel-activitylog", + "version": "4.12.3", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-activitylog.git", + "reference": "2a2024fcac05628b0d1bfdbb1b94dda8b0661dc0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-activitylog/zipball/2a2024fcac05628b0d1bfdbb1b94dda8b0661dc0", + "reference": "2a2024fcac05628b0d1bfdbb1b94dda8b0661dc0", + "shasum": "" + }, + "require": { + "illuminate/config": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0", + "illuminate/database": "^8.69 || ^9.27 || ^10.0 || ^11.0 || ^12.0 || ^13.0", + "illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0", "php": "^8.1", "spatie/laravel-package-tools": "^1.6.3" }, "require-dev": { "ext-json": "*", - "orchestra/testbench": "^6.23 || ^7.0 || ^8.0 || ^9.0 || ^10.0", - "pestphp/pest": "^1.20 || ^2.0 || ^3.0" + "orchestra/testbench": "^6.23 || ^7.0 || ^8.0 || ^9.6 || ^10.0 || ^11.0", + "pestphp/pest": "^1.20 || ^2.0 || ^3.0 || ^4.0" }, "type": "library", "extra": { @@ -4890,7 +5319,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-activitylog/issues", - "source": "https://github.com/spatie/laravel-activitylog/tree/4.10.2" + "source": "https://github.com/spatie/laravel-activitylog/tree/4.12.3" }, "funding": [ { @@ -4902,33 +5331,33 @@ "type": "github" } ], - "time": "2025-06-15T06:59:49+00:00" + "time": "2026-03-24T12:33:53+00:00" }, { "name": "spatie/laravel-package-tools", - "version": "1.92.7", + "version": "1.93.1", "source": { "type": "git", "url": "https://github.com/spatie/laravel-package-tools.git", - "reference": "f09a799850b1ed765103a4f0b4355006360c49a5" + "reference": "d5552849801f2642aea710557463234b59ef65eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/f09a799850b1ed765103a4f0b4355006360c49a5", - "reference": "f09a799850b1ed765103a4f0b4355006360c49a5", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/d5552849801f2642aea710557463234b59ef65eb", + "reference": "d5552849801f2642aea710557463234b59ef65eb", "shasum": "" }, "require": { - "illuminate/contracts": "^9.28|^10.0|^11.0|^12.0", - "php": "^8.0" + "illuminate/contracts": "^10.0|^11.0|^12.0|^13.0", + "php": "^8.1" }, "require-dev": { "mockery/mockery": "^1.5", - "orchestra/testbench": "^7.7|^8.0|^9.0|^10.0", - "pestphp/pest": "^1.23|^2.1|^3.1", - "phpunit/php-code-coverage": "^9.0|^10.0|^11.0", - "phpunit/phpunit": "^9.5.24|^10.5|^11.5", - "spatie/pest-plugin-test-time": "^1.1|^2.2" + "orchestra/testbench": "^8.0|^9.2|^10.0|^11.0", + "pestphp/pest": "^2.1|^3.1|^4.0", + "phpunit/php-code-coverage": "^10.0|^11.0|^12.0", + "phpunit/phpunit": "^10.5|^11.5|^12.5", + "spatie/pest-plugin-test-time": "^2.2|^3.0" }, "type": "library", "autoload": { @@ -4955,7 +5384,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-package-tools/issues", - "source": "https://github.com/spatie/laravel-package-tools/tree/1.92.7" + "source": "https://github.com/spatie/laravel-package-tools/tree/1.93.1" }, "funding": [ { @@ -4963,34 +5392,35 @@ "type": "github" } ], - "time": "2025-07-17T15:46:43+00:00" + "time": "2026-05-19T14:06:37+00:00" }, { "name": "spatie/laravel-permission", - "version": "6.21.0", + "version": "6.25.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-permission.git", - "reference": "6a118e8855dfffcd90403aab77bbf35a03db51b3" + "reference": "d7d4cb0d58616722f1afc90e0484e4825155b9b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-permission/zipball/6a118e8855dfffcd90403aab77bbf35a03db51b3", - "reference": "6a118e8855dfffcd90403aab77bbf35a03db51b3", + "url": "https://api.github.com/repos/spatie/laravel-permission/zipball/d7d4cb0d58616722f1afc90e0484e4825155b9b3", + "reference": "d7d4cb0d58616722f1afc90e0484e4825155b9b3", "shasum": "" }, "require": { - "illuminate/auth": "^8.12|^9.0|^10.0|^11.0|^12.0", - "illuminate/container": "^8.12|^9.0|^10.0|^11.0|^12.0", - "illuminate/contracts": "^8.12|^9.0|^10.0|^11.0|^12.0", - "illuminate/database": "^8.12|^9.0|^10.0|^11.0|^12.0", + "illuminate/auth": "^8.12|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/container": "^8.12|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/contracts": "^8.12|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/database": "^8.12|^9.0|^10.0|^11.0|^12.0|^13.0", "php": "^8.0" }, "require-dev": { - "laravel/passport": "^11.0|^12.0", + "laravel/passport": "^11.0|^12.0|^13.0", "laravel/pint": "^1.0", - "orchestra/testbench": "^6.23|^7.0|^8.0|^9.0|^10.0", - "phpunit/phpunit": "^9.4|^10.1|^11.5" + "orchestra/testbench": "^6.23|^7.0|^8.0|^9.0|^10.0|^11.0", + "pestphp/pest": "^2.0|^3.0|^4.0", + "pestphp/pest-plugin-laravel": "^2.0|^3.0|^4.0" }, "type": "library", "extra": { @@ -5038,7 +5468,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-permission/issues", - "source": "https://github.com/spatie/laravel-permission/tree/6.21.0" + "source": "https://github.com/spatie/laravel-permission/tree/6.25.0" }, "funding": [ { @@ -5046,26 +5476,267 @@ "type": "github" } ], - "time": "2025-07-23T16:08:05+00:00" + "time": "2026-03-17T22:46:46+00:00" }, { - "name": "symfony/clock", - "version": "v7.3.0", + "name": "spatie/temporary-directory", + "version": "2.4.0", "source": { "type": "git", - "url": "https://github.com/symfony/clock.git", - "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24" + "url": "https://github.com/spatie/temporary-directory.git", + "reference": "32cbb9645b28839cf4f476708e99a2c70e6802c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", - "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "url": "https://api.github.com/repos/spatie/temporary-directory/zipball/32cbb9645b28839cf4f476708e99a2c70e6802c9", + "reference": "32cbb9645b28839cf4f476708e99a2c70e6802c9", "shasum": "" }, "require": { - "php": ">=8.2", - "psr/clock": "^1.0", - "symfony/polyfill-php83": "^1.28" + "php": "^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\TemporaryDirectory\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Vanderbist", + "email": "alex@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Easily create, use and destroy temporary directories", + "homepage": "https://github.com/spatie/temporary-directory", + "keywords": [ + "php", + "spatie", + "temporary-directory" + ], + "support": { + "issues": "https://github.com/spatie/temporary-directory/issues", + "source": "https://github.com/spatie/temporary-directory/tree/2.4.0" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2026-06-22T07:55:44+00:00" + }, + { + "name": "spomky-labs/cbor-php", + "version": "3.2.3", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/cbor-php.git", + "reference": "dd6eb84e6d92f7b8bd0da56b4b4dd7235aed0c32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/cbor-php/zipball/dd6eb84e6d92f7b8bd0da56b4b4dd7235aed0c32", + "reference": "dd6eb84e6d92f7b8bd0da56b4b4dd7235aed0c32", + "shasum": "" + }, + "require": { + "brick/math": "^0.9|^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17", + "ext-mbstring": "*", + "php": ">=8.0" + }, + "require-dev": { + "ext-json": "*", + "roave/security-advisories": "dev-latest", + "symfony/error-handler": "^6.4|^7.1|^8.0", + "symfony/var-dumper": "^6.4|^7.1|^8.0" + }, + "suggest": { + "ext-bcmath": "GMP or BCMath extensions will drastically improve the library performance. BCMath extension needed to handle the Big Float and Decimal Fraction Tags", + "ext-gmp": "GMP or BCMath extensions will drastically improve the library performance" + }, + "type": "library", + "autoload": { + "psr-4": { + "CBOR\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/Spomky-Labs/cbor-php/contributors" + } + ], + "description": "CBOR Encoder/Decoder for PHP", + "keywords": [ + "Concise Binary Object Representation", + "RFC7049", + "cbor" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/cbor-php/issues", + "source": "https://github.com/Spomky-Labs/cbor-php/tree/3.2.3" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2026-04-01T12:15:20+00:00" + }, + { + "name": "spomky-labs/pki-framework", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/pki-framework.git", + "reference": "aa576cbd07128075bef97ac2f8af9854e67513d8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/pki-framework/zipball/aa576cbd07128075bef97ac2f8af9854e67513d8", + "reference": "aa576cbd07128075bef97ac2f8af9854e67513d8", + "shasum": "" + }, + "require": { + "brick/math": "^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17", + "ext-mbstring": "*", + "php": ">=8.1", + "psr/clock": "^1.0" + }, + "require-dev": { + "ekino/phpstan-banned-code": "^1.0|^2.0|^3.0", + "ext-gmp": "*", + "ext-openssl": "*", + "infection/infection": "^0.28|^0.29|^0.31|^0.32", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/extension-installer": "^1.3|^2.0", + "phpstan/phpstan": "^1.8|^2.0", + "phpstan/phpstan-deprecation-rules": "^1.0|^2.0", + "phpstan/phpstan-phpunit": "^1.1|^2.0", + "phpstan/phpstan-strict-rules": "^1.3|^2.0", + "phpunit/phpunit": "^10.1|^11.0|^12.0|^13.0", + "rector/rector": "^1.0|^2.0", + "roave/security-advisories": "dev-latest", + "symfony/string": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symplify/easy-coding-standard": "^12.0|^13.0" + }, + "suggest": { + "ext-bcmath": "For better performance (or GMP)", + "ext-gmp": "For better performance (or BCMath)", + "ext-openssl": "For OpenSSL based cyphering" + }, + "type": "library", + "autoload": { + "psr-4": { + "SpomkyLabs\\Pki\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joni Eskelinen", + "email": "jonieske@gmail.com", + "role": "Original developer" + }, + { + "name": "Florent Morselli", + "email": "florent.morselli@spomky-labs.com", + "role": "Spomky-Labs PKI Framework developer" + } + ], + "description": "A PHP framework for managing Public Key Infrastructures. It comprises X.509 public key certificates, attribute certificates, certification requests and certification path validation.", + "homepage": "https://github.com/spomky-labs/pki-framework", + "keywords": [ + "DER", + "Private Key", + "ac", + "algorithm identifier", + "asn.1", + "asn1", + "attribute certificate", + "certificate", + "certification request", + "cryptography", + "csr", + "decrypt", + "ec", + "encrypt", + "pem", + "pkcs", + "public key", + "rsa", + "sign", + "signature", + "verify", + "x.509", + "x.690", + "x509", + "x690" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/pki-framework/issues", + "source": "https://github.com/Spomky-Labs/pki-framework/tree/1.4.2" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2026-03-23T22:56:56+00:00" + }, + { + "name": "symfony/clock", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "701ef4de9705d6c32292ebee5e8044094a09fbf6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/701ef4de9705d6c32292ebee5e8044094a09fbf6", + "reference": "701ef4de9705d6c32292ebee5e8044094a09fbf6", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "psr/clock": "^1.0" }, "provide": { "psr/clock-implementation": "1.0" @@ -5104,7 +5775,7 @@ "time" ], "support": { - "source": "https://github.com/symfony/clock/tree/v7.3.0" + "source": "https://github.com/symfony/clock/tree/v8.1.0" }, "funding": [ { @@ -5115,25 +5786,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/console", - "version": "v7.3.3", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7" + "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7", - "reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7", + "url": "https://api.github.com/repos/symfony/console/zipball/85095d2573eaefaf35e40b9513a9bf09f72cd217", + "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217", "shasum": "" }, "require": { @@ -5141,7 +5816,7 @@ "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.2" + "symfony/string": "^7.2|^8.0" }, "conflict": { "symfony/dependency-injection": "<6.4", @@ -5155,16 +5830,16 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -5198,7 +5873,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.3" + "source": "https://github.com/symfony/console/tree/v7.4.13" }, "funding": [ { @@ -5218,24 +5893,24 @@ "type": "tidelift" } ], - "time": "2025-08-25T06:35:40+00:00" + "time": "2026-05-24T08:56:14+00:00" }, { "name": "symfony/css-selector", - "version": "v7.3.0", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2" + "reference": "dc0e2be45c9b5588c82414f02ac574b4b986abcd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/601a5ce9aaad7bf10797e3663faefce9e26c24e2", - "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/dc0e2be45c9b5588c82414f02ac574b4b986abcd", + "reference": "dc0e2be45c9b5588c82414f02ac574b4b986abcd", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.4.1" }, "type": "library", "autoload": { @@ -5267,7 +5942,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v7.3.0" + "source": "https://github.com/symfony/css-selector/tree/v8.1.0" }, "funding": [ { @@ -5278,25 +5953,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", "shasum": "" }, "require": { @@ -5309,7 +5988,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -5334,7 +6013,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" }, "funding": [ { @@ -5345,41 +6024,46 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-04-13T15:52:40+00:00" }, { "name": "symfony/error-handler", - "version": "v7.3.2", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "0b31a944fcd8759ae294da4d2808cbc53aebd0c3" + "reference": "8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/0b31a944fcd8759ae294da4d2808cbc53aebd0c3", - "reference": "0b31a944fcd8759ae294da4d2808cbc53aebd0c3", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa", + "reference": "8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa", "shasum": "" }, "require": { "php": ">=8.2", "psr/log": "^1|^2|^3", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "conflict": { "symfony/deprecation-contracts": "<2.5", "symfony/http-kernel": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0|^8.0", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", "symfony/webpack-encore-bundle": "^1.0|^2.0" }, "bin": [ @@ -5411,7 +6095,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.3.2" + "source": "https://github.com/symfony/error-handler/tree/v7.4.8" }, "funding": [ { @@ -5431,28 +6115,29 @@ "type": "tidelift" } ], - "time": "2025-07-07T08:17:57+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.3.3", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191" + "reference": "f249ae3f680958b6f1f9dd76e5747cf0695b4102" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7dc69e71de420ac04bc9ab830cf3ffebba48191", - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f249ae3f680958b6f1f9dd76e5747cf0695b4102", + "reference": "f249ae3f680958b6f1f9dd76e5747cf0695b4102", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "symfony/dependency-injection": "<6.4", + "symfony/security-http": "<7.4", "symfony/service-contracts": "<2.5" }, "provide": { @@ -5461,13 +6146,14 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/error-handler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^6.4|^7.0" + "symfony/stopwatch": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -5495,7 +6181,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.3" + "source": "https://github.com/symfony/event-dispatcher/tree/v8.1.0" }, "funding": [ { @@ -5515,20 +6201,20 @@ "type": "tidelift" } ], - "time": "2025-08-13T11:49:31+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", - "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/ccba7060602b7fed0b03c85bf025257f76d9ef32", + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32", "shasum": "" }, "require": { @@ -5542,7 +6228,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -5575,7 +6261,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.0" }, "funding": [ { @@ -5586,32 +6272,36 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-01-05T13:30:16+00:00" }, { "name": "symfony/finder", - "version": "v7.3.2", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe" + "reference": "e0be088d22278583a82da281886e8c3592fbf149" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/2a6614966ba1074fa93dae0bc804227422df4dfe", - "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe", + "url": "https://api.github.com/repos/symfony/finder/zipball/e0be088d22278583a82da281886e8c3592fbf149", + "reference": "e0be088d22278583a82da281886e8c3592fbf149", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "symfony/filesystem": "^6.4|^7.0" + "symfony/filesystem": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -5639,7 +6329,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.3.2" + "source": "https://github.com/symfony/finder/tree/v7.4.8" }, "funding": [ { @@ -5659,27 +6349,26 @@ "type": "tidelift" } ], - "time": "2025-07-15T13:41:35+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.3.3", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "7475561ec27020196c49bb7c4f178d33d7d3dc00" + "reference": "bc354f47c62301e990b7874fa662326368508e2c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/7475561ec27020196c49bb7c4f178d33d7d3dc00", - "reference": "7475561ec27020196c49bb7c4f178d33d7d3dc00", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/bc354f47c62301e990b7874fa662326368508e2c", + "reference": "bc354f47c62301e990b7874fa662326368508e2c", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", - "symfony/polyfill-mbstring": "~1.1", - "symfony/polyfill-php83": "^1.27" + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" }, "conflict": { "doctrine/dbal": "<3.6", @@ -5688,13 +6377,13 @@ "require-dev": { "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", - "symfony/cache": "^6.4.12|^7.1.5", - "symfony/clock": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/mime": "^6.4|^7.0", - "symfony/rate-limiter": "^6.4|^7.0" + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -5722,7 +6411,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.3.3" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.13" }, "funding": [ { @@ -5742,29 +6431,29 @@ "type": "tidelift" } ], - "time": "2025-08-20T08:04:18+00:00" + "time": "2026-05-24T11:20:33+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.3.3", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "72c304de37e1a1cec6d5d12b81187ebd4850a17b" + "reference": "9df847980c436451f4f51d1284491bb4356dd989" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/72c304de37e1a1cec6d5d12b81187ebd4850a17b", - "reference": "72c304de37e1a1cec6d5d12b81187ebd4850a17b", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/9df847980c436451f4f51d1284491bb4356dd989", + "reference": "9df847980c436451f4f51d1284491bb4356dd989", "shasum": "" }, "require": { "php": ">=8.2", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/error-handler": "^6.4|^7.0", - "symfony/event-dispatcher": "^7.3", - "symfony/http-foundation": "^7.3", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", "symfony/polyfill-ctype": "^1.8" }, "conflict": { @@ -5774,6 +6463,7 @@ "symfony/console": "<6.4", "symfony/dependency-injection": "<6.4", "symfony/doctrine-bridge": "<6.4", + "symfony/flex": "<2.10", "symfony/form": "<6.4", "symfony/http-client": "<6.4", "symfony/http-client-contracts": "<2.5", @@ -5791,27 +6481,27 @@ }, "require-dev": { "psr/cache": "^1.0|^2.0|^3.0", - "symfony/browser-kit": "^6.4|^7.0", - "symfony/clock": "^6.4|^7.0", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/css-selector": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/dom-crawler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4.1|^7.0.1|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", "symfony/http-client-contracts": "^2.5|^3", - "symfony/process": "^6.4|^7.0", - "symfony/property-access": "^7.1", - "symfony/routing": "^6.4|^7.0", - "symfony/serializer": "^7.1", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/translation": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^7.1|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.1|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", "symfony/translation-contracts": "^2.5|^3", - "symfony/uid": "^6.4|^7.0", - "symfony/validator": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0", - "symfony/var-exporter": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", "twig/twig": "^3.12" }, "type": "library", @@ -5840,7 +6530,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.3.3" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.13" }, "funding": [ { @@ -5860,20 +6550,20 @@ "type": "tidelift" } ], - "time": "2025-08-29T08:23:45+00:00" + "time": "2026-05-27T08:31:43+00:00" }, { "name": "symfony/mailer", - "version": "v7.3.3", + "version": "v7.4.12", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "a32f3f45f1990db8c4341d5122a7d3a381c7e575" + "reference": "5cefb712a25f320579615ba9e1942abaeade7dff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/a32f3f45f1990db8c4341d5122a7d3a381c7e575", - "reference": "a32f3f45f1990db8c4341d5122a7d3a381c7e575", + "url": "https://api.github.com/repos/symfony/mailer/zipball/5cefb712a25f320579615ba9e1942abaeade7dff", + "reference": "5cefb712a25f320579615ba9e1942abaeade7dff", "shasum": "" }, "require": { @@ -5881,8 +6571,8 @@ "php": ">=8.2", "psr/event-dispatcher": "^1", "psr/log": "^1|^2|^3", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/mime": "^7.2", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/mime": "^7.2|^8.0", "symfony/service-contracts": "^2.5|^3" }, "conflict": { @@ -5893,10 +6583,10 @@ "symfony/twig-bridge": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/twig-bridge": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -5924,7 +6614,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.3.3" + "source": "https://github.com/symfony/mailer/tree/v7.4.12" }, "funding": [ { @@ -5944,43 +6634,44 @@ "type": "tidelift" } ], - "time": "2025-08-13T11:49:31+00:00" + "time": "2026-05-20T07:20:23+00:00" }, { "name": "symfony/mime", - "version": "v7.3.2", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "e0a0f859148daf1edf6c60b398eb40bfc96697d1" + "reference": "a845722765c4f6b2ce88beaf4f4479975b186770" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/e0a0f859148daf1edf6c60b398eb40bfc96697d1", - "reference": "e0a0f859148daf1edf6c60b398eb40bfc96697d1", + "url": "https://api.github.com/repos/symfony/mime/zipball/a845722765c4f6b2ce88beaf4f4479975b186770", + "reference": "a845722765c4f6b2ce88beaf4f4479975b186770", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-intl-idn": "^1.10", "symfony/polyfill-mbstring": "^1.0" }, "conflict": { "egulias/email-validator": "~3.0.0", - "phpdocumentor/reflection-docblock": "<3.2.2", - "phpdocumentor/type-resolver": "<1.4.0", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", "symfony/mailer": "<6.4", "symfony/serializer": "<6.4.3|>7.0,<7.0.3" }, "require-dev": { "egulias/email-validator": "^2.1.10|^3.1|^4", "league/html-to-markdown": "^5.0", - "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/property-access": "^6.4|^7.0", - "symfony/property-info": "^6.4|^7.0", - "symfony/serializer": "^6.4.3|^7.0.3" + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0" }, "type": "library", "autoload": { @@ -6012,7 +6703,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.3.2" + "source": "https://github.com/symfony/mime/tree/v7.4.13" }, "funding": [ { @@ -6032,20 +6723,20 @@ "type": "tidelift" } ], - "time": "2025-07-15T13:41:35+00:00" + "time": "2026-05-23T16:22:37+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", "shasum": "" }, "require": { @@ -6095,7 +6786,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" }, "funding": [ { @@ -6115,20 +6806,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.33.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + "reference": "e9247d281d694a5120554d9afaf54e070e88a603" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", + "reference": "e9247d281d694a5120554d9afaf54e070e88a603", "shasum": "" }, "require": { @@ -6177,7 +6868,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" }, "funding": [ { @@ -6197,20 +6888,20 @@ "type": "tidelift" } ], - "time": "2025-06-27T09:58:17+00:00" + "time": "2026-05-26T05:58:03+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.33.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + "reference": "dc21118016c039a66235cf93d96b435ffb282412" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", - "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/dc21118016c039a66235cf93d96b435ffb282412", + "reference": "dc21118016c039a66235cf93d96b435ffb282412", "shasum": "" }, "require": { @@ -6264,7 +6955,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.38.1" }, "funding": [ { @@ -6284,20 +6975,20 @@ "type": "tidelift" } ], - "time": "2024-09-10T14:38:51+00:00" + "time": "2026-05-25T15:22:23+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.33.0", + "version": "v1.38.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "3833d7255cc303546435cb650316bff708a1c75c" + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", - "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", "shasum": "" }, "require": { @@ -6349,7 +7040,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" }, "funding": [ { @@ -6369,20 +7060,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-05-25T13:48:31+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.33.0", + "version": "v1.38.2", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", "shasum": "" }, "require": { @@ -6434,7 +7125,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" }, "funding": [ { @@ -6454,20 +7145,20 @@ "type": "tidelift" } ], - "time": "2024-12-23T08:48:59+00:00" + "time": "2026-05-27T06:59:30+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", - "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", "shasum": "" }, "require": { @@ -6518,7 +7209,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" }, "funding": [ { @@ -6538,20 +7229,20 @@ "type": "tidelift" } ], - "time": "2025-01-02T08:10:11+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-php83", - "version": "v1.33.0", + "version": "v1.38.2", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + "reference": "796a26abb75ce49f3a84433cd81bf1009d73d5f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", - "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/796a26abb75ce49f3a84433cd81bf1009d73d5f8", + "reference": "796a26abb75ce49f3a84433cd81bf1009d73d5f8", "shasum": "" }, "require": { @@ -6598,7 +7289,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.38.2" }, "funding": [ { @@ -6618,20 +7309,20 @@ "type": "tidelift" } ], - "time": "2025-07-08T02:45:35+00:00" + "time": "2026-05-27T06:51:48+00:00" }, { "name": "symfony/polyfill-php84", - "version": "v1.33.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php84.git", - "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", - "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", "shasum": "" }, "require": { @@ -6678,7 +7369,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php84/tree/v1.38.1" }, "funding": [ { @@ -6698,20 +7389,20 @@ "type": "tidelift" } ], - "time": "2025-06-24T13:30:11+00:00" + "time": "2026-05-26T12:51:13+00:00" }, { "name": "symfony/polyfill-php85", - "version": "v1.33.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php85.git", - "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" + "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", - "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", + "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", "shasum": "" }, "require": { @@ -6758,7 +7449,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php85/tree/v1.38.1" }, "funding": [ { @@ -6778,20 +7469,20 @@ "type": "tidelift" } ], - "time": "2025-06-23T16:12:55+00:00" + "time": "2026-05-26T02:25:22+00:00" }, { "name": "symfony/polyfill-uuid", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-uuid.git", - "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2" + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2", - "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/26dfec253c4cf3e51b541b52ddf7e42cb0908e94", + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94", "shasum": "" }, "require": { @@ -6841,7 +7532,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/polyfill-uuid/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.37.0" }, "funding": [ { @@ -6861,20 +7552,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/process", - "version": "v7.3.3", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "32241012d521e2e8a9d713adb0812bb773b907f1" + "reference": "f5804be144caceb570f6747519999636b664f24c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/32241012d521e2e8a9d713adb0812bb773b907f1", - "reference": "32241012d521e2e8a9d713adb0812bb773b907f1", + "url": "https://api.github.com/repos/symfony/process/zipball/f5804be144caceb570f6747519999636b664f24c", + "reference": "f5804be144caceb570f6747519999636b664f24c", "shasum": "" }, "require": { @@ -6906,7 +7597,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.3.3" + "source": "https://github.com/symfony/process/tree/v7.4.13" }, "funding": [ { @@ -6926,20 +7617,187 @@ "type": "tidelift" } ], - "time": "2025-08-18T09:42:54+00:00" + "time": "2026-05-23T16:05:06+00:00" }, { - "name": "symfony/routing", - "version": "v7.3.2", + "name": "symfony/property-access", + "version": "v8.1.0", "source": { "type": "git", - "url": "https://github.com/symfony/routing.git", - "reference": "7614b8ca5fa89b9cd233e21b627bfc5774f586e4" + "url": "https://github.com/symfony/property-access.git", + "reference": "9261ef060f26cc7b728f67f141ba19b98a6209a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/7614b8ca5fa89b9cd233e21b627bfc5774f586e4", - "reference": "7614b8ca5fa89b9cd233e21b627bfc5774f586e4", + "url": "https://api.github.com/repos/symfony/property-access/zipball/9261ef060f26cc7b728f67f141ba19b98a6209a9", + "reference": "9261ef060f26cc7b728f67f141ba19b98a6209a9", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/property-info": "^7.4.4|^8.0.4" + }, + "require-dev": { + "symfony/cache": "^7.4|^8.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyAccess\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides functions to read and write from/to an object or array using a simple string notation", + "homepage": "https://symfony.com", + "keywords": [ + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property-path", + "reflection" + ], + "support": { + "source": "https://github.com/symfony/property-access/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/property-info", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-info.git", + "reference": "4721e8c56d0cd2378e0ef9a9899f810008b859f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-info/zipball/4721e8c56d0cd2378e0ef9a9899f810008b859f7", + "reference": "4721e8c56d0cd2378e0ef9a9899f810008b859f7", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/string": "^7.4|^8.0", + "symfony/type-info": "^7.4.7|^8.0.7" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1" + }, + "require-dev": { + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "symfony/cache": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts information about PHP class' properties using metadata of popular sources", + "homepage": "https://symfony.com", + "keywords": [ + "doctrine", + "phpdoc", + "property", + "symfony", + "type", + "validator" + ], + "support": { + "source": "https://github.com/symfony/property-info/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/routing", + "version": "v7.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "3a162171bb008e5e0f15dce6581373a4c0e8390d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/3a162171bb008e5e0f15dce6581373a4c0e8390d", + "reference": "3a162171bb008e5e0f15dce6581373a4c0e8390d", "shasum": "" }, "require": { @@ -6953,11 +7811,11 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -6991,7 +7849,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.3.2" + "source": "https://github.com/symfony/routing/tree/v7.4.13" }, "funding": [ { @@ -7011,20 +7869,119 @@ "type": "tidelift" } ], - "time": "2025-07-15T11:36:08+00:00" + "time": "2026-05-24T11:20:33+00:00" }, { - "name": "symfony/service-contracts", - "version": "v3.6.0", + "name": "symfony/serializer", + "version": "v8.1.0", "source": { "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + "url": "https://github.com/symfony/serializer.git", + "reference": "d101886195c5f772cf7033641fe9c40c3e3969e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "url": "https://api.github.com/repos/symfony/serializer/zipball/d101886195c5f772cf7033641fe9c40c3e3969e1", + "reference": "d101886195c5f772cf7033641fe9c40c3e3969e1", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/property-access": "<8.1", + "symfony/property-info": "<7.4", + "symfony/type-info": "<7.4" + }, + "require-dev": { + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "seld/jsonlint": "^1.10", + "symfony/cache": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/filesystem": "^7.4|^8.0", + "symfony/form": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/property-access": "^8.1", + "symfony/property-info": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/type-info": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0", + "symfony/var-exporter": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Serializer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/serializer/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", "shasum": "" }, "require": { @@ -7042,7 +7999,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -7078,7 +8035,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" }, "funding": [ { @@ -7089,44 +8046,47 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-25T09:37:31+00:00" + "time": "2026-03-28T09:44:51+00:00" }, { "name": "symfony/string", - "version": "v7.3.3", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c" + "reference": "afd5944f4005862d961efb85c8bbd5c523c4e3c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/17a426cce5fd1f0901fefa9b2a490d0038fd3c9c", - "reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c", + "url": "https://api.github.com/repos/symfony/string/zipball/afd5944f4005862d961efb85c8bbd5c523c4e3c9", + "reference": "afd5944f4005862d961efb85c8bbd5c523c4e3c9", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0" + "php": ">=8.4.1", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" }, "conflict": { "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/emoji": "^7.1", - "symfony/error-handler": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/var-exporter": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -7165,7 +8125,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.3" + "source": "https://github.com/symfony/string/tree/v8.1.0" }, "funding": [ { @@ -7185,38 +8145,31 @@ "type": "tidelift" } ], - "time": "2025-08-25T06:35:40+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/translation", - "version": "v7.3.3", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "e0837b4cbcef63c754d89a4806575cada743a38d" + "reference": "b2bd012ca28c4acae830ee1206a5b6e35dd99693" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/e0837b4cbcef63c754d89a4806575cada743a38d", - "reference": "e0837b4cbcef63c754d89a4806575cada743a38d", + "url": "https://api.github.com/repos/symfony/translation/zipball/b2bd012ca28c4acae830ee1206a5b6e35dd99693", + "reference": "b2bd012ca28c4acae830ee1206a5b6e35dd99693", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0", - "symfony/translation-contracts": "^2.5|^3.0" + "php": ">=8.4.1", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation-contracts": "^3.6.1" }, "conflict": { "nikic/php-parser": "<5.0", - "symfony/config": "<6.4", - "symfony/console": "<6.4", - "symfony/dependency-injection": "<6.4", "symfony/http-client-contracts": "<2.5", - "symfony/http-kernel": "<6.4", - "symfony/service-contracts": "<2.5", - "symfony/twig-bundle": "<6.4", - "symfony/yaml": "<6.4" + "symfony/service-contracts": "<2.5" }, "provide": { "symfony/translation-implementation": "2.3|3.0" @@ -7224,17 +8177,17 @@ "require-dev": { "nikic/php-parser": "^5.0", "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", "symfony/http-client-contracts": "^2.5|^3.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", "symfony/polyfill-intl-icu": "^1.21", - "symfony/routing": "^6.4|^7.0", + "symfony/routing": "^7.4|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^6.4|^7.0" + "symfony/yaml": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -7265,7 +8218,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.3.3" + "source": "https://github.com/symfony/translation/tree/v8.1.0" }, "funding": [ { @@ -7285,20 +8238,20 @@ "type": "tidelift" } ], - "time": "2025-08-01T21:02:37+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d" + "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d", - "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/0ab302977a952b42fd51475c4ebac81f8da0a95d", + "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d", "shasum": "" }, "require": { @@ -7311,7 +8264,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -7347,7 +8300,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/translation-contracts/tree/v3.7.0" }, "funding": [ { @@ -7358,25 +8311,111 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-27T08:32:26+00:00" + "time": "2026-01-05T13:30:16+00:00" }, { - "name": "symfony/uid", - "version": "v7.3.1", + "name": "symfony/type-info", + "version": "v8.1.0", "source": { "type": "git", - "url": "https://github.com/symfony/uid.git", - "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb" + "url": "https://github.com/symfony/type-info.git", + "reference": "9f24df8a79781b9b9f030fea7dfd2f3bd1e7e7e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/a69f69f3159b852651a6bf45a9fdd149520525bb", - "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb", + "url": "https://api.github.com/repos/symfony/type-info/zipball/9f24df8a79781b9b9f030fea7dfd2f3bd1e7e7e7", + "reference": "9f24df8a79781b9b9f030fea7dfd2f3bd1e7e7e7", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "psr/container": "^1.1|^2.0" + }, + "conflict": { + "phpstan/phpdoc-parser": "<1.30" + }, + "require-dev": { + "phpstan/phpdoc-parser": "^1.30|^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\TypeInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mathias Arlaud", + "email": "mathias.arlaud@gmail.com" + }, + { + "name": "Baptiste LEDUC", + "email": "baptiste.leduc@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts PHP types information.", + "homepage": "https://symfony.com", + "keywords": [ + "PHPStan", + "phpdoc", + "symfony", + "type" + ], + "support": { + "source": "https://github.com/symfony/type-info/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/uid", + "version": "v7.4.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "2676b524340abcfe4d6151ec698463cebafee439" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/2676b524340abcfe4d6151ec698463cebafee439", + "reference": "2676b524340abcfe4d6151ec698463cebafee439", "shasum": "" }, "require": { @@ -7384,7 +8423,7 @@ "symfony/polyfill-uuid": "^1.15" }, "require-dev": { - "symfony/console": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -7421,7 +8460,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.3.1" + "source": "https://github.com/symfony/uid/tree/v7.4.9" }, "funding": [ { @@ -7432,25 +8471,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-27T19:55:54+00:00" + "time": "2026-04-30T15:19:22+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.3.3", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "34d8d4c4b9597347306d1ec8eb4e1319b1e6986f" + "reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/34d8d4c4b9597347306d1ec8eb4e1319b1e6986f", - "reference": "34d8d4c4b9597347306d1ec8eb4e1319b1e6986f", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9510c3966f749a1d1ff0059e1eabef6cc621e7fd", + "reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd", "shasum": "" }, "require": { @@ -7462,10 +8505,10 @@ "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/uid": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", "twig/twig": "^3.12" }, "bin": [ @@ -7504,7 +8547,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.3.3" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.8" }, "funding": [ { @@ -7524,27 +8567,170 @@ "type": "tidelift" } ], - "time": "2025-08-13T11:49:31+00:00" + "time": "2026-03-30T13:44:50+00:00" }, { - "name": "tijsverkoyen/css-to-inline-styles", - "version": "v2.3.0", + "name": "thecodingmachine/safe", + "version": "v3.4.0", "source": { "type": "git", - "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", - "reference": "0d72ac1c00084279c1816675284073c5a337c20d" + "url": "https://github.com/thecodingmachine/safe.git", + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/0d72ac1c00084279c1816675284073c5a337c20d", - "reference": "0d72ac1c00084279c1816675284073c5a337c20d", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/705683a25bacf0d4860c7dea4d7947bfd09eea19", + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^10", + "squizlabs/php_codesniffer": "^3.2" + }, + "type": "library", + "autoload": { + "files": [ + "lib/special_cases.php", + "generated/apache.php", + "generated/apcu.php", + "generated/array.php", + "generated/bzip2.php", + "generated/calendar.php", + "generated/classobj.php", + "generated/com.php", + "generated/cubrid.php", + "generated/curl.php", + "generated/datetime.php", + "generated/dir.php", + "generated/eio.php", + "generated/errorfunc.php", + "generated/exec.php", + "generated/fileinfo.php", + "generated/filesystem.php", + "generated/filter.php", + "generated/fpm.php", + "generated/ftp.php", + "generated/funchand.php", + "generated/gettext.php", + "generated/gmp.php", + "generated/gnupg.php", + "generated/hash.php", + "generated/ibase.php", + "generated/ibmDb2.php", + "generated/iconv.php", + "generated/image.php", + "generated/imap.php", + "generated/info.php", + "generated/inotify.php", + "generated/json.php", + "generated/ldap.php", + "generated/libxml.php", + "generated/lzf.php", + "generated/mailparse.php", + "generated/mbstring.php", + "generated/misc.php", + "generated/mysql.php", + "generated/mysqli.php", + "generated/network.php", + "generated/oci8.php", + "generated/opcache.php", + "generated/openssl.php", + "generated/outcontrol.php", + "generated/pcntl.php", + "generated/pcre.php", + "generated/pgsql.php", + "generated/posix.php", + "generated/ps.php", + "generated/pspell.php", + "generated/readline.php", + "generated/rnp.php", + "generated/rpminfo.php", + "generated/rrd.php", + "generated/sem.php", + "generated/session.php", + "generated/shmop.php", + "generated/sockets.php", + "generated/sodium.php", + "generated/solr.php", + "generated/spl.php", + "generated/sqlsrv.php", + "generated/ssdeep.php", + "generated/ssh2.php", + "generated/stream.php", + "generated/strings.php", + "generated/swoole.php", + "generated/uodbc.php", + "generated/uopz.php", + "generated/url.php", + "generated/var.php", + "generated/xdiff.php", + "generated/xml.php", + "generated/xmlrpc.php", + "generated/yaml.php", + "generated/yaz.php", + "generated/zip.php", + "generated/zlib.php" + ], + "classmap": [ + "lib/DateTime.php", + "lib/DateTimeImmutable.php", + "lib/Exceptions/", + "generated/Exceptions/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP core functions that throw exceptions instead of returning FALSE on error", + "support": { + "issues": "https://github.com/thecodingmachine/safe/issues", + "source": "https://github.com/thecodingmachine/safe/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://github.com/OskarStark", + "type": "github" + }, + { + "url": "https://github.com/shish", + "type": "github" + }, + { + "url": "https://github.com/silasjoisten", + "type": "github" + }, + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2026-02-04T18:08:13+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "php": "^7.4 || ^8.0", - "symfony/css-selector": "^5.4 || ^6.0 || ^7.0" + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, "require-dev": { "phpstan/phpstan": "^2.0", @@ -7577,32 +8763,32 @@ "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", "support": { "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", - "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.3.0" + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0" }, - "time": "2024-12-21T16:25:41+00:00" + "time": "2025-12-02T11:56:42+00:00" }, { "name": "vlucas/phpdotenv", - "version": "v5.6.2", + "version": "v5.6.3", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af" + "reference": "955e7815d677a3eaa7075231212f2110983adecc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af", - "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", + "reference": "955e7815d677a3eaa7075231212f2110983adecc", "shasum": "" }, "require": { "ext-pcre": "*", - "graham-campbell/result-type": "^1.1.3", + "graham-campbell/result-type": "^1.1.4", "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.3", - "symfony/polyfill-ctype": "^1.24", - "symfony/polyfill-mbstring": "^1.24", - "symfony/polyfill-php80": "^1.24" + "phpoption/phpoption": "^1.9.5", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", @@ -7651,7 +8837,7 @@ ], "support": { "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2" + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" }, "funding": [ { @@ -7663,27 +8849,27 @@ "type": "tidelift" } ], - "time": "2025-04-30T23:37:27+00:00" + "time": "2025-12-27T19:49:13+00:00" }, { "name": "voku/portable-ascii", - "version": "2.0.3", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/voku/portable-ascii.git", - "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d" + "reference": "8e1051fe39379367aecf014f41744ce7539a856f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", - "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/8e1051fe39379367aecf014f41744ce7539a856f", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f", "shasum": "" }, "require": { - "php": ">=7.0.0" + "php": ">=7.1.0" }, "require-dev": { - "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" + "phpunit/phpunit": "~8.5 || ~9.6 || ~10.5 || ~11.5" }, "suggest": { "ext-intl": "Use Intl for transliterator_transliterate() support" @@ -7713,7 +8899,7 @@ ], "support": { "issues": "https://github.com/voku/portable-ascii/issues", - "source": "https://github.com/voku/portable-ascii/tree/2.0.3" + "source": "https://github.com/voku/portable-ascii/tree/2.1.1" }, "funding": [ { @@ -7737,37 +8923,198 @@ "type": "tidelift" } ], - "time": "2024-11-21T01:49:47+00:00" + "time": "2026-04-26T05:33:54+00:00" }, { - "name": "webmozart/assert", - "version": "1.11.0", + "name": "web-auth/cose-lib", + "version": "4.5.2", "source": { "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + "url": "https://github.com/web-auth/cose-lib.git", + "reference": "5b38660f90070a8e45f3dbc9528ade3b608dd77d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "url": "https://api.github.com/repos/web-auth/cose-lib/zipball/5b38660f90070a8e45f3dbc9528ade3b608dd77d", + "reference": "5b38660f90070a8e45f3dbc9528ade3b608dd77d", + "shasum": "" + }, + "require": { + "brick/math": "^0.9|^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17", + "ext-json": "*", + "ext-openssl": "*", + "php": ">=8.1", + "spomky-labs/pki-framework": "^1.0" + }, + "require-dev": { + "spomky-labs/cbor-php": "^3.2.2" + }, + "suggest": { + "ext-bcmath": "For better performance, please install either GMP (recommended) or BCMath extension", + "ext-gmp": "For better performance, please install either GMP (recommended) or BCMath extension", + "spomky-labs/cbor-php": "For COSE Signature support" + }, + "type": "library", + "autoload": { + "psr-4": { + "Cose\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-auth/cose/contributors" + } + ], + "description": "CBOR Object Signing and Encryption (COSE) For PHP", + "homepage": "https://github.com/web-auth", + "keywords": [ + "COSE", + "RFC8152" + ], + "support": { + "issues": "https://github.com/web-auth/cose-lib/issues", + "source": "https://github.com/web-auth/cose-lib/tree/4.5.2" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2026-05-03T09:49:50+00:00" + }, + { + "name": "web-auth/webauthn-lib", + "version": "5.3.5", + "source": { + "type": "git", + "url": "https://github.com/web-auth/webauthn-lib.git", + "reference": "9e0986d999f4102e24ac8a598d3a80d98b56c19f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-auth/webauthn-lib/zipball/9e0986d999f4102e24ac8a598d3a80d98b56c19f", + "reference": "9e0986d999f4102e24ac8a598d3a80d98b56c19f", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-openssl": "*", + "paragonie/constant_time_encoding": "^2.6|^3.0", + "php": ">=8.2", + "phpdocumentor/reflection-docblock": "^5.3|^6.0", + "psr/clock": "^1.0", + "psr/event-dispatcher": "^1.0", + "psr/log": "^1.0|^2.0|^3.0", + "spomky-labs/cbor-php": "^3.0", + "spomky-labs/pki-framework": "^1.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^3.2", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "web-auth/cose-lib": "^4.2.3" + }, + "suggest": { + "psr/log-implementation": "Recommended to receive logs from the library", + "symfony/event-dispatcher": "Recommended to use dispatched events", + "web-token/jwt-library": "Mandatory for fetching Metadata Statement from distant sources" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/web-auth/webauthn-framework", + "name": "web-auth/webauthn-framework" + } + }, + "autoload": { + "psr-4": { + "Webauthn\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-auth/webauthn-library/contributors" + } + ], + "description": "FIDO2/Webauthn Support For PHP", + "homepage": "https://github.com/web-auth", + "keywords": [ + "FIDO2", + "fido", + "webauthn" + ], + "support": { + "source": "https://github.com/web-auth/webauthn-lib/tree/5.3.5" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2026-05-31T15:00:08+00:00" + }, + { + "name": "webmozart/assert", + "version": "2.4.1", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/2ccb7c2e821038c03a3e6e1700c570c158c55f70", + "reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70", "shasum": "" }, "require": { "ext-ctype": "*", - "php": "^7.2 || ^8.0" + "ext-date": "*", + "ext-filter": "*", + "php": "^8.2" }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" }, "type": "library", "extra": { + "psalm": { + "pluginClass": "Webmozart\\Assert\\PsalmPlugin" + }, "branch-alias": { - "dev-master": "1.10-dev" + "dev-master": "2.0-dev", + "dev-feature/2-0": "2.0-dev" } }, "autoload": { @@ -7783,6 +9130,10 @@ { "name": "Bernhard Schussek", "email": "bschussek@gmail.com" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com" } ], "description": "Assertions to validate method input/output with nice error messages.", @@ -7793,9 +9144,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.11.0" + "source": "https://github.com/webmozarts/assert/tree/2.4.1" }, - "time": "2022-06-03T18:03:27+00:00" + "time": "2026-06-15T15:31:57+00:00" }, { "name": "wikimedia/composer-merge-plugin", @@ -8042,37 +9393,38 @@ }, { "name": "laravel/pail", - "version": "v1.2.3", + "version": "v1.2.7", "source": { "type": "git", "url": "https://github.com/laravel/pail.git", - "reference": "8cc3d575c1f0e57eeb923f366a37528c50d2385a" + "reference": "2f7d27dada8effc48b8c424445a69cca7007daaa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pail/zipball/8cc3d575c1f0e57eeb923f366a37528c50d2385a", - "reference": "8cc3d575c1f0e57eeb923f366a37528c50d2385a", + "url": "https://api.github.com/repos/laravel/pail/zipball/2f7d27dada8effc48b8c424445a69cca7007daaa", + "reference": "2f7d27dada8effc48b8c424445a69cca7007daaa", "shasum": "" }, "require": { "ext-mbstring": "*", - "illuminate/console": "^10.24|^11.0|^12.0", - "illuminate/contracts": "^10.24|^11.0|^12.0", - "illuminate/log": "^10.24|^11.0|^12.0", - "illuminate/process": "^10.24|^11.0|^12.0", - "illuminate/support": "^10.24|^11.0|^12.0", + "illuminate/console": "^10.24|^11.0|^12.0|^13.0", + "illuminate/contracts": "^10.24|^11.0|^12.0|^13.0", + "illuminate/log": "^10.24|^11.0|^12.0|^13.0", + "illuminate/process": "^10.24|^11.0|^12.0|^13.0", + "illuminate/support": "^10.24|^11.0|^12.0|^13.0", "nunomaduro/termwind": "^1.15|^2.0", "php": "^8.2", - "symfony/console": "^6.0|^7.0" + "symfony/console": "^6.0|^7.0|^8.0" }, "require-dev": { - "laravel/framework": "^10.24|^11.0|^12.0", + "laravel/framework": "^10.24|^11.0|^12.0|^13.0", "laravel/pint": "^1.13", - "orchestra/testbench-core": "^8.13|^9.0|^10.0", - "pestphp/pest": "^2.20|^3.0", - "pestphp/pest-plugin-type-coverage": "^2.3|^3.0", + "orchestra/testbench-core": "^8.13|^9.17|^10.8|^11.0", + "pestphp/pest": "^2.20|^3.0|^4.0", + "pestphp/pest-plugin-type-coverage": "^2.3|^3.0|^4.0", "phpstan/phpstan": "^1.12.27", - "symfony/var-dumper": "^6.3|^7.0" + "symfony/var-dumper": "^6.3|^7.0|^8.0", + "symfony/yaml": "^6.3|^7.0|^8.0" }, "type": "library", "extra": { @@ -8117,20 +9469,20 @@ "issues": "https://github.com/laravel/pail/issues", "source": "https://github.com/laravel/pail" }, - "time": "2025-06-05T13:55:57+00:00" + "time": "2026-05-20T22:24:57+00:00" }, { "name": "laravel/pint", - "version": "v1.25.1", + "version": "v1.29.3", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9" + "reference": "da1d1111a6aa2e082d2a388b194afe1ba0a05d14" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/5016e263f95d97670d71b9a987bd8996ade6d8d9", - "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9", + "url": "https://api.github.com/repos/laravel/pint/zipball/da1d1111a6aa2e082d2a388b194afe1ba0a05d14", + "reference": "da1d1111a6aa2e082d2a388b194afe1ba0a05d14", "shasum": "" }, "require": { @@ -8141,13 +9493,14 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.87.2", - "illuminate/view": "^11.46.0", - "larastan/larastan": "^3.7.1", - "laravel-zero/framework": "^11.45.0", + "friendsofphp/php-cs-fixer": "^3.95.8", + "illuminate/view": "^12.62.0", + "larastan/larastan": "^3.10.0", + "laravel-zero/framework": "^12.1.0", + "laravel/agent-detector": "^2.0.2", "mockery/mockery": "^1.6.12", - "nunomaduro/termwind": "^2.3.1", - "pestphp/pest": "^2.36.0" + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest": "^3.8.6" }, "bin": [ "builds/pint" @@ -8173,6 +9526,7 @@ "description": "An opinionated code formatter for PHP.", "homepage": "https://laravel.com", "keywords": [ + "dev", "format", "formatter", "lint", @@ -8183,33 +9537,33 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-09-19T02:57:12+00:00" + "time": "2026-06-16T15:34:04+00:00" }, { "name": "laravel/sail", - "version": "v1.45.0", + "version": "v1.63.0", "source": { "type": "git", "url": "https://github.com/laravel/sail.git", - "reference": "019a2933ff4a9199f098d4259713f9bc266a874e" + "reference": "51bbce3f803c1d386cabbb44e618c955a12ff5fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sail/zipball/019a2933ff4a9199f098d4259713f9bc266a874e", - "reference": "019a2933ff4a9199f098d4259713f9bc266a874e", + "url": "https://api.github.com/repos/laravel/sail/zipball/51bbce3f803c1d386cabbb44e618c955a12ff5fc", + "reference": "51bbce3f803c1d386cabbb44e618c955a12ff5fc", "shasum": "" }, "require": { - "illuminate/console": "^9.52.16|^10.0|^11.0|^12.0", - "illuminate/contracts": "^9.52.16|^10.0|^11.0|^12.0", - "illuminate/support": "^9.52.16|^10.0|^11.0|^12.0", + "illuminate/console": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "illuminate/contracts": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^9.52.16|^10.0|^11.0|^12.0|^13.0", "php": "^8.0", - "symfony/console": "^6.0|^7.0", - "symfony/yaml": "^6.0|^7.0" + "symfony/console": "^6.0|^7.0|^8.0", + "symfony/yaml": "^6.0|^7.0|^8.0" }, "require-dev": { - "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", - "phpstan/phpstan": "^1.10" + "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0|^11.0", + "phpstan/phpstan": "^2.0" }, "bin": [ "bin/sail" @@ -8246,36 +9600,36 @@ "issues": "https://github.com/laravel/sail/issues", "source": "https://github.com/laravel/sail" }, - "time": "2025-08-25T19:28:31+00:00" + "time": "2026-06-18T08:54:14+00:00" }, { "name": "laravel/telescope", - "version": "v5.11.4", + "version": "v5.20.0", "source": { "type": "git", "url": "https://github.com/laravel/telescope.git", - "reference": "8b7bd77857d6b1b8c9362560cde74911cee68ab8" + "reference": "38ec6e6006a67e05e0c476c5f8ef3550b72e43d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/telescope/zipball/8b7bd77857d6b1b8c9362560cde74911cee68ab8", - "reference": "8b7bd77857d6b1b8c9362560cde74911cee68ab8", + "url": "https://api.github.com/repos/laravel/telescope/zipball/38ec6e6006a67e05e0c476c5f8ef3550b72e43d8", + "reference": "38ec6e6006a67e05e0c476c5f8ef3550b72e43d8", "shasum": "" }, "require": { "ext-json": "*", - "laravel/framework": "^8.37|^9.0|^10.0|^11.0|^12.0", + "laravel/framework": "^8.37|^9.0|^10.0|^11.0|^12.0|^13.0", + "laravel/sentinel": "^1.0", "php": "^8.0", - "symfony/console": "^5.3|^6.0|^7.0", - "symfony/var-dumper": "^5.0|^6.0|^7.0" + "symfony/console": "^5.3|^6.0|^7.0|^8.0", + "symfony/var-dumper": "^5.0|^6.0|^7.0|^8.0" }, "require-dev": { "ext-gd": "*", "guzzlehttp/guzzle": "^6.0|^7.0", - "laravel/octane": "^1.4|^2.0|dev-develop", - "orchestra/testbench": "^6.40|^7.37|^8.17|^9.0|^10.0", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.0|^10.5|^11.5" + "laravel/octane": "^1.4|^2.0", + "orchestra/testbench": "^6.47.1|^7.55|^8.36|^9.15|^10.8|^11.0", + "phpstan/phpstan": "^1.10" }, "type": "library", "extra": { @@ -8313,9 +9667,9 @@ ], "support": { "issues": "https://github.com/laravel/telescope/issues", - "source": "https://github.com/laravel/telescope/tree/v5.11.4" + "source": "https://github.com/laravel/telescope/tree/v5.20.0" }, - "time": "2025-09-12T14:36:07+00:00" + "time": "2026-04-06T12:52:26+00:00" }, { "name": "mockery/mockery", @@ -8462,39 +9816,36 @@ }, { "name": "nunomaduro/collision", - "version": "v8.8.2", + "version": "v8.9.4", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb" + "reference": "716af8f95a470e9094cfca09ed897b023be191a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/60207965f9b7b7a4ce15a0f75d57f9dadb105bdb", - "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/716af8f95a470e9094cfca09ed897b023be191a5", + "reference": "716af8f95a470e9094cfca09ed897b023be191a5", "shasum": "" }, "require": { - "filp/whoops": "^2.18.1", - "nunomaduro/termwind": "^2.3.1", + "filp/whoops": "^2.18.4", + "nunomaduro/termwind": "^2.4.0", "php": "^8.2.0", - "symfony/console": "^7.3.0" + "symfony/console": "^7.4.8 || ^8.0.8" }, "conflict": { - "laravel/framework": "<11.44.2 || >=13.0.0", - "phpunit/phpunit": "<11.5.15 || >=13.0.0" + "laravel/framework": "<11.48.0 || >=14.0.0", + "phpunit/phpunit": "<11.5.50 || >=14.0.0" }, "require-dev": { - "brianium/paratest": "^7.8.3", - "larastan/larastan": "^3.4.2", - "laravel/framework": "^11.44.2 || ^12.18", - "laravel/pint": "^1.22.1", - "laravel/sail": "^1.43.1", - "laravel/sanctum": "^4.1.1", - "laravel/tinker": "^2.10.1", - "orchestra/testbench-core": "^9.12.0 || ^10.4", - "pestphp/pest": "^3.8.2", - "sebastian/environment": "^7.2.1 || ^8.0" + "brianium/paratest": "^7.8.5", + "larastan/larastan": "^3.9.6", + "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.5.0", + "laravel/pint": "^1.29.1", + "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.2.1", + "pestphp/pest": "^3.8.5 || ^4.4.3 || ^5.0.0", + "sebastian/environment": "^7.2.1 || ^8.0.4 || ^9.3.0" }, "type": "library", "extra": { @@ -8557,7 +9908,7 @@ "type": "patreon" } ], - "time": "2025-06-25T02:12:12+00:00" + "time": "2026-04-21T14:04:20+00:00" }, { "name": "phar-io/manifest", @@ -8679,35 +10030,35 @@ }, { "name": "phpunit/php-code-coverage", - "version": "11.0.11", + "version": "11.0.12", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4" + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4", - "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^5.4.0", + "nikic/php-parser": "^5.7.0", "php": ">=8.2", "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-text-template": "^4.0.1", "sebastian/code-unit-reverse-lookup": "^4.0.1", "sebastian/complexity": "^4.0.1", - "sebastian/environment": "^7.2.0", + "sebastian/environment": "^7.2.1", "sebastian/lines-of-code": "^3.0.1", "sebastian/version": "^5.0.2", - "theseer/tokenizer": "^1.2.3" + "theseer/tokenizer": "^1.3.1" }, "require-dev": { - "phpunit/phpunit": "^11.5.2" + "phpunit/phpunit": "^11.5.46" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -8745,7 +10096,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.11" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.12" }, "funding": [ { @@ -8765,32 +10116,32 @@ "type": "tidelift" } ], - "time": "2025-08-27T14:37:49+00:00" + "time": "2025-12-24T07:01:01+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "5.1.0", + "version": "5.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6" + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6", - "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "5.1-dev" } }, "autoload": { @@ -8818,15 +10169,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" } ], - "time": "2024-08-27T05:02:59+00:00" + "time": "2026-02-02T13:52:54+00:00" }, { "name": "phpunit/php-invoker", @@ -9014,16 +10377,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.39", + "version": "11.5.55", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "ad5597f79d8489d2870073ac0bc0dd0ad1fa9931" + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ad5597f79d8489d2870073ac0bc0dd0ad1fa9931", - "reference": "ad5597f79d8489d2870073ac0bc0dd0ad1fa9931", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00", "shasum": "" }, "require": { @@ -9037,19 +10400,20 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.2", - "phpunit/php-code-coverage": "^11.0.11", - "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-code-coverage": "^11.0.12", + "phpunit/php-file-iterator": "^5.1.1", "phpunit/php-invoker": "^5.0.1", "phpunit/php-text-template": "^4.0.1", "phpunit/php-timer": "^7.0.1", "sebastian/cli-parser": "^3.0.2", "sebastian/code-unit": "^3.0.3", - "sebastian/comparator": "^6.3.2", + "sebastian/comparator": "^6.3.3", "sebastian/diff": "^6.0.2", "sebastian/environment": "^7.2.1", - "sebastian/exporter": "^6.3.0", + "sebastian/exporter": "^6.3.2", "sebastian/global-state": "^7.0.2", "sebastian/object-enumerator": "^6.0.1", + "sebastian/recursion-context": "^6.0.3", "sebastian/type": "^5.1.3", "sebastian/version": "^5.0.2", "staabm/side-effects-detector": "^1.0.5" @@ -9095,7 +10459,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.39" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.55" }, "funding": [ { @@ -9119,7 +10483,7 @@ "type": "tidelift" } ], - "time": "2025-09-14T06:20:41+00:00" + "time": "2026-02-18T12:37:06+00:00" }, { "name": "sebastian/cli-parser", @@ -9293,16 +10657,16 @@ }, { "name": "sebastian/comparator", - "version": "6.3.2", + "version": "6.3.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8" + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/85c77556683e6eee4323e4c5468641ca0237e2e8", - "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", "shasum": "" }, "require": { @@ -9361,7 +10725,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.2" + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3" }, "funding": [ { @@ -9381,7 +10745,7 @@ "type": "tidelift" } ], - "time": "2025-08-10T08:07:46+00:00" + "time": "2026-01-24T09:26:40+00:00" }, { "name": "sebastian/complexity", @@ -9586,16 +10950,16 @@ }, { "name": "sebastian/exporter", - "version": "6.3.0", + "version": "6.3.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3" + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/3473f61172093b2da7de1fb5782e1f24cc036dc3", - "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74", "shasum": "" }, "require": { @@ -9609,7 +10973,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "6.1-dev" + "dev-main": "6.3-dev" } }, "autoload": { @@ -9652,15 +11016,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.0" + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-12-05T09:17:50+00:00" + "time": "2025-09-24T06:12:51+00:00" }, { "name": "sebastian/global-state", @@ -10149,28 +11525,28 @@ }, { "name": "symfony/yaml", - "version": "v7.3.3", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "d4f4a66866fe2451f61296924767280ab5732d9d" + "reference": "efb42bd2c6f4f3ccfd4683583449938b5fc146b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/d4f4a66866fe2451f61296924767280ab5732d9d", - "reference": "d4f4a66866fe2451f61296924767280ab5732d9d", + "url": "https://api.github.com/repos/symfony/yaml/zipball/efb42bd2c6f4f3ccfd4683583449938b5fc146b0", + "reference": "efb42bd2c6f4f3ccfd4683583449938b5fc146b0", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", + "php": ">=8.4.1", "symfony/polyfill-ctype": "^1.8" }, "conflict": { - "symfony/console": "<6.4" + "symfony/console": "<7.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0" + "symfony/console": "^7.4|^8.0", + "yaml/yaml-test-suite": "*" }, "bin": [ "Resources/bin/yaml-lint" @@ -10201,7 +11577,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.3.3" + "source": "https://github.com/symfony/yaml/tree/v8.1.0" }, "funding": [ { @@ -10221,20 +11597,20 @@ "type": "tidelift" } ], - "time": "2025-08-27T11:34:33+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "theseer/tokenizer", - "version": "1.2.3", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { @@ -10263,7 +11639,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { @@ -10271,7 +11647,7 @@ "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" + "time": "2025-11-17T20:03:58+00:00" } ], "aliases": [], @@ -10283,5 +11659,5 @@ "php": "^8.2" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/be/config/auth.php b/be/config/auth.php index 3f1a624..59abb17 100644 --- a/be/config/auth.php +++ b/be/config/auth.php @@ -122,4 +122,15 @@ return [ 'max_attempts' => (int) env('EMAIL_VERIFICATION_OTP_MAX_ATTEMPTS', 5), ], + /* + |-------------------------------------------------------------------------- + | Password Reset OTP + |-------------------------------------------------------------------------- + */ + + 'password_reset' => [ + 'expiry_minutes' => (int) env('PASSWORD_RESET_OTP_EXPIRY_MINUTES', 10), + 'max_attempts' => (int) env('PASSWORD_RESET_OTP_MAX_ATTEMPTS', 5), + ], + ]; diff --git a/be/config/browsershot.php b/be/config/browsershot.php new file mode 100644 index 0000000..ecf85f0 --- /dev/null +++ b/be/config/browsershot.php @@ -0,0 +1,36 @@ + env('BROWSERSHOT_NODE_BINARY'), + + 'npm_binary' => env('BROWSERSHOT_NPM_BINARY'), + + 'node_module_path' => env('BROWSERSHOT_NODE_MODULE_PATH', base_path()), + + 'chrome_path' => env('BROWSERSHOT_CHROME_PATH'), + + /* + |-------------------------------------------------------------------------- + | Chrome sandbox + |-------------------------------------------------------------------------- + | + | Disable sandbox in Docker/Linux containers where Chrome cannot run sandboxed. + | + */ + + 'no_sandbox' => env('BROWSERSHOT_NO_SANDBOX', false), + + 'timeout' => (int) env('BROWSERSHOT_TIMEOUT', 60), + +]; diff --git a/be/config/cors.php b/be/config/cors.php index 28c3045..715d028 100644 --- a/be/config/cors.php +++ b/be/config/cors.php @@ -20,13 +20,15 @@ return [ 'allowed_methods' => ['*'], 'allowed_origins' => [ + 'http://localhost', + 'http://127.0.0.1', 'http://localhost:5173', 'http://127.0.0.1:5173', 'http://20.11.32.49', 'http://20.11.32.50', ], - 'allowed_origins_patterns' => ['*'], + 'allowed_origins_patterns' => [], 'allowed_headers' => ['*'], diff --git a/be/config/letter.php b/be/config/letter.php new file mode 100644 index 0000000..dad463b --- /dev/null +++ b/be/config/letter.php @@ -0,0 +1,85 @@ + [ + 'format' => 'A4', + 'width_mm' => 210, + 'height_mm' => 297, + 'margin_top_mm' => 20, + 'margin_right_mm' => 10, + 'margin_bottom_mm' => 1, + 'margin_left_mm' => 10, + ], + + /* + |-------------------------------------------------------------------------- + | Content + |-------------------------------------------------------------------------- + */ + + 'content' => [ + 'padding_x_mm' => 10, + ], + + /* + |-------------------------------------------------------------------------- + | Typography + |-------------------------------------------------------------------------- + */ + + 'typography' => [ + 'font_family' => 'Arial, Helvetica, sans-serif', + 'font_size' => '11pt', + 'line_height' => 1.5, + 'body_color' => '#1a1a1a', + 'signatory_title_size' => '10pt', + 'signatory_title_color' => '#333333', + 'footer_font_size' => '10pt', + 'footer_line_height' => 1.45, + 'letterhead_registration_size' => '9pt', + ], + + /* + |-------------------------------------------------------------------------- + | Spacing (pixels in CSS) + |-------------------------------------------------------------------------- + */ + + 'spacing' => [ + 'letterhead_margin_bottom' => 20, + 'meta_margin_bottom' => 20, + 'recipient_margin_bottom' => 16, + 'subject_margin_bottom' => 16, + 'body_margin_bottom' => 20, + 'body_paragraph_margin_bottom' => 10, + 'signature_block_margin_bottom' => 20, + 'signature_company_margin_bottom' => 16, + 'signature_space_height' => 48, + 'logo_height' => 80, + ], + + /* + |-------------------------------------------------------------------------- + | Brand colours + |-------------------------------------------------------------------------- + */ + + 'colors' => [ + 'blue' => '#0e579b', + 'orange' => '#d35400', + 'white' => '#ffffff', + 'black' => '#000000', + ], + +]; diff --git a/be/config/mail.php b/be/config/mail.php index 522b284..c875aaf 100644 --- a/be/config/mail.php +++ b/be/config/mail.php @@ -115,4 +115,15 @@ return [ 'name' => env('MAIL_FROM_NAME', 'Example'), ], + /* + |-------------------------------------------------------------------------- + | Email Logo + |-------------------------------------------------------------------------- + | + | Path to the logo embedded in transactional email templates. + | + */ + + 'logo_path' => env('MAIL_LOGO_PATH', public_path('images/logo/logo-kopkb-letterhead.svg')), + ]; diff --git a/be/database/migrations/2025_08_02_061334_create_documents_table.php b/be/database/migrations/2026_06_14_061334_create_documents_table.php similarity index 78% rename from be/database/migrations/2025_08_02_061334_create_documents_table.php rename to be/database/migrations/2026_06_14_061334_create_documents_table.php index 7530ea0..58a8006 100644 --- a/be/database/migrations/2025_08_02_061334_create_documents_table.php +++ b/be/database/migrations/2026_06_14_061334_create_documents_table.php @@ -13,19 +13,16 @@ return new class extends Migration { Schema::create('documents', function (Blueprint $table) { $table->uuid('id')->primary(); - $table->string('documentable_type'); - $table->unsignedBigInteger('documentable_id'); + $table->uuidMorphs('documentable'); $table->string('name'); $table->string('path', 500); $table->unsignedBigInteger('file_size')->nullable(); $table->string('mime_type', 100)->nullable(); $table->string('type', 100)->nullable()->default('general')->index(); $table->text('description')->nullable(); - $table->uuid('uploaded_by')->foreign('users')->onDelete('restrict'); + $table->foreignUuid('uploaded_by')->nullable()->constrained('users')->nullOnDelete(); $table->timestamps(); $table->softDeletes(); - - $table->index(['documentable_type', 'documentable_id']); }); } diff --git a/be/database/migrations/2026_06_25_120000_create_password_reset_otps_table.php b/be/database/migrations/2026_06_25_120000_create_password_reset_otps_table.php new file mode 100644 index 0000000..3a7409a --- /dev/null +++ b/be/database/migrations/2026_06_25_120000_create_password_reset_otps_table.php @@ -0,0 +1,33 @@ +uuid('id')->primary(); + $table->foreignUuid('user_id')->constrained('users')->cascadeOnDelete(); + $table->string('code'); + $table->timestamp('expires_at'); + $table->unsignedTinyInteger('attempts')->default(0); + $table->timestamps(); + + $table->index(['user_id', 'expires_at']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('password_reset_otps'); + } +}; diff --git a/be/docker/common/unified/Dockerfile b/be/docker/common/unified/Dockerfile index c586227..8a506eb 100644 --- a/be/docker/common/unified/Dockerfile +++ b/be/docker/common/unified/Dockerfile @@ -4,15 +4,15 @@ FROM node:22 AS frontend-builder WORKDIR /app/frontend # Copy frontend package files -COPY SUTERA-frontend/package*.json ./ -COPY SUTERA-frontend/pnpm-lock.yaml* ./ +COPY fe/package*.json ./ +COPY fe/pnpm-lock.yaml* ./ # Install pnpm and frontend dependencies (including dev dependencies for build) RUN npm install -g pnpm RUN pnpm install --frozen-lockfile # Copy frontend source code -COPY SUTERA-frontend/ ./ +COPY fe/ ./ # Build TWO frontend bundles (path-based): # - Production: served at / @@ -67,7 +67,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ WORKDIR /var/www # Copy the entire Laravel application code into the container -COPY SUTERA-backend/ /var/www +COPY be/ /var/www # Copy Composer from official image (avoids flaky getcomposer.org in CI) COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer @@ -115,11 +115,11 @@ RUN curl -o /usr/local/bin/php-fpm-healthcheck \ && chmod +x /usr/local/bin/php-fpm-healthcheck # Copy the initialization script -COPY SUTERA-backend/docker/common/unified/entrypoint.sh /usr/local/bin/entrypoint.sh +COPY be/docker/common/unified/entrypoint.sh /usr/local/bin/entrypoint.sh RUN chmod +x /usr/local/bin/entrypoint.sh # Copy supervisor configuration files -COPY SUTERA-backend/docker/common/unified/supervisor/supervisord.conf /etc/supervisor/supervisord.conf +COPY be/docker/common/unified/supervisor/supervisord.conf /etc/supervisor/supervisord.conf # Create supervisor conf.d directory RUN mkdir -p /etc/supervisor/conf.d @@ -129,11 +129,11 @@ RUN mkdir -p /etc/supervisor/conf.d # by mounting files into: # - /etc/nginx/nginx.conf # - /etc/supervisor/conf.d/ -COPY SUTERA-backend/docker/common/unified/supervisor/unified.conf /etc/supervisor/conf.d/unified.conf -COPY SUTERA-backend/docker/common/unified/nginx/nginx.conf /etc/nginx/nginx.conf +COPY be/docker/common/unified/supervisor/unified.conf /etc/supervisor/conf.d/unified.conf +COPY be/docker/common/unified/nginx/nginx.conf /etc/nginx/nginx.conf # Copy the initial storage structure -COPY SUTERA-backend/storage /var/www/storage-init +COPY be/storage /var/www/storage-init # Copy PHP extensions and libraries from the builder stage COPY --from=builder /usr/local/lib/php/extensions/ /usr/local/lib/php/extensions/ diff --git a/be/docker/production/docker-compose.production.yml b/be/docker/production/docker-compose.production.yml index 7bc1e8a..242719d 100644 --- a/be/docker/production/docker-compose.production.yml +++ b/be/docker/production/docker-compose.production.yml @@ -2,7 +2,7 @@ services: app: build: context: . - dockerfile: ./SUTERA-backend/docker/common/unified/Dockerfile + dockerfile: ./be/docker/common/unified/Dockerfile platforms: - linux/amd64 - linux/arm64 @@ -23,7 +23,7 @@ services: - app-storage-production:/var/www/storage - app-logs-production:/var/www/storage/logs - ./.env.production:/var/www/.env:ro - - ./SUTERA-backend/docker/production/nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./be/docker/production/nginx/nginx.conf:/etc/nginx/nginx.conf:ro networks: - sutera-production-network depends_on: diff --git a/be/docker/staging/docker-compose.staging.yml b/be/docker/staging/docker-compose.staging.yml index 4c13517..3217647 100644 --- a/be/docker/staging/docker-compose.staging.yml +++ b/be/docker/staging/docker-compose.staging.yml @@ -2,7 +2,7 @@ services: app: build: context: . - dockerfile: ./SUTERA-backend/docker/common/unified/Dockerfile + dockerfile: ./be/docker/common/unified/Dockerfile args: ENV: staging platforms: @@ -87,7 +87,7 @@ services: - POSTGRES_PASSWORD=${DB_PASSWORD} volumes: - postgres-data-staging:/var/lib/postgres - - ./SUTERA-backend/docker/postgres/conf.d:/etc/postgres/conf.d:ro + - ./be/docker/postgres/conf.d:/etc/postgres/conf.d:ro networks: - sutera-staging-network healthcheck: diff --git a/be/docker/training/docker-compose.training.yml b/be/docker/training/docker-compose.training.yml index 16efeae..030aae7 100644 --- a/be/docker/training/docker-compose.training.yml +++ b/be/docker/training/docker-compose.training.yml @@ -2,7 +2,7 @@ services: app: build: context: . - dockerfile: ./SUTERA-backend/docker/common/unified/Dockerfile + dockerfile: ./be/docker/common/unified/Dockerfile platforms: - linux/amd64 - linux/arm64 @@ -28,7 +28,7 @@ services: - app-storage-training:/var/www/storage - app-logs-training:/var/www/storage/logs - ./.env.training:/var/www/.env:ro - - ./SUTERA-backend/docker/training/nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./be/docker/training/nginx/nginx.conf:/etc/nginx/nginx.conf:ro networks: - sutera-training-network depends_on: @@ -84,7 +84,7 @@ services: - POSTGRES_PASSWORD=${DB_PASSWORD} volumes: - postgres-data-training:/var/lib/postgres - - ./SUTERA-backend/docker/postgres/conf.d:/etc/postgres/conf.d:ro + - ./be/docker/postgres/conf.d:/etc/postgres/conf.d:ro networks: - sutera-training-network healthcheck: diff --git a/be/modules_statuses.json b/be/modules_statuses.json index c645c10..c835c83 100644 --- a/be/modules_statuses.json +++ b/be/modules_statuses.json @@ -1,29 +1,11 @@ { "Auth": true, - "KJCVariant": true, - "Rank": true, - "Position": true, - "Unit": true, - "Formation": true, - "Camp": true, "User": true, - "KJC": true, - "KJCAssetEntitlement": true, - "KJCAssetHolding": true, - "KJCSparePart": true, - "KJCRepair": true, - "KJCRepairDirective": true, - "PKJAssetEntitlement": true, - "PKJVariant": true, - "PKJAssetHolding": true, "ActivityLog": true, "Role": true, - "KJCReport": true, "Dashboard": true, - "PKJReport": true, "ExternalAPI": true, - "KJCTemporaryLoan": true, "Notification": true, - "KJCRepairTool": true, - "Feedback": true + "MembershipApplication": true, + "Activity": true } \ No newline at end of file diff --git a/be/package-lock.json b/be/package-lock.json new file mode 100644 index 0000000..10bc260 --- /dev/null +++ b/be/package-lock.json @@ -0,0 +1,2761 @@ +{ + "name": "html", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "puppeteer": "^25.2.0" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "axios": "^1.11.0", + "concurrently": "^9.0.1", + "laravel-vite-plugin": "^2.0.0", + "tailwindcss": "^4.0.0", + "vite": "^7.0.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.0.5.tgz", + "integrity": "sha512-xYXNuEQmHNIPWWcbL/skf2KF7seyp7c1xmKFRk3wmdFx7VwBsKVrtOLKs8ecaezsKPsWeF1YsgwIiElAscaryA==", + "license": "Apache-2.0", + "dependencies": { + "modern-tar": "^0.7.6", + "yargs": "^18.0.0" + }, + "bin": { + "browsers": "lib/main-cli.js" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1" + }, + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + } + } + }, + "node_modules/@puppeteer/browsers/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@puppeteer/browsers/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@puppeteer/browsers/node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@puppeteer/browsers/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/@puppeteer/browsers/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@puppeteer/browsers/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@puppeteer/browsers/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@puppeteer/browsers/node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/@puppeteer/browsers/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.1.tgz", + "integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "5.21.6", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.1" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.1.tgz", + "integrity": "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.1", + "@tailwindcss/oxide-darwin-arm64": "4.3.1", + "@tailwindcss/oxide-darwin-x64": "4.3.1", + "@tailwindcss/oxide-freebsd-x64": "4.3.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.1", + "@tailwindcss/oxide-linux-x64-musl": "4.3.1", + "@tailwindcss/oxide-wasm32-wasi": "4.3.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.1" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz", + "integrity": "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.1.tgz", + "integrity": "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.1.tgz", + "integrity": "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.1.tgz", + "integrity": "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.1.tgz", + "integrity": "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.1.tgz", + "integrity": "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.1.tgz", + "integrity": "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz", + "integrity": "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz", + "integrity": "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz", + "integrity": "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz", + "integrity": "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.1.tgz", + "integrity": "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.1.tgz", + "integrity": "sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.1", + "@tailwindcss/oxide": "4.3.1", + "tailwindcss": "4.3.1" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chromium-bidi": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-16.0.1.tgz", + "integrity": "sha512-J63PGu/9PpeCwLIcKYyzWP6yaVL5pxuBc0shlYCYM8BaAkmlwiQboXO1iNbOgSDbVklEyYFfNEcHD8oOAWacUA==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concurrently": { + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.3.tgz", + "integrity": "sha512-ihjs0E2SxvDgq/MK418hX6YycQgKhsqxpbZuZbHo0yKfqDWdymWMjWYIpCIzqDDLLKClHlXev8whW/8WXmJ0BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.4", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1638949", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1638949.tgz", + "integrity": "sha512-mXwg4Fqnv0WR4iuAT/gYUmctNkjILwXFHyZ+m7Ty1dfr0ezZt2U3gnrrJTfRobJTHoXf+IbuFvFITzLrLFjwJA==", + "license": "BSD-3-Clause" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.21.6", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/laravel-vite-plugin": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-2.1.0.tgz", + "integrity": "sha512-z+ck2BSV6KWtYcoIzk9Y5+p4NEjqM+Y4i8/H+VZRLq0OgNjW2DqyADquwYu5j8qRvaXwzNmfCWl1KrMlV1zpsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "vite-plugin-full-reload": "^1.1.0" + }, + "bin": { + "clean-orphaned-assets": "bin/clean.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^7.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/modern-tar": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.6.tgz", + "integrity": "sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/puppeteer": { + "version": "25.2.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.2.0.tgz", + "integrity": "sha512-JPMPd/2+lgdkLhEyPqH895oR3ccMt1wSra6oewgjjTuLmo2s9zPZpKXQTFEIiA/fMKpiL01kjU3+2zPEReRWNg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "3.0.5", + "chromium-bidi": "16.0.1", + "devtools-protocol": "0.0.1638949", + "lilconfig": "^3.1.3", + "puppeteer-core": "25.2.0", + "typed-query-selector": "^2.12.2" + }, + "bin": { + "puppeteer": "lib/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/puppeteer-core": { + "version": "25.2.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.2.0.tgz", + "integrity": "sha512-jGhuGAlkgOcbyGRc0Cm9b/y4vvqoxhyAyl6a1diVe8F3sHsgTaQ60QQT5F3rGegTZV3prysgHVc+0LsvPZo3GA==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "3.0.5", + "chromium-bidi": "16.0.1", + "devtools-protocol": "0.0.1638949", + "typed-query-selector": "^2.12.2", + "webdriver-bidi-protocol": "0.4.2", + "ws": "^8.21.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/shell-quote": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", + "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz", + "integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/typed-query-selector": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", + "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", + "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-full-reload": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.2.0.tgz", + "integrity": "sha512-kz18NW79x0IHbxRSHm0jttP4zoO9P9gXh+n6UTwlNKnviTTEpOlum6oS9SmecrTtSr+muHEn5TUuC75UovQzcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "picomatch": "^2.3.1" + } + }, + "node_modules/vite-plugin-full-reload/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz", + "integrity": "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA==", + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/be/package.json b/be/package.json index a5707d8..63e1661 100644 --- a/be/package.json +++ b/be/package.json @@ -13,5 +13,8 @@ "laravel-vite-plugin": "^2.0.0", "tailwindcss": "^4.0.0", "vite": "^7.0.4" + }, + "dependencies": { + "puppeteer": "^25.2.0" } } diff --git a/be/public/images/logo/Logo-RAJD.png b/be/public/images/logo/Logo-RAJD.png deleted file mode 100644 index 37e434c..0000000 Binary files a/be/public/images/logo/Logo-RAJD.png and /dev/null differ diff --git a/be/public/images/logo/logo-kopkb-letterhead.svg b/be/public/images/logo/logo-kopkb-letterhead.svg new file mode 100644 index 0000000..5c3ea85 --- /dev/null +++ b/be/public/images/logo/logo-kopkb-letterhead.svg @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/be/public/images/logo/logo-kopkb.svg b/be/public/images/logo/logo-kopkb.svg new file mode 100644 index 0000000..ef88ccb --- /dev/null +++ b/be/public/images/logo/logo-kopkb.svg @@ -0,0 +1,418 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/be/resources/views/emails/partials/footer.blade.php b/be/resources/views/emails/partials/footer.blade.php new file mode 100644 index 0000000..7e275d9 --- /dev/null +++ b/be/resources/views/emails/partials/footer.blade.php @@ -0,0 +1,2 @@ +Terima kasih,
+{{ config('app.name') }} diff --git a/be/resources/views/emails/partials/header.blade.php b/be/resources/views/emails/partials/header.blade.php new file mode 100644 index 0000000..628f5a8 --- /dev/null +++ b/be/resources/views/emails/partials/header.blade.php @@ -0,0 +1,9 @@ +@php + $logoPath = $logoPath ?? config('mail.logo_path'); +@endphp + +@if (! empty($logoPath) && file_exists($logoPath) && isset($message)) +
+ {{ config('app.name') }} +
+@endif diff --git a/be/resources/views/pdf/layouts/letter.blade.php b/be/resources/views/pdf/layouts/letter.blade.php new file mode 100644 index 0000000..e649a3a --- /dev/null +++ b/be/resources/views/pdf/layouts/letter.blade.php @@ -0,0 +1,380 @@ +{{-- + Reusable base layout for official PDF letters. + + Extend this layout and fill the sections below: + + @extends('pdf.layouts.letter') + + @section('letter-reference') Ruj: APP-2026-00001 @endsection + @section('letter-date') {{ now()->format('d F Y') }} @endsection + @section('letter-recipient') ... @endsection + @section('letter-subject') Perkara: ... @endsection + @section('letter-salutation') Assalamualaikum dan Salam Sejahtera, @endsection + @section('letter-body') ... @endsection + @section('letter-closing') Yang benar, @endsection + @section('letter-signature') ... @endsection + + Optional overrides via view data: + - logoPath, organizationAddress, organizationContact + - showReference, showDate, showSubject (bool, default true) +--}} +@php + $showReference = $showReference ?? true; + $showDate = $showDate ?? true; + $showSubject = $showSubject ?? true; + $letterDate = trim($__env->yieldContent('letter-date')); + $css = ($letterLayout ?? \App\Support\LetterLayout::resolve())['css']; +@endphp + + + + + @yield('letter-title', trim($__env->yieldContent('letter-subject', 'Surat Rasmi'))) + + @stack('letter-styles') + + +
+ @include('pdf.partials.letterhead') + +
+
+ @if ($showReference && trim($__env->yieldContent('letter-reference')) !== '') + + + + + +
Ruj.: @yield('letter-reference')
+ @endif + @if ($showDate && $letterDate !== '') +
{{ $letterDate }}
+ @endif +
+ + @hasSection('letter-recipient') +
+
@yield('letter-recipient')
+
+ @endif + + @if ($showSubject && trim($__env->yieldContent('letter-subject')) !== '') +
+ @yield('letter-subject') +
+ @endif + + @hasSection('letter-salutation') +
@yield('letter-salutation')
+ @endif + +
+ @yield('letter-body') +
+ + @hasSection('letter-closing') +
@yield('letter-closing')
+ @endif +
+ +
+ @hasSection('letter-signature') +
@yield('letter-signature')
+ @endif + + +
+
+ + @stack('letter-scripts') + + diff --git a/be/resources/views/pdf/partials/footer-icon.blade.php b/be/resources/views/pdf/partials/footer-icon.blade.php new file mode 100644 index 0000000..cc9dd0d --- /dev/null +++ b/be/resources/views/pdf/partials/footer-icon.blade.php @@ -0,0 +1,30 @@ +@php + $color = $color ?? '#ffffff'; +@endphp +@switch($icon) + @case('phone') + + + + @break + @case('fax') + + + + @break + @case('email') + + + + @break + @case('web') + + + + @break + @case('location') + + + + @break +@endswitch diff --git a/be/resources/views/pdf/partials/letterfooter.blade.php b/be/resources/views/pdf/partials/letterfooter.blade.php new file mode 100644 index 0000000..de80b00 --- /dev/null +++ b/be/resources/views/pdf/partials/letterfooter.blade.php @@ -0,0 +1,38 @@ + + + + + + diff --git a/be/resources/views/pdf/partials/letterhead.blade.php b/be/resources/views/pdf/partials/letterhead.blade.php new file mode 100644 index 0000000..e27b900 --- /dev/null +++ b/be/resources/views/pdf/partials/letterhead.blade.php @@ -0,0 +1,28 @@ + + + + + +
+ + + + + +
  
+
+ + + @if (! empty($logoUrl)) + + @endif + + +
+
KOPERASI PERMODALAN
+
KELANTAN BERHAD
+
D-5-0228
+
+
diff --git a/be/resources/views/pdf/preview-approved-membership.blade.php b/be/resources/views/pdf/preview-approved-membership.blade.php new file mode 100644 index 0000000..89c7937 --- /dev/null +++ b/be/resources/views/pdf/preview-approved-membership.blade.php @@ -0,0 +1,72 @@ +@extends('pdf.layouts.letter') + +@section('letter-title', 'Surat') + +@section('letter-reference', 'APP-2026-00001') + +@section('letter-date', now()->translatedFormat('d F Y')) + +@section('letter-recipient') + Ahmad bin Abdullah
+ No. 12, Jalan Contoh,
+ 43000 Kajang, Selangor +@endsection + +@section('letter-subject') + KELULUSAN KEANGGOTAAN KOPERASI PERMODALAN KELANTAN BERHAD +@endsection + +{{-- @section('letter-salutation') + Assalamualaikum dan Salam Sejahtera, +@endsection --}} + +@section('letter-body') +

+ Dengan segala hormatnya saya merujuk kepada perkara di atas. +

+

+ 2. Sukacita dimaklumkan bahawa, permohonan tuan untuk menjadi anggota Koperasi + Permodalan Kelantan Berhad(KoPKB) telah DILULUSKAN dalam Mesyuarat Lembaga + Koperasi Bil. 4-2025/2026 pada 24 Jun 2026. +

+

+ 3. Pihak Koperasi akan membuat potongan gaji melalui majikan tuan sebagaiman ketetapan berikut:- +

    +
  • Saham
  • +
      +
    • Potongan sebanyak RM 84.00 (Mei 2026 - Jun 2026)
    • +
    • Potongan sebanyak RM 50.00 sehingga Syer Maksima
    • +
    +
  • Yuran
  • +
      +
    • Potongan sebanyak RM 50.00 setiap bulan
    • +
    +
  • Fi Masuk
  • +
      +
    • Potongan sebanyak RM 10.00 (Pembayaran Pertama)
    • +
    +
+

+

+ 4. Untuk makluman tuan, kelulusan keanggotaan ini adalah bersyarat di mana tuan dikehendaki + menjelaskan modal syer minimum sebanyak RM500.00. Bayaran tersebut + perlu dibuat secara ansuran melalui potongan gaji dalam tempoh enam (6) bulan. + Pemenuhan syarat ini adalah bagi melayakkan tuan memperoleh hak, kewajipan dan + liabiliti sebagai anggota KoPKB sebagaimana terkandung di dalam Undang-Undang Kecil (UUK), + Perkara 59 yang telah didaftarkan pada 11 September 2024. Manakala had maksima modal syer adalah + sebanyak RM5,000.00. +

+@endsection + +@section('letter-closing') + Sekian, terima kasih. +@endsection + +@section('letter-signature') +
Dengan hormatnya,
+
KOPERASI PERMODALAN KELANTAN BERHAD
+
+
MUHAMMAD NAFIS BIN ZAINUDDIN
+
Pengurus Besar
+
b/p Setiausaha
+@endsection diff --git a/be/resources/views/pdf/preview.blade.php b/be/resources/views/pdf/preview.blade.php new file mode 100644 index 0000000..e81357a --- /dev/null +++ b/be/resources/views/pdf/preview.blade.php @@ -0,0 +1,43 @@ +@extends('pdf.layouts.letter') + +@section('letter-title', 'Pratonton Surat') + +@section('letter-reference', 'APP-2026-00001') + +@section('letter-date', now()->translatedFormat('d F Y')) + +@section('letter-recipient') + Ahmad bin Abdullah
+ No. 12, Jalan Contoh,
+ 43000 Kajang, Selangor +@endsection + +@section('letter-subject') + PERKARA: PRATONTON REKA BENTUK SURAT RASMI +@endsection + +@section('letter-salutation') + Assalamualaikum dan Salam Sejahtera, +@endsection + +@section('letter-body') +

+ Dengan hormatnya perkara di atas adalah dirujuk. +

+

+ 1. Ini adalah kandungan contoh untuk pratonton reka bentuk surat. Gunakan halaman ini + untuk menguji susun atur, fon, jarak, dan elemen letterhead sebelum menghasilkan PDF sebenar. +

+

+ 2. Teks ini boleh diganti apabila templat surat muktamad disediakan. +

+@endsection + +@section('letter-closing') + Sekian, terima kasih. +@endsection + +@section('letter-signature') +
NAMA PENGARAH
+
Jawatan / {{ config('app.name') }}
+@endsection diff --git a/be/routes/api.php b/be/routes/api.php index 0c60ae2..b16345e 100644 --- a/be/routes/api.php +++ b/be/routes/api.php @@ -1,32 +1,3 @@ middleware(['auth:sanctum', 'single.session']); - -// Social Media - Admin endpoints -Route::middleware(['auth:sanctum', 'single.session'])->prefix('social-media')->group(function () { - Route::get('/settings', [SocialMediaController::class, 'getSocialMediaSettings']); - Route::post('/settings', [SocialMediaController::class, 'updateSocialMedia']); - Route::post('/', [SocialMediaController::class, 'addSocialMedia']); - Route::put('/{id}', [SocialMediaController::class, 'updateSocialMediaPlatform']); - Route::delete('/{id}', [SocialMediaController::class, 'deleteSocialMediaPlatform']); - Route::patch('/{id}/toggle', [SocialMediaController::class, 'toggleSocialMediaPlatform']); -}); - -// Contact Settings - Admin endpoints -Route::middleware(['auth:sanctum', 'single.session'])->prefix('contact')->group(function () { - Route::get('/settings', [ContactController::class, 'getContactSettings']); - Route::post('/settings', [ContactController::class, 'updateContacts']); - Route::post('/', [ContactController::class, 'addContact']); - Route::put('/{id}', [ContactController::class, 'updateContact']); - Route::delete('/{id}', [ContactController::class, 'deleteContact']); - Route::patch('/{id}/toggle', [ContactController::class, 'toggleContact']); -}); - +use Illuminate\Support\Facades\Route; \ No newline at end of file diff --git a/be/routes/web.php b/be/routes/web.php index bfe8a20..a4def31 100644 --- a/be/routes/web.php +++ b/be/routes/web.php @@ -1,7 +1,14 @@ name('letters.preview'); + +Route::get('/letters/preview-approved-membership', [LetterPreviewController::class, 'showMembershipApprovedLetter']) + ->name('letters.preview-membership-approved-membership'); \ No newline at end of file diff --git a/docs/SENARAI DAFTAR ANGGOTA 2026.xls b/docs/SENARAI DAFTAR ANGGOTA 2026.xls deleted file mode 100644 index 8011f86..0000000 Binary files a/docs/SENARAI DAFTAR ANGGOTA 2026.xls and /dev/null differ diff --git a/docs/membership_application-db_design.md b/docs/membership_application-db_design.md new file mode 100644 index 0000000..240423f --- /dev/null +++ b/docs/membership_application-db_design.md @@ -0,0 +1,283 @@ +The differences are actually quite small, but they're important because they reflect the **real business process** more accurately. + +## Earlier Design + +I initially assumed: + +```text +Management + | +Approve/Reject + | +Board + | +Pass/Fail + | +General Manager + | +Approve/Reject +``` + +Therefore I proposed: + +```text +application_management_reviews +application_board_evaluations +application_gm_decisions +``` + +or later: + +```text +application_reviews + - MANAGEMENT + - BOARD + - GM +``` + +because I thought the GM was making a decision. + +--- + +## Current Design + +After your clarification: + +> The outcome is already known after board evaluation. +> +> The GM only prints the result letter and notifies the applicant. + +The workflow is actually: + +```text +Management + | +Approve/Reject + | +Board + | +Pass/Fail + | +General Manager +(Print letter) +(Send email) +``` + +The GM is no longer a reviewer. + +Therefore: + +### Removed + +```text +application_gm_decisions +``` + +because there is no GM decision to store. + +--- + +### Added + +```text +application_notifications +``` + +because the GM's responsibility is administrative: + +* Generate acceptance/rejection letter +* Send email +* Mark notification completed + +Example: + +| application_id | letter_generated_at | email_sent_at | +| -------------- | ------------------- | ---------------- | +| 1001 | 2026-06-01 10:00 | 2026-06-01 10:05 | + +--- + +## Status Changes + +### Earlier + +```text +MANAGEMENT_REJECTED +GM_REJECTED +APPROVED +ACCOUNT_CREATED +``` + +### Current + +```text +MANAGEMENT_REJECTED +BOARD_FAILED +BOARD_PASSED +NOTIFICATION_PENDING +COMPLETED +``` + +because the board determines the outcome. + +--- + +## Review Tables + +### Earlier + +I proposed three tables: + +```text +application_management_reviews +application_board_evaluations +application_gm_decisions +``` + +Then simplified to: + +```text +application_reviews +``` + +with: + +```text +MANAGEMENT +BOARD +GM +``` + +### Current + +Still: + +```text +application_reviews +``` + +but only: + +```text +MANAGEMENT +BOARD +``` + +No GM stage. + +--- + +## What I Would Simplify Even Further + +Looking at your process, I'd actually make it even leaner. + +### membership_applications + +Add outcome fields directly: + +```sql +membership_applications +----------------------- +id + +status + +management_result +board_result + +notification_sent_at + +submitted_at +completed_at +``` + +Where: + +```text +management_result +----------------- +PENDING +APPROVED +REJECTED + +board_result +------------ +PENDING +PASS +FAIL +``` + +Then you might not even need `application_reviews` if: + +* One management review only +* One board evaluation only + +You could store everything directly on the application record. + +Example: + +| application | mgmt | board | +| ----------- | -------- | ----- | +| APP001 | APPROVED | PASS | +| APP002 | APPROVED | FAIL | +| APP003 | REJECTED | NULL | + +--- + +## My Recommendation + +For your described workflow, I'd use: + +```text +membership_applications +application_applicants +application_heirs +application_documents +application_references +application_reviews +application_notifications +users +members +``` + +and remove: + +```text +application_management_reviews +application_board_evaluations +application_gm_decisions +application_workflow_logs +``` + +unless you have a compliance/audit requirement. + +### Why remove `application_workflow_logs`? + +If your workflow is fixed and simple: + +* Management reviews once. +* Board evaluates once. +* GM sends notification once. + +Then: + +```text +application_reviews +``` + +already tells you who reviewed and when. + +A workflow log becomes useful only when you need things like: + +```text +Reopened +Returned for correction +Resubmitted +Reassigned +Escalated +Withdrawn +``` + +If those scenarios don't exist, it's additional complexity without much benefit. + +So the "latest" design is mainly different because **the GM is no longer treated as a decision-maker**, only as a notification handler. That removes an entire approval layer from the data model and simplifies the workflow considerably. diff --git a/fe/.DS_Store b/fe/.DS_Store index 531073e..bea6f35 100644 Binary files a/fe/.DS_Store and b/fe/.DS_Store differ diff --git a/fe/public/favicon.ico b/fe/public/favicon.ico index 84fbd8a..4aea46d 100644 Binary files a/fe/public/favicon.ico and b/fe/public/favicon.ico differ diff --git a/fe/src/.DS_Store b/fe/src/.DS_Store new file mode 100644 index 0000000..0c5a35f Binary files /dev/null and b/fe/src/.DS_Store differ diff --git a/fe/src/assets/.DS_Store b/fe/src/assets/.DS_Store new file mode 100644 index 0000000..6b03fd7 Binary files /dev/null and b/fe/src/assets/.DS_Store differ diff --git a/fe/src/components/daily-notes/DailyNotes.vue b/fe/src/components/daily-notes/DailyNotes.vue index 0b5be3e..e10bc45 100644 --- a/fe/src/components/daily-notes/DailyNotes.vue +++ b/fe/src/components/daily-notes/DailyNotes.vue @@ -21,7 +21,7 @@ import { TableRow, TableCell, } from '@/components/ui/table' -import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select' + import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { CarouselRoot, @@ -95,12 +95,7 @@ const sport = ref(false)
- +
{{ faker['news'][0]!['title'] }} @@ -113,12 +108,7 @@ const sport = ref(false) -
diff --git a/fe/src/components/notification-dropdown/NotificationDropdown.vue b/fe/src/components/notification-dropdown/NotificationDropdown.vue index da6e3d6..6f77938 100644 --- a/fe/src/components/notification-dropdown/NotificationDropdown.vue +++ b/fe/src/components/notification-dropdown/NotificationDropdown.vue @@ -1,9 +1,12 @@ diff --git a/fe/src/components/recent-activities/RecentActivities.vue b/fe/src/components/recent-activities/RecentActivities.vue index e68e89b..e77ccf2 100644 --- a/fe/src/components/recent-activities/RecentActivities.vue +++ b/fe/src/components/recent-activities/RecentActivities.vue @@ -21,7 +21,7 @@ import { TableRow, TableCell, } from '@/components/ui/table' -import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select' + import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { CarouselRoot, @@ -79,12 +79,9 @@ const sport = ref(false) Show More
+ class="before:bg-foreground/10 relative mt-5 before:absolute before:ms-5 before:mt-5 before:block before:h-[85%] before:w-px">
-
+
PA @@ -101,9 +98,7 @@ const sport = ref(false)
-
+
PA @@ -137,9 +132,7 @@ const sport = ref(false)
12 November
-
+
PA @@ -162,9 +155,7 @@ const sport = ref(false)
-
+
PA diff --git a/fe/src/components/schedules/Schedules.vue b/fe/src/components/schedules/Schedules.vue index d015de6..2a1e600 100644 --- a/fe/src/components/schedules/Schedules.vue +++ b/fe/src/components/schedules/Schedules.vue @@ -21,7 +21,7 @@ import { TableRow, TableCell, } from '@/components/ui/table' -import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select' + import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { CarouselRoot, @@ -141,22 +141,22 @@ const sport = ref(false)
+ class="bg-(--color)/20 border-(--color)/60 me-3 size-2 rounded-full border [--color:var(--color-pending)]"> +
UI/UX Workshop 23th
+ class="bg-(--color)/20 border-(--color)/60 me-3 size-2 rounded-full border [--color:var(--color-primary)]"> +
VueJs Frontend Development 10th
+ class="bg-(--color)/20 border-(--color)/60 me-3 size-2 rounded-full border [--color:var(--color-warning)]"> +
Laravel Rest API 31th
diff --git a/fe/src/components/transactions/Transactions.vue b/fe/src/components/transactions/Transactions.vue index c0957b0..b582b7e 100644 --- a/fe/src/components/transactions/Transactions.vue +++ b/fe/src/components/transactions/Transactions.vue @@ -21,7 +21,7 @@ import { TableRow, TableCell, } from '@/components/ui/table' -import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select' + import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { CarouselRoot, @@ -78,10 +78,7 @@ const sport = ref(false)

Transactions

- + PA @@ -92,12 +89,10 @@ const sport = ref(false) {{ faker['dates'][0] }}
-
+
{{ faker['trueFalse'][0] ? '+' : '-' }}${{ faker['totals'][0] }}
diff --git a/fe/src/components/ui/datepicker/DatePickerMonthSelect.vue b/fe/src/components/ui/datepicker/DatePickerMonthSelect.vue index 1b1ffb3..89cd30d 100644 --- a/fe/src/components/ui/datepicker/DatePickerMonthSelect.vue +++ b/fe/src/components/ui/datepicker/DatePickerMonthSelect.vue @@ -1,10 +1,6 @@ - - diff --git a/fe/src/components/ui/native-select/NativeSelectOption.vue b/fe/src/components/ui/native-select/NativeSelectOption.vue deleted file mode 100644 index 1d79dfe..0000000 --- a/fe/src/components/ui/native-select/NativeSelectOption.vue +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/fe/src/components/ui/native-select/NativeSelectOptionGroup.vue b/fe/src/components/ui/native-select/NativeSelectOptionGroup.vue deleted file mode 100644 index a365051..0000000 --- a/fe/src/components/ui/native-select/NativeSelectOptionGroup.vue +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/fe/src/components/ui/native-select/index.ts b/fe/src/components/ui/native-select/index.ts deleted file mode 100644 index b4b5fef..0000000 --- a/fe/src/components/ui/native-select/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { default as NativeSelect } from "./NativeSelect.vue"; -export { default as NativeSelectOption } from "./NativeSelectOption.vue"; -export { default as NativeSelectOptionGroup } from "./NativeSelectOptionGroup.vue"; diff --git a/fe/src/components/ui/textarea/Textarea.vue b/fe/src/components/ui/textarea/Textarea.vue index b594852..9aa563d 100644 --- a/fe/src/components/ui/textarea/Textarea.vue +++ b/fe/src/components/ui/textarea/Textarea.vue @@ -2,11 +2,13 @@ import { cn } from "@mykopkb/core/utils/cn"; import { textarea } from "@mykopkb/core/styles/textarea.styles"; -const { class: className, ...props } = defineProps<{ +const modelValue = defineModel(); + +const { class: className } = defineProps<{ class?: string; }>();