diff --git a/.env.example b/.env.example
index 2e82c21..30da688 100644
--- a/.env.example
+++ b/.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/Modules/Activity/Actions/.gitkeep b/Modules/Activity/Actions/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/Activity/Config/.gitkeep b/Modules/Activity/Config/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/Activity/Config/config.php b/Modules/Activity/Config/config.php
new file mode 100644
index 0000000..c4ab535
--- /dev/null
+++ b/Modules/Activity/Config/config.php
@@ -0,0 +1,5 @@
+ 'Activity',
+];
diff --git a/Modules/Activity/Console/.gitkeep b/Modules/Activity/Console/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/Activity/Database/Factories/.gitkeep b/Modules/Activity/Database/Factories/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/Activity/Database/Migrations/.gitkeep b/Modules/Activity/Database/Migrations/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/database/migrations/2025_07_25_051334_create_countries_table.php b/Modules/Activity/Database/Migrations/2026_06_24_003108_create_activity_types_table.php
similarity index 66%
rename from database/migrations/2025_07_25_051334_create_countries_table.php
rename to Modules/Activity/Database/Migrations/2026_06_24_003108_create_activity_types_table.php
index 76ad330..e6c73a5 100644
--- a/database/migrations/2025_07_25_051334_create_countries_table.php
+++ b/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/Modules/Activity/Database/Migrations/2026_06_24_083114_create_activities_table.php b/Modules/Activity/Database/Migrations/2026_06_24_083114_create_activities_table.php
new file mode 100644
index 0000000..c189a33
--- /dev/null
+++ b/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/Modules/Activity/Database/Migrations/2026_06_25_004305_create_activity_reports_table.php b/Modules/Activity/Database/Migrations/2026_06_25_004305_create_activity_reports_table.php
new file mode 100644
index 0000000..2a4f5e6
--- /dev/null
+++ b/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/Modules/Activity/Database/Seeders/.gitkeep b/Modules/Activity/Database/Seeders/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/Activity/Database/Seeders/ActivityDatabaseSeeder.php b/Modules/Activity/Database/Seeders/ActivityDatabaseSeeder.php
new file mode 100644
index 0000000..63dbc3b
--- /dev/null
+++ b/Modules/Activity/Database/Seeders/ActivityDatabaseSeeder.php
@@ -0,0 +1,16 @@
+call([]);
+ }
+}
diff --git a/Modules/Activity/Emails/.gitkeep b/Modules/Activity/Emails/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/Activity/Entities/.gitkeep b/Modules/Activity/Entities/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/Activity/Entities/Activity.php b/Modules/Activity/Entities/Activity.php
new file mode 100644
index 0000000..407b8d5
--- /dev/null
+++ b/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/Modules/Activity/Entities/ActivityReport.php b/Modules/Activity/Entities/ActivityReport.php
new file mode 100644
index 0000000..eed0baf
--- /dev/null
+++ b/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/Modules/Activity/Entities/ActivityType.php b/Modules/Activity/Entities/ActivityType.php
new file mode 100644
index 0000000..692fa9e
--- /dev/null
+++ b/Modules/Activity/Entities/ActivityType.php
@@ -0,0 +1,32 @@
+ 'string',
+ 'code' => 'string',
+ ];
+ }
+
+ public function activities(): HasMany
+ {
+ return $this->hasMany(Activity::class);
+ }
+}
diff --git a/Modules/Activity/Helpers/.gitkeep b/Modules/Activity/Helpers/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/Activity/Http/Controllers/.gitkeep b/Modules/Activity/Http/Controllers/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/Activity/Http/Controllers/ActivityController.php b/Modules/Activity/Http/Controllers/ActivityController.php
new file mode 100644
index 0000000..a0ebdbf
--- /dev/null
+++ b/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/Modules/Activity/Http/Controllers/ActivityReportController.php b/Modules/Activity/Http/Controllers/ActivityReportController.php
new file mode 100644
index 0000000..006ba41
--- /dev/null
+++ b/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/Modules/Activity/Http/Controllers/ActivityTypeController.php b/Modules/Activity/Http/Controllers/ActivityTypeController.php
new file mode 100644
index 0000000..7707fc4
--- /dev/null
+++ b/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/Modules/Activity/Http/Requests/.gitkeep b/Modules/Activity/Http/Requests/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/Activity/Http/Requests/ActivityReportRequest.php b/Modules/Activity/Http/Requests/ActivityReportRequest.php
new file mode 100644
index 0000000..73ff3aa
--- /dev/null
+++ b/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/Modules/Activity/Http/Requests/ActivityRequest.php b/Modules/Activity/Http/Requests/ActivityRequest.php
new file mode 100644
index 0000000..fed44a4
--- /dev/null
+++ b/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/Modules/Activity/Http/Requests/ActivityTypeRequest.php b/Modules/Activity/Http/Requests/ActivityTypeRequest.php
new file mode 100644
index 0000000..723dc2b
--- /dev/null
+++ b/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/Modules/Activity/Http/Requests/UploadActivityDocumentRequest.php b/Modules/Activity/Http/Requests/UploadActivityDocumentRequest.php
new file mode 100644
index 0000000..e715ab7
--- /dev/null
+++ b/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/Modules/Activity/Http/Requests/UploadActivityGalleryImageRequest.php b/Modules/Activity/Http/Requests/UploadActivityGalleryImageRequest.php
new file mode 100644
index 0000000..c8d21e1
--- /dev/null
+++ b/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/Modules/Activity/Jobs/.gitkeep b/Modules/Activity/Jobs/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/Activity/Notifications/.gitkeep b/Modules/Activity/Notifications/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/Activity/Policies/.gitkeep b/Modules/Activity/Policies/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/Activity/Policies/ActivityPolicy.php b/Modules/Activity/Policies/ActivityPolicy.php
new file mode 100644
index 0000000..ac7a46b
--- /dev/null
+++ b/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/Modules/Activity/Policies/ActivityReportPolicy.php b/Modules/Activity/Policies/ActivityReportPolicy.php
new file mode 100644
index 0000000..a82f136
--- /dev/null
+++ b/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/Modules/Activity/Policies/ActivityTypePolicy.php b/Modules/Activity/Policies/ActivityTypePolicy.php
new file mode 100644
index 0000000..4824517
--- /dev/null
+++ b/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/Modules/Activity/Providers/.gitkeep b/Modules/Activity/Providers/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/Activity/Providers/ActivityServiceProvider.php b/Modules/Activity/Providers/ActivityServiceProvider.php
new file mode 100644
index 0000000..d41dab5
--- /dev/null
+++ b/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/Modules/Activity/Providers/EventServiceProvider.php b/Modules/Activity/Providers/EventServiceProvider.php
new file mode 100644
index 0000000..67ff054
--- /dev/null
+++ b/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/Modules/Activity/Providers/RouteServiceProvider.php b/Modules/Activity/Providers/RouteServiceProvider.php
new file mode 100644
index 0000000..f2d7d5e
--- /dev/null
+++ b/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/Modules/Activity/Repositories/.gitkeep b/Modules/Activity/Repositories/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/Activity/Repositories/ActivityReportRepository.php b/Modules/Activity/Repositories/ActivityReportRepository.php
new file mode 100644
index 0000000..54e2458
--- /dev/null
+++ b/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/Modules/Activity/Repositories/ActivityRepository.php b/Modules/Activity/Repositories/ActivityRepository.php
new file mode 100644
index 0000000..d45617d
--- /dev/null
+++ b/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/Modules/Activity/Repositories/ActivityTypeRepository.php b/Modules/Activity/Repositories/ActivityTypeRepository.php
new file mode 100644
index 0000000..8d79d89
--- /dev/null
+++ b/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/Modules/Activity/Repositories/Contracts/.gitkeep b/Modules/Activity/Repositories/Contracts/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/Activity/Repositories/Contracts/ActivityReportRepositoryInterface.php b/Modules/Activity/Repositories/Contracts/ActivityReportRepositoryInterface.php
new file mode 100644
index 0000000..a771c18
--- /dev/null
+++ b/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/Modules/Activity/Routes/web.php b/Modules/Activity/Routes/web.php
new file mode 100644
index 0000000..31f6ce8
--- /dev/null
+++ b/Modules/Activity/Routes/web.php
@@ -0,0 +1,8 @@
+group(function () {
+ Route::resource('activities', ActivityController::class)->names('activity');
+});
diff --git a/Modules/Activity/Services/.gitkeep b/Modules/Activity/Services/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/Activity/Services/ActivityService.php b/Modules/Activity/Services/ActivityService.php
new file mode 100644
index 0000000..d47c76b
--- /dev/null
+++ b/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/Modules/Activity/Tests/Feature/.gitkeep b/Modules/Activity/Tests/Feature/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/Activity/Tests/Unit/.gitkeep b/Modules/Activity/Tests/Unit/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/Activity/Transformers/.gitkeep b/Modules/Activity/Transformers/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/Activity/Transformers/ActivityDocumentResource.php b/Modules/Activity/Transformers/ActivityDocumentResource.php
new file mode 100644
index 0000000..9d436d5
--- /dev/null
+++ b/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/Modules/Activity/Transformers/ActivityGalleryImageResource.php b/Modules/Activity/Transformers/ActivityGalleryImageResource.php
new file mode 100644
index 0000000..df3e4e9
--- /dev/null
+++ b/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/Modules/Activity/Transformers/ActivityListResource.php b/Modules/Activity/Transformers/ActivityListResource.php
new file mode 100644
index 0000000..8c47d33
--- /dev/null
+++ b/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/Modules/Activity/Transformers/ActivityReportResource.php b/Modules/Activity/Transformers/ActivityReportResource.php
new file mode 100644
index 0000000..cc19c9b
--- /dev/null
+++ b/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/Modules/Activity/Transformers/ActivityResource.php b/Modules/Activity/Transformers/ActivityResource.php
new file mode 100644
index 0000000..f2f850b
--- /dev/null
+++ b/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/Modules/Activity/Transformers/ActivityTypeResource.php b/Modules/Activity/Transformers/ActivityTypeResource.php
new file mode 100644
index 0000000..ca6d008
--- /dev/null
+++ b/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/Modules/Activity/composer.json b/Modules/Activity/composer.json
new file mode 100644
index 0000000..fb29613
--- /dev/null
+++ b/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/Modules/Activity/module.json b/Modules/Activity/module.json
new file mode 100644
index 0000000..cdb201d
--- /dev/null
+++ b/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/Modules/Activity/package.json b/Modules/Activity/package.json
new file mode 100644
index 0000000..d6fbfc8
--- /dev/null
+++ b/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/Modules/Auth/Emails/EmailVerificationOtpEmail.php b/Modules/Auth/Emails/EmailVerificationOtpEmail.php
index 3dcb338..2dd14a8 100644
--- a/Modules/Auth/Emails/EmailVerificationOtpEmail.php
+++ b/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/Modules/Auth/Emails/PasswordResetOtpEmail.php b/Modules/Auth/Emails/PasswordResetOtpEmail.php
new file mode 100644
index 0000000..154063c
--- /dev/null
+++ b/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/Modules/Auth/Entities/PasswordResetOtp.php b/Modules/Auth/Entities/PasswordResetOtp.php
new file mode 100644
index 0000000..74fa260
--- /dev/null
+++ b/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/Modules/Auth/Entities/User.php b/Modules/Auth/Entities/User.php
index 28ff4c1..3c094b5 100644
--- a/Modules/Auth/Entities/User.php
+++ b/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/Modules/Auth/Http/Controllers/PasswordResetController.php b/Modules/Auth/Http/Controllers/PasswordResetController.php
new file mode 100644
index 0000000..ffd5b20
--- /dev/null
+++ b/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/Modules/Auth/Routes/api.php b/Modules/Auth/Routes/api.php
index 21d38a3..ea3eb18 100644
--- a/Modules/Auth/Routes/api.php
+++ b/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/Modules/Auth/Services/PasswordResetOtpService.php b/Modules/Auth/Services/PasswordResetOtpService.php
new file mode 100644
index 0000000..0fc8017
--- /dev/null
+++ b/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/Modules/Auth/Transformers/UserResource.php b/Modules/Auth/Transformers/UserResource.php
index a064874..98518ef 100644
--- a/Modules/Auth/Transformers/UserResource.php
+++ b/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/Modules/Auth/resources/views/emails/password-reset-otp.blade.php b/Modules/Auth/resources/views/emails/password-reset-otp.blade.php
new file mode 100644
index 0000000..95a249d
--- /dev/null
+++ b/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/Modules/Auth/resources/views/emails/verification-otp.blade.php b/Modules/Auth/resources/views/emails/verification-otp.blade.php
index 05c3263..dcaf385 100644
--- a/Modules/Auth/resources/views/emails/verification-otp.blade.php
+++ b/Modules/Auth/resources/views/emails/verification-otp.blade.php
@@ -1,9 +1,5 @@
@component('mail::message')
-@if (! empty($logoPath) && file_exists($logoPath))
-
-
-
-@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/Modules/MembershipApplication/Actions/.gitkeep b/Modules/MembershipApplication/Actions/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/MembershipApplication/Config/.gitkeep b/Modules/MembershipApplication/Config/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/MembershipApplication/Config/config.php b/Modules/MembershipApplication/Config/config.php
new file mode 100644
index 0000000..9b65007
--- /dev/null
+++ b/Modules/MembershipApplication/Config/config.php
@@ -0,0 +1,5 @@
+ 'MembershipApplication',
+];
diff --git a/Modules/MembershipApplication/Console/.gitkeep b/Modules/MembershipApplication/Console/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/MembershipApplication/Database/Factories/.gitkeep b/Modules/MembershipApplication/Database/Factories/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/MembershipApplication/Database/Migrations/.gitkeep b/Modules/MembershipApplication/Database/Migrations/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/MembershipApplication/Database/Migrations/2026_06_14_022844_create_membership_applications_table.php b/Modules/MembershipApplication/Database/Migrations/2026_06_14_022844_create_membership_applications_table.php
new file mode 100644
index 0000000..ced621e
--- /dev/null
+++ b/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/Modules/MembershipApplication/Database/Migrations/2026_06_14_023448_create_membership_application_applicants_table.php b/Modules/MembershipApplication/Database/Migrations/2026_06_14_023448_create_membership_application_applicants_table.php
new file mode 100644
index 0000000..3f356be
--- /dev/null
+++ b/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/Modules/MembershipApplication/Database/Migrations/2026_06_14_044009_create_membership_application_heirs_table.php b/Modules/MembershipApplication/Database/Migrations/2026_06_14_044009_create_membership_application_heirs_table.php
new file mode 100644
index 0000000..501a190
--- /dev/null
+++ b/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/Modules/MembershipApplication/Database/Migrations/2026_06_14_044224_create_membership_application_references_table.php b/Modules/MembershipApplication/Database/Migrations/2026_06_14_044224_create_membership_application_references_table.php
new file mode 100644
index 0000000..58cbce4
--- /dev/null
+++ b/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/Modules/MembershipApplication/Database/Migrations/2026_06_14_050728_create_membership_application_reviews_table.php b/Modules/MembershipApplication/Database/Migrations/2026_06_14_050728_create_membership_application_reviews_table.php
new file mode 100644
index 0000000..cd5db29
--- /dev/null
+++ b/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/Modules/MembershipApplication/Database/Seeders/.gitkeep b/Modules/MembershipApplication/Database/Seeders/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/MembershipApplication/Database/Seeders/MembershipApplicationDatabaseSeeder.php b/Modules/MembershipApplication/Database/Seeders/MembershipApplicationDatabaseSeeder.php
new file mode 100644
index 0000000..31aa8e8
--- /dev/null
+++ b/Modules/MembershipApplication/Database/Seeders/MembershipApplicationDatabaseSeeder.php
@@ -0,0 +1,16 @@
+call([]);
+ }
+}
diff --git a/Modules/MembershipApplication/Emails/.gitkeep b/Modules/MembershipApplication/Emails/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/MembershipApplication/Emails/MembershipApplicationFailedNotification.php b/Modules/MembershipApplication/Emails/MembershipApplicationFailedNotification.php
new file mode 100644
index 0000000..ba3f09a
--- /dev/null
+++ b/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/Modules/MembershipApplication/Emails/MembershipApplicationPassedNotification.php b/Modules/MembershipApplication/Emails/MembershipApplicationPassedNotification.php
new file mode 100644
index 0000000..e9ccbd8
--- /dev/null
+++ b/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/Modules/MembershipApplication/Emails/MembershipApplicationSubmittedNotification.php b/Modules/MembershipApplication/Emails/MembershipApplicationSubmittedNotification.php
new file mode 100644
index 0000000..4b77632
--- /dev/null
+++ b/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/Modules/MembershipApplication/Entities/.gitkeep b/Modules/MembershipApplication/Entities/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/MembershipApplication/Entities/MembershipApplication.php b/Modules/MembershipApplication/Entities/MembershipApplication.php
new file mode 100644
index 0000000..57409a1
--- /dev/null
+++ b/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/Modules/MembershipApplication/Entities/MembershipApplicationApplicant.php b/Modules/MembershipApplication/Entities/MembershipApplicationApplicant.php
new file mode 100644
index 0000000..81f484a
--- /dev/null
+++ b/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/Modules/MembershipApplication/Entities/MembershipApplicationHeir.php b/Modules/MembershipApplication/Entities/MembershipApplicationHeir.php
new file mode 100644
index 0000000..1c11c0d
--- /dev/null
+++ b/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/Modules/MembershipApplication/Entities/MembershipApplicationReference.php b/Modules/MembershipApplication/Entities/MembershipApplicationReference.php
new file mode 100644
index 0000000..f4c729a
--- /dev/null
+++ b/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/Modules/MembershipApplication/Entities/MembershipApplicationReview.php b/Modules/MembershipApplication/Entities/MembershipApplicationReview.php
new file mode 100644
index 0000000..2fb89f2
--- /dev/null
+++ b/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/Modules/MembershipApplication/Enums/ApplicationStatus.php b/Modules/MembershipApplication/Enums/ApplicationStatus.php
new file mode 100644
index 0000000..29fc472
--- /dev/null
+++ b/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/Modules/MembershipApplication/Http/Controllers/PublicMembershipApplicationController.php b/Modules/MembershipApplication/Http/Controllers/PublicMembershipApplicationController.php
new file mode 100644
index 0000000..5ae0c97
--- /dev/null
+++ b/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/Modules/MembershipApplication/Http/Requests/.gitkeep b/Modules/MembershipApplication/Http/Requests/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/MembershipApplication/Http/Requests/AssignReferencesRequest.php b/Modules/MembershipApplication/Http/Requests/AssignReferencesRequest.php
new file mode 100644
index 0000000..90b7dc2
--- /dev/null
+++ b/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/Modules/MembershipApplication/Http/Requests/BatchCompleteRequest.php b/Modules/MembershipApplication/Http/Requests/BatchCompleteRequest.php
new file mode 100644
index 0000000..deb9c19
--- /dev/null
+++ b/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/Modules/MembershipApplication/Http/Requests/BoardReviewRequest.php b/Modules/MembershipApplication/Http/Requests/BoardReviewRequest.php
new file mode 100644
index 0000000..20ea6fe
--- /dev/null
+++ b/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/Modules/MembershipApplication/Http/Requests/LookupMemberRequest.php b/Modules/MembershipApplication/Http/Requests/LookupMemberRequest.php
new file mode 100644
index 0000000..5265792
--- /dev/null
+++ b/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/Modules/MembershipApplication/Http/Requests/ManagementReviewRequest.php b/Modules/MembershipApplication/Http/Requests/ManagementReviewRequest.php
new file mode 100644
index 0000000..2271a5e
--- /dev/null
+++ b/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/Modules/MembershipApplication/Http/Requests/StoreMembershipApplicationRequest.php b/Modules/MembershipApplication/Http/Requests/StoreMembershipApplicationRequest.php
new file mode 100644
index 0000000..8027dc1
--- /dev/null
+++ b/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/Modules/MembershipApplication/Http/Requests/UpdateMembershipApplicationRequest.php b/Modules/MembershipApplication/Http/Requests/UpdateMembershipApplicationRequest.php
new file mode 100644
index 0000000..b987064
--- /dev/null
+++ b/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/Modules/MembershipApplication/Http/Requests/UploadMembershipApplicationDocumentRequest.php b/Modules/MembershipApplication/Http/Requests/UploadMembershipApplicationDocumentRequest.php
new file mode 100644
index 0000000..542495f
--- /dev/null
+++ b/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/Modules/MembershipApplication/Jobs/.gitkeep b/Modules/MembershipApplication/Jobs/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/MembershipApplication/Notifications/.gitkeep b/Modules/MembershipApplication/Notifications/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/MembershipApplication/Policies/.gitkeep b/Modules/MembershipApplication/Policies/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/MembershipApplication/Policies/MembershipApplicationPolicy.php b/Modules/MembershipApplication/Policies/MembershipApplicationPolicy.php
new file mode 100644
index 0000000..9ce7624
--- /dev/null
+++ b/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/Modules/MembershipApplication/Providers/.gitkeep b/Modules/MembershipApplication/Providers/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/MembershipApplication/Providers/EventServiceProvider.php b/Modules/MembershipApplication/Providers/EventServiceProvider.php
new file mode 100644
index 0000000..9af6b7f
--- /dev/null
+++ b/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/Modules/MembershipApplication/Providers/MembershipApplicationServiceProvider.php b/Modules/MembershipApplication/Providers/MembershipApplicationServiceProvider.php
new file mode 100644
index 0000000..26c48fc
--- /dev/null
+++ b/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/Modules/MembershipApplication/Providers/RouteServiceProvider.php b/Modules/MembershipApplication/Providers/RouteServiceProvider.php
new file mode 100644
index 0000000..41c7cf8
--- /dev/null
+++ b/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/Modules/MembershipApplication/Repositories/.gitkeep b/Modules/MembershipApplication/Repositories/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/MembershipApplication/Repositories/Contracts/.gitkeep b/Modules/MembershipApplication/Repositories/Contracts/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/MembershipApplication/Repositories/Contracts/MembershipApplicationRepositoryInterface.php b/Modules/MembershipApplication/Repositories/Contracts/MembershipApplicationRepositoryInterface.php
new file mode 100644
index 0000000..61ee511
--- /dev/null
+++ b/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/Modules/MembershipApplication/Resources/views/emails/failed.blade.php b/Modules/MembershipApplication/Resources/views/emails/failed.blade.php
new file mode 100644
index 0000000..62b70a8
--- /dev/null
+++ b/Modules/MembershipApplication/Resources/views/emails/failed.blade.php
@@ -0,0 +1,18 @@
+@component('mail::message')
+@if (! empty($logoPath) && file_exists($logoPath))
+
+
+
+@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/Modules/MembershipApplication/Resources/views/emails/passed.blade.php b/Modules/MembershipApplication/Resources/views/emails/passed.blade.php
new file mode 100644
index 0000000..3c962c2
--- /dev/null
+++ b/Modules/MembershipApplication/Resources/views/emails/passed.blade.php
@@ -0,0 +1,18 @@
+@component('mail::message')
+@if (! empty($logoPath) && file_exists($logoPath))
+
+
+
+@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/Modules/MembershipApplication/Resources/views/emails/submitted.blade.php b/Modules/MembershipApplication/Resources/views/emails/submitted.blade.php
new file mode 100644
index 0000000..7a75b9e
--- /dev/null
+++ b/Modules/MembershipApplication/Resources/views/emails/submitted.blade.php
@@ -0,0 +1,79 @@
+@component('mail::message')
+@if (! empty($logoPath) && file_exists($logoPath))
+
+
+
+@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/Modules/MembershipApplication/Resources/views/pdf/approved-letter.blade.php b/Modules/MembershipApplication/Resources/views/pdf/approved-letter.blade.php
new file mode 100644
index 0000000..3358ce7
--- /dev/null
+++ b/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/Modules/MembershipApplication/Routes/.gitkeep b/Modules/MembershipApplication/Routes/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/MembershipApplication/Routes/api.php b/Modules/MembershipApplication/Routes/api.php
new file mode 100644
index 0000000..7989d78
--- /dev/null
+++ b/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/Modules/MembershipApplication/Routes/web.php b/Modules/MembershipApplication/Routes/web.php
new file mode 100644
index 0000000..ccef19d
--- /dev/null
+++ b/Modules/MembershipApplication/Routes/web.php
@@ -0,0 +1,8 @@
+group(function () {
+ Route::resource('membershipapplications', MembershipApplicationController::class)->names('membershipapplication');
+});
diff --git a/Modules/MembershipApplication/Services/.gitkeep b/Modules/MembershipApplication/Services/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/MembershipApplication/Services/MembershipApplicationService.php b/Modules/MembershipApplication/Services/MembershipApplicationService.php
new file mode 100644
index 0000000..7cb0712
--- /dev/null
+++ b/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/Modules/MembershipApplication/Tests/Feature/.gitkeep b/Modules/MembershipApplication/Tests/Feature/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/MembershipApplication/Tests/Unit/.gitkeep b/Modules/MembershipApplication/Tests/Unit/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/MembershipApplication/Transformers/.gitkeep b/Modules/MembershipApplication/Transformers/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Modules/MembershipApplication/Transformers/MembershipApplicationApplicantResource.php b/Modules/MembershipApplication/Transformers/MembershipApplicationApplicantResource.php
new file mode 100644
index 0000000..cea8e34
--- /dev/null
+++ b/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/Modules/MembershipApplication/Transformers/MembershipApplicationDocumentResource.php b/Modules/MembershipApplication/Transformers/MembershipApplicationDocumentResource.php
new file mode 100644
index 0000000..571a488
--- /dev/null
+++ b/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/Modules/MembershipApplication/Transformers/MembershipApplicationHeirResource.php b/Modules/MembershipApplication/Transformers/MembershipApplicationHeirResource.php
new file mode 100644
index 0000000..dc03a53
--- /dev/null
+++ b/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/Modules/MembershipApplication/Transformers/MembershipApplicationListResource.php b/Modules/MembershipApplication/Transformers/MembershipApplicationListResource.php
new file mode 100644
index 0000000..eafacb6
--- /dev/null
+++ b/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/Modules/MembershipApplication/Transformers/MembershipApplicationReferenceResource.php b/Modules/MembershipApplication/Transformers/MembershipApplicationReferenceResource.php
new file mode 100644
index 0000000..5a49e11
--- /dev/null
+++ b/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/Modules/MembershipApplication/Transformers/MembershipApplicationResource.php b/Modules/MembershipApplication/Transformers/MembershipApplicationResource.php
new file mode 100644
index 0000000..49989e3
--- /dev/null
+++ b/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/Modules/MembershipApplication/Transformers/MembershipApplicationReviewResource.php b/Modules/MembershipApplication/Transformers/MembershipApplicationReviewResource.php
new file mode 100644
index 0000000..ba7843e
--- /dev/null
+++ b/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/Modules/MembershipApplication/composer.json b/Modules/MembershipApplication/composer.json
new file mode 100644
index 0000000..165fcd1
--- /dev/null
+++ b/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/Modules/MembershipApplication/module.json b/Modules/MembershipApplication/module.json
new file mode 100644
index 0000000..95a81e7
--- /dev/null
+++ b/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/Modules/MembershipApplication/package.json b/Modules/MembershipApplication/package.json
new file mode 100644
index 0000000..d6fbfc8
--- /dev/null
+++ b/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/Modules/Role/Http/Controllers/RoleController.php b/Modules/Role/Http/Controllers/RoleController.php
index f9a03ed..6320d20 100644
--- a/Modules/Role/Http/Controllers/RoleController.php
+++ b/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/Modules/Role/Policies/RolePolicy.php b/Modules/Role/Policies/RolePolicy.php
index 07a49df..b756050 100644
--- a/Modules/Role/Policies/RolePolicy.php
+++ b/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/Modules/Role/Routes/api.php b/Modules/Role/Routes/api.php
index d75036b..8a9d6bf 100644
--- a/Modules/Role/Routes/api.php
+++ b/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/Modules/User/Database/Migrations/2026_06_09_025926_add_more_details_to_users_table.php b/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/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/Modules/User/Database/Migrations/2026_06_09_081007_create_addresses_table.php b/Modules/User/Database/Migrations/2026_06_09_081007_create_addresses_table.php
new file mode 100644
index 0000000..112319e
--- /dev/null
+++ b/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/Modules/User/Database/Migrations/2026_06_10_022314_create_employments_table.php b/Modules/User/Database/Migrations/2026_06_10_022314_create_employments_table.php
new file mode 100644
index 0000000..f216891
--- /dev/null
+++ b/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/Modules/User/Database/Migrations/2026_06_10_040351_create_banks_table.php b/Modules/User/Database/Migrations/2026_06_10_040351_create_banks_table.php
new file mode 100644
index 0000000..85b4398
--- /dev/null
+++ b/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/Modules/User/Database/Migrations/2026_06_10_040453_create_bank_details_table.php b/Modules/User/Database/Migrations/2026_06_10_040453_create_bank_details_table.php
new file mode 100644
index 0000000..d9a5ae2
--- /dev/null
+++ b/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/Modules/User/Database/Migrations/2026_06_10_072612_create_heirs_table.php b/Modules/User/Database/Migrations/2026_06_10_072612_create_heirs_table.php
new file mode 100644
index 0000000..8761403
--- /dev/null
+++ b/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/Modules/User/Entities/Address.php b/Modules/User/Entities/Address.php
new file mode 100644
index 0000000..b102768
--- /dev/null
+++ b/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/Modules/User/Entities/Bank.php b/Modules/User/Entities/Bank.php
new file mode 100644
index 0000000..d13930e
--- /dev/null
+++ b/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/Modules/User/Entities/BankDetail.php b/Modules/User/Entities/BankDetail.php
new file mode 100644
index 0000000..8fa1c96
--- /dev/null
+++ b/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/Modules/User/Entities/Employment.php b/Modules/User/Entities/Employment.php
new file mode 100644
index 0000000..330dc13
--- /dev/null
+++ b/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/Modules/User/Entities/Heir.php b/Modules/User/Entities/Heir.php
new file mode 100644
index 0000000..b60110a
--- /dev/null
+++ b/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/Modules/User/Http/Controllers/AddressController.php b/Modules/User/Http/Controllers/AddressController.php
new file mode 100644
index 0000000..543dd02
--- /dev/null
+++ b/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/Modules/User/Http/Controllers/BankController.php b/Modules/User/Http/Controllers/BankController.php
new file mode 100644
index 0000000..fb7ee02
--- /dev/null
+++ b/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/Modules/User/Http/Controllers/BankDetailController.php b/Modules/User/Http/Controllers/BankDetailController.php
new file mode 100644
index 0000000..edbf1e3
--- /dev/null
+++ b/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/Modules/User/Http/Controllers/EmploymentController.php b/Modules/User/Http/Controllers/EmploymentController.php
new file mode 100644
index 0000000..87b5a41
--- /dev/null
+++ b/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/Modules/User/Http/Controllers/HeirController.php b/Modules/User/Http/Controllers/HeirController.php
new file mode 100644
index 0000000..c830bed
--- /dev/null
+++ b/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/Modules/User/Http/Controllers/UserController.php b/Modules/User/Http/Controllers/UserController.php
index 78e9aa0..2526ff6 100644
--- a/Modules/User/Http/Controllers/UserController.php
+++ b/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/Modules/User/Http/Requests/AddressRequest.php b/Modules/User/Http/Requests/AddressRequest.php
new file mode 100644
index 0000000..31a8d48
--- /dev/null
+++ b/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/Modules/User/Http/Requests/BankDetailRequest.php b/Modules/User/Http/Requests/BankDetailRequest.php
new file mode 100644
index 0000000..4fe2c80
--- /dev/null
+++ b/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/Modules/User/Http/Requests/BankRequest.php b/Modules/User/Http/Requests/BankRequest.php
new file mode 100644
index 0000000..242fa6d
--- /dev/null
+++ b/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/Modules/User/Http/Requests/EmploymentRequest.php b/Modules/User/Http/Requests/EmploymentRequest.php
new file mode 100644
index 0000000..d02e12e
--- /dev/null
+++ b/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/Modules/User/Http/Requests/HeirRequest.php b/Modules/User/Http/Requests/HeirRequest.php
new file mode 100644
index 0000000..bfc0362
--- /dev/null
+++ b/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/Modules/User/Http/Requests/UserRequest.php b/Modules/User/Http/Requests/UserRequest.php
index 939fdfd..1fb5207 100644
--- a/Modules/User/Http/Requests/UserRequest.php
+++ b/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/Modules/User/Notifications/UserActivationNotification.php b/Modules/User/Notifications/UserActivationNotification.php
index 62cee5d..e18e998 100644
--- a/Modules/User/Notifications/UserActivationNotification.php
+++ b/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/Modules/User/Policies/AddressPolicy.php b/Modules/User/Policies/AddressPolicy.php
new file mode 100644
index 0000000..b33041d
--- /dev/null
+++ b/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/Modules/User/Policies/BankDetailPolicy.php b/Modules/User/Policies/BankDetailPolicy.php
new file mode 100644
index 0000000..c52f6fa
--- /dev/null
+++ b/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/Modules/User/Policies/BankPolicy.php b/Modules/User/Policies/BankPolicy.php
new file mode 100644
index 0000000..e0cc392
--- /dev/null
+++ b/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/Modules/User/Policies/EmploymentPolicy.php b/Modules/User/Policies/EmploymentPolicy.php
new file mode 100644
index 0000000..1d03c84
--- /dev/null
+++ b/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/Modules/User/Policies/HeirPolicy.php b/Modules/User/Policies/HeirPolicy.php
new file mode 100644
index 0000000..ebb5680
--- /dev/null
+++ b/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/Modules/User/Policies/UserPolicy.php b/Modules/User/Policies/UserPolicy.php
index 559a272..22d6212 100644
--- a/Modules/User/Policies/UserPolicy.php
+++ b/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/Modules/User/Providers/UserServiceProvider.php b/Modules/User/Providers/UserServiceProvider.php
index e135007..9c28f9e 100644
--- a/Modules/User/Providers/UserServiceProvider.php
+++ b/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/Modules/User/Repositories/AddressRepository.php b/Modules/User/Repositories/AddressRepository.php
new file mode 100644
index 0000000..ed197d0
--- /dev/null
+++ b/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/Modules/User/Repositories/BankDetailRepository.php b/Modules/User/Repositories/BankDetailRepository.php
new file mode 100644
index 0000000..47a9fb6
--- /dev/null
+++ b/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/Modules/User/Repositories/BankRepository.php b/Modules/User/Repositories/BankRepository.php
new file mode 100644
index 0000000..a762a6e
--- /dev/null
+++ b/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/Modules/User/Repositories/Contracts/AddressRepositoryInterface.php b/Modules/User/Repositories/Contracts/AddressRepositoryInterface.php
new file mode 100644
index 0000000..ebad98c
--- /dev/null
+++ b/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/Modules/User/Repositories/HeirRepository.php b/Modules/User/Repositories/HeirRepository.php
new file mode 100644
index 0000000..7d70282
--- /dev/null
+++ b/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/Modules/User/Repositories/UserRepository.php b/Modules/User/Repositories/UserRepository.php
index 5010a32..33ce614 100644
--- a/Modules/User/Repositories/UserRepository.php
+++ b/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/Modules/User/Routes/api.php b/Modules/User/Routes/api.php
index d09e4f1..28c0793 100644
--- a/Modules/User/Routes/api.php
+++ b/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/Modules/User/Services/UserService.php b/Modules/User/Services/UserService.php
index 0a9d9bf..90f20fb 100644
--- a/Modules/User/Services/UserService.php
+++ b/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/Modules/User/Transformers/AddressResource.php b/Modules/User/Transformers/AddressResource.php
new file mode 100644
index 0000000..ffbf6ae
--- /dev/null
+++ b/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/Modules/User/Transformers/BankDetailResource.php b/Modules/User/Transformers/BankDetailResource.php
new file mode 100644
index 0000000..9acd024
--- /dev/null
+++ b/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/Modules/User/Transformers/BankResource.php b/Modules/User/Transformers/BankResource.php
new file mode 100644
index 0000000..065dc31
--- /dev/null
+++ b/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/Modules/User/Transformers/EmploymentResource.php b/Modules/User/Transformers/EmploymentResource.php
new file mode 100644
index 0000000..f734dbc
--- /dev/null
+++ b/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/Modules/User/Transformers/HeirResource.php b/Modules/User/Transformers/HeirResource.php
new file mode 100644
index 0000000..f32c38a
--- /dev/null
+++ b/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/Modules/User/Transformers/UserListResource.php b/Modules/User/Transformers/UserListResource.php
index 5426fee..136aef2 100644
--- a/Modules/User/Transformers/UserListResource.php
+++ b/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/Modules/User/Transformers/UserResource.php b/Modules/User/Transformers/UserResource.php
deleted file mode 100644
index b7021e4..0000000
--- a/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/app/Http/Controllers/ContactController.php b/app/Http/Controllers/ContactController.php
deleted file mode 100644
index 5fae078..0000000
--- a/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/app/Http/Controllers/CountryController.php b/app/Http/Controllers/CountryController.php
deleted file mode 100644
index 1beeaf8..0000000
--- a/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/app/Http/Controllers/LetterPreviewController.php b/app/Http/Controllers/LetterPreviewController.php
new file mode 100644
index 0000000..32d79d4
--- /dev/null
+++ b/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/app/Http/Controllers/OnlineUsersController.php b/app/Http/Controllers/OnlineUsersController.php
deleted file mode 100644
index 3e53297..0000000
--- a/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/app/Http/Controllers/SocialMediaController.php b/app/Http/Controllers/SocialMediaController.php
deleted file mode 100644
index 3421812..0000000
--- a/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/app/Models/Country.php b/app/Models/Country.php
deleted file mode 100644
index 431c4bf..0000000
--- a/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/app/Notifications/Concerns/BuildsMailMessage.php b/app/Notifications/Concerns/BuildsMailMessage.php
new file mode 100644
index 0000000..6516731
--- /dev/null
+++ b/app/Notifications/Concerns/BuildsMailMessage.php
@@ -0,0 +1,17 @@
+subject($subject)
+ ->markdown($view, array_merge([
+ 'logoPath' => config('mail.logo_path'),
+ ], $data));
+ }
+}
diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php
index f9abbaa..0077392 100644
--- a/app/Providers/FortifyServiceProvider.php
+++ b/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/app/Services/ContactService.php b/app/Services/ContactService.php
deleted file mode 100644
index e98d14a..0000000
--- a/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/app/Services/DocumentService.php b/app/Services/DocumentService.php
index 4aaaee8..4b78240 100644
--- a/app/Services/DocumentService.php
+++ b/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/app/Services/LetterService.php b/app/Services/LetterService.php
new file mode 100644
index 0000000..efc6931
--- /dev/null
+++ b/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/app/Services/OnlineUsersService.php b/app/Services/OnlineUsersService.php
deleted file mode 100644
index 8d6a3a4..0000000
--- a/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/app/Services/SocialMediaService.php b/app/Services/SocialMediaService.php
deleted file mode 100644
index 433dc8b..0000000
--- a/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/app/Services/VisibilityService.php b/app/Services/VisibilityService.php
deleted file mode 100644
index dad6050..0000000
--- a/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/app/Support/LetterLayout.php b/app/Support/LetterLayout.php
new file mode 100644
index 0000000..c8543fa
--- /dev/null
+++ b/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/app/Traits/HasDocuments.php b/app/Traits/HasDocuments.php
index e4580a3..ffd6cd5 100644
--- a/app/Traits/HasDocuments.php
+++ b/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/app/Traits/HasVisibility.php b/app/Traits/HasVisibility.php
deleted file mode 100644
index 1d42fd0..0000000
--- a/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/composer.json b/composer.json
index 5fda7bc..93d2c6f 100644
--- a/composer.json
+++ b/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/composer.lock b/composer.lock
index 18879a6..2f61a0a 100644
--- a/composer.lock
+++ b/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/config/auth.php b/config/auth.php
index 3f1a624..59abb17 100644
--- a/config/auth.php
+++ b/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/config/browsershot.php b/config/browsershot.php
new file mode 100644
index 0000000..ecf85f0
--- /dev/null
+++ b/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/config/cors.php b/config/cors.php
index 28c3045..715d028 100644
--- a/config/cors.php
+++ b/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/config/letter.php b/config/letter.php
new file mode 100644
index 0000000..dad463b
--- /dev/null
+++ b/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/config/mail.php b/config/mail.php
index 522b284..c875aaf 100644
--- a/config/mail.php
+++ b/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/database/migrations/2025_08_02_061334_create_documents_table.php b/database/migrations/2026_06_14_061334_create_documents_table.php
similarity index 78%
rename from database/migrations/2025_08_02_061334_create_documents_table.php
rename to database/migrations/2026_06_14_061334_create_documents_table.php
index 7530ea0..58a8006 100644
--- a/database/migrations/2025_08_02_061334_create_documents_table.php
+++ b/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/database/migrations/2026_06_25_120000_create_password_reset_otps_table.php b/database/migrations/2026_06_25_120000_create_password_reset_otps_table.php
new file mode 100644
index 0000000..3a7409a
--- /dev/null
+++ b/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/docker/common/unified/Dockerfile b/docker/common/unified/Dockerfile
index c586227..8a506eb 100644
--- a/docker/common/unified/Dockerfile
+++ b/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/docker/production/docker-compose.production.yml b/docker/production/docker-compose.production.yml
index 7bc1e8a..242719d 100644
--- a/docker/production/docker-compose.production.yml
+++ b/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/docker/staging/docker-compose.staging.yml b/docker/staging/docker-compose.staging.yml
index 4c13517..3217647 100644
--- a/docker/staging/docker-compose.staging.yml
+++ b/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/docker/training/docker-compose.training.yml b/docker/training/docker-compose.training.yml
index 16efeae..030aae7 100644
--- a/docker/training/docker-compose.training.yml
+++ b/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/modules_statuses.json b/modules_statuses.json
index c645c10..c835c83 100644
--- a/modules_statuses.json
+++ b/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/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..10bc260
--- /dev/null
+++ b/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/package.json b/package.json
index a5707d8..63e1661 100644
--- a/package.json
+++ b/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/public/images/logo/Logo-RAJD.png b/public/images/logo/Logo-RAJD.png
deleted file mode 100644
index 37e434c..0000000
Binary files a/public/images/logo/Logo-RAJD.png and /dev/null differ
diff --git a/public/images/logo/logo-kopkb-letterhead.svg b/public/images/logo/logo-kopkb-letterhead.svg
new file mode 100644
index 0000000..5c3ea85
--- /dev/null
+++ b/public/images/logo/logo-kopkb-letterhead.svg
@@ -0,0 +1,161 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/images/logo/logo-kopkb.svg b/public/images/logo/logo-kopkb.svg
new file mode 100644
index 0000000..ef88ccb
--- /dev/null
+++ b/public/images/logo/logo-kopkb.svg
@@ -0,0 +1,418 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/views/emails/partials/footer.blade.php b/resources/views/emails/partials/footer.blade.php
new file mode 100644
index 0000000..7e275d9
--- /dev/null
+++ b/resources/views/emails/partials/footer.blade.php
@@ -0,0 +1,2 @@
+Terima kasih,
+{{ config('app.name') }}
diff --git a/resources/views/emails/partials/header.blade.php b/resources/views/emails/partials/header.blade.php
new file mode 100644
index 0000000..628f5a8
--- /dev/null
+++ b/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))
+
+
+
+@endif
diff --git a/resources/views/pdf/layouts/letter.blade.php b/resources/views/pdf/layouts/letter.blade.php
new file mode 100644
index 0000000..e649a3a
--- /dev/null
+++ b/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')
+
+
+
+
+ @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/resources/views/pdf/partials/footer-icon.blade.php b/resources/views/pdf/partials/footer-icon.blade.php
new file mode 100644
index 0000000..cc9dd0d
--- /dev/null
+++ b/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/resources/views/pdf/partials/letterfooter.blade.php b/resources/views/pdf/partials/letterfooter.blade.php
new file mode 100644
index 0000000..de80b00
--- /dev/null
+++ b/resources/views/pdf/partials/letterfooter.blade.php
@@ -0,0 +1,38 @@
+
diff --git a/resources/views/pdf/partials/letterhead.blade.php b/resources/views/pdf/partials/letterhead.blade.php
new file mode 100644
index 0000000..e27b900
--- /dev/null
+++ b/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/resources/views/pdf/preview-approved-membership.blade.php b/resources/views/pdf/preview-approved-membership.blade.php
new file mode 100644
index 0000000..89c7937
--- /dev/null
+++ b/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')
+
+ KOPERASI PERMODALAN KELANTAN BERHAD
+
+ MUHAMMAD NAFIS BIN ZAINUDDIN
+ Pengurus Besar
+ b/p Setiausaha
+@endsection
diff --git a/resources/views/pdf/preview.blade.php b/resources/views/pdf/preview.blade.php
new file mode 100644
index 0000000..e81357a
--- /dev/null
+++ b/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/routes/api.php b/routes/api.php
index 0c60ae2..b16345e 100644
--- a/routes/api.php
+++ b/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/routes/web.php b/routes/web.php
index bfe8a20..a4def31 100644
--- a/routes/web.php
+++ b/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