From 61345e9cf0bcb40100aae504de51f6f82fa07da0 Mon Sep 17 00:00:00 2001
From: ISMAIL MASSERAN
Date: Mon, 6 Jul 2026 09:37:08 +0800
Subject: [PATCH 1/4] DONE: membership application to generate letter along
with member_number and attach to email, change to dropdown for company or job
selection, redesign profile ui, change to incremental ids for jobs; WIP:
member digital card
---
TODO.md | 15 +-
...embershipApplicationFailedNotification.php | 35 +-
...embershipApplicationPassedNotification.php | 35 +-
.../Entities/MembershipApplication.php | 2 +
.../MembershipApplicationController.php | 29 +-
.../Http/Requests/BatchCompleteRequest.php | 3 +
.../CompleteMembershipApplicationRequest.php | 34 +
.../StoreMembershipApplicationRequest.php | 4 +-
...adMembershipApplicationDocumentRequest.php | 9 +-
.../MembershipApplicationRepository.php | 1 +
.../Resources/views/emails/failed.blade.php | 15 +-
.../Resources/views/emails/passed.blade.php | 15 +-
.../views/pdf/approved-letter.blade.php | 4 +-
.../MembershipApplication/Routes/api.php | 1 +
.../Services/MembershipApplicationService.php | 145 +-
.../0001_01_01_000002_create_jobs_table.php | 6 +-
fe/src/assets/images/logo-kopkb.svg | 418 ----
fe/src/assets/images/logo.svg | 425 +++-
fe/src/docs/pages/accordion.vue | 129 -
fe/src/docs/pages/avatar.vue | 239 --
fe/src/docs/pages/badge.vue | 351 ---
fe/src/docs/pages/button.vue | 1743 --------------
fe/src/docs/pages/carousel.vue | 445 ----
fe/src/docs/pages/checkbox.vue | 235 --
fe/src/docs/pages/input.vue | 74 -
fe/src/docs/pages/map.vue | 369 ---
fe/src/docs/pages/table.vue | 653 -----
fe/src/main/side-menu.ts | 284 +--
fe/src/modules/auth/pages/AccountPending.vue | 4 +-
fe/src/modules/auth/pages/ForgotPassword.vue | 2 +-
fe/src/modules/auth/pages/Login.vue | 9 +-
fe/src/modules/auth/pages/PrivacyPolicy.vue | 6 +-
fe/src/modules/auth/pages/Register.vue | 2 +-
fe/src/modules/auth/pages/ResetPassword.vue | 63 +-
fe/src/modules/auth/pages/TermsConditions.vue | 1 -
fe/src/modules/auth/pages/VerifyEmail.vue | 4 +-
.../pages/MembershipApplication.vue | 145 +-
.../pages/MembershipApplicationDetail.vue | 319 +--
.../pages/MembershipApplicationEdit.vue | 215 +-
.../pages/MembershipApplicationList.vue | 173 +-
.../membership-application.service.ts | 23 +-
.../types/membership-application.types.ts | 8 +-
.../membership-application-form.utils.ts | 20 +-
.../modules/profile/pages/EmploymentTab.vue | 69 +-
fe/src/modules/profile/pages/HeirTab.vue | 167 +-
.../profile/pages/ProfileOverview2.vue | 208 +-
fe/src/modules/user/pages/UserHeirTab.vue | 17 +-
fe/src/modules/user/pages/UserProfileView.vue | 2 +-
fe/src/themes/Layout.vue | 2 -
fe/src/views/Chat.vue | 2120 -----------------
fe/src/views/DashboardOverview1.vue | 18 +-
fe/src/views/Inbox.vue | 266 ---
fe/src/views/Post.vue | 481 ----
fe/src/views/UsersLayout1.vue | 153 --
fe/src/views/UsersLayout2.vue | 103 -
fe/src/views/UsersLayout3.vue | 133 --
fe/src/views/WizardLayout1.vue | 70 -
fe/src/views/WizardLayout2.vue | 85 -
fe/src/views/WizardLayout3.vue | 88 -
59 files changed, 1538 insertions(+), 9156 deletions(-)
create mode 100644 be/Modules/MembershipApplication/Http/Requests/CompleteMembershipApplicationRequest.php
delete mode 100644 fe/src/assets/images/logo-kopkb.svg
delete mode 100644 fe/src/docs/pages/accordion.vue
delete mode 100644 fe/src/docs/pages/avatar.vue
delete mode 100644 fe/src/docs/pages/badge.vue
delete mode 100644 fe/src/docs/pages/button.vue
delete mode 100644 fe/src/docs/pages/carousel.vue
delete mode 100644 fe/src/docs/pages/checkbox.vue
delete mode 100644 fe/src/docs/pages/input.vue
delete mode 100644 fe/src/docs/pages/map.vue
delete mode 100644 fe/src/docs/pages/table.vue
delete mode 100644 fe/src/views/Chat.vue
delete mode 100644 fe/src/views/Inbox.vue
delete mode 100644 fe/src/views/Post.vue
delete mode 100644 fe/src/views/UsersLayout1.vue
delete mode 100644 fe/src/views/UsersLayout2.vue
delete mode 100644 fe/src/views/UsersLayout3.vue
delete mode 100644 fe/src/views/WizardLayout1.vue
delete mode 100644 fe/src/views/WizardLayout2.vue
delete mode 100644 fe/src/views/WizardLayout3.vue
diff --git a/TODO.md b/TODO.md
index fff4912..f934127 100644
--- a/TODO.md
+++ b/TODO.md
@@ -16,5 +16,16 @@
[ ] jana surat lepas lulus anggota
## Present to Boss (2/7/2026)
-[ ] discuss logo baru MyKOPKB
-[ ] layout footer untuk surat rasmi
\ No newline at end of file
+[x] discuss logo baru MyKOPKB
+[x] layout footer untuk surat rasmi
+[ ] maklumat pekerjaan kena buat dropdown untuk nama majikan
+[ ] tambah minima untuk borang permohonan (tukar jadi caruman saham potongan bulanan)
+[x] tukar waris ke penama
+[x] ic dan surat pengesahan majikkan perlu mandatori
+[ ] semakan lembaga tukar jadi maklumat keputusan
+[x] boleh attach document untuk peringkat pentadbir
+[x] surat attach dekat email selepas bos
+[ ] pembayaran pertama tukar jadi one-off
+[ ] syer maksima silap tukar jadi rm50.00
+[ ] letak sign digital
+[x] running no anggota dalam surat dan masa.
\ No newline at end of file
diff --git a/be/Modules/MembershipApplication/Emails/MembershipApplicationFailedNotification.php b/be/Modules/MembershipApplication/Emails/MembershipApplicationFailedNotification.php
index ba3f09a..36018fc 100644
--- a/be/Modules/MembershipApplication/Emails/MembershipApplicationFailedNotification.php
+++ b/be/Modules/MembershipApplication/Emails/MembershipApplicationFailedNotification.php
@@ -2,19 +2,28 @@
namespace Modules\MembershipApplication\Emails;
+use App\Models\Document;
+use App\Notifications\Concerns\BuildsMailMessage;
use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
+use Illuminate\Support\Facades\Storage;
use Modules\MembershipApplication\Entities\MembershipApplication;
-class MembershipApplicationFailedNotification extends Notification
+class MembershipApplicationFailedNotification extends Notification implements ShouldQueue
{
+ use BuildsMailMessage;
use Queueable;
public function __construct(
- protected MembershipApplication $application
+ public MembershipApplication $application,
+ public Document $resultLetter,
) {}
+ /**
+ * @return array
+ */
public function via(object $notifiable): array
{
return ['mail'];
@@ -23,13 +32,21 @@ class MembershipApplicationFailedNotification extends Notification
public function toMail(object $notifiable): MailMessage
{
$this->application->loadMissing('applicant');
+ $applicant = $this->application->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'),
- ]);
+ return $this->mailMessage(
+ subject: 'Keputusan Permohonan Keahlian KOPKB - TIDAK LULUS',
+ view: 'membershipapplication::emails.failed',
+ data: [
+ 'application' => $this->application,
+ 'applicant' => $applicant,
+ ],
+ )->attach(
+ Storage::disk(Document::STORAGE_DISK)->path($this->resultLetter->path),
+ [
+ 'as' => $this->resultLetter->name,
+ 'mime' => $this->resultLetter->mime_type,
+ ]
+ );
}
}
diff --git a/be/Modules/MembershipApplication/Emails/MembershipApplicationPassedNotification.php b/be/Modules/MembershipApplication/Emails/MembershipApplicationPassedNotification.php
index e9ccbd8..c07ec92 100644
--- a/be/Modules/MembershipApplication/Emails/MembershipApplicationPassedNotification.php
+++ b/be/Modules/MembershipApplication/Emails/MembershipApplicationPassedNotification.php
@@ -2,19 +2,28 @@
namespace Modules\MembershipApplication\Emails;
+use App\Models\Document;
+use App\Notifications\Concerns\BuildsMailMessage;
use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
+use Illuminate\Support\Facades\Storage;
use Modules\MembershipApplication\Entities\MembershipApplication;
-class MembershipApplicationPassedNotification extends Notification
+class MembershipApplicationPassedNotification extends Notification implements ShouldQueue
{
+ use BuildsMailMessage;
use Queueable;
public function __construct(
- protected MembershipApplication $application
+ public MembershipApplication $application,
+ public Document $resultLetter,
) {}
+ /**
+ * @return array
+ */
public function via(object $notifiable): array
{
return ['mail'];
@@ -23,13 +32,21 @@ class MembershipApplicationPassedNotification extends Notification
public function toMail(object $notifiable): MailMessage
{
$this->application->loadMissing('applicant');
+ $applicant = $this->application->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'),
- ]);
+ return $this->mailMessage(
+ subject: 'Keputusan Permohonan Keahlian KOPKB - LULUS',
+ view: 'membershipapplication::emails.passed',
+ data: [
+ 'application' => $this->application,
+ 'applicant' => $applicant,
+ ],
+ )->attach(
+ Storage::disk(Document::STORAGE_DISK)->path($this->resultLetter->path),
+ [
+ 'as' => $this->resultLetter->name,
+ 'mime' => $this->resultLetter->mime_type,
+ ]
+ );
}
}
diff --git a/be/Modules/MembershipApplication/Entities/MembershipApplication.php b/be/Modules/MembershipApplication/Entities/MembershipApplication.php
index 9464541..15a7ff0 100644
--- a/be/Modules/MembershipApplication/Entities/MembershipApplication.php
+++ b/be/Modules/MembershipApplication/Entities/MembershipApplication.php
@@ -18,6 +18,8 @@ class MembershipApplication extends Model
public const RESULT_LETTER_DOCUMENT_TYPE = 'result_letter';
+ public const ADMIN_ATTACHMENT_DOCUMENT_TYPE = 'admin_attachment';
+
public const MINIMUM_SHARE_CAPITAL = 500;
public const SHARE_INSTALLMENT_MONTHS = 6;
diff --git a/be/Modules/MembershipApplication/Http/Controllers/MembershipApplicationController.php b/be/Modules/MembershipApplication/Http/Controllers/MembershipApplicationController.php
index 006de7b..7da1e09 100644
--- a/be/Modules/MembershipApplication/Http/Controllers/MembershipApplicationController.php
+++ b/be/Modules/MembershipApplication/Http/Controllers/MembershipApplicationController.php
@@ -17,6 +17,7 @@ use Modules\MembershipApplication\Enums\ManagementDecision;
use Modules\MembershipApplication\Http\Requests\AssignReferencesRequest;
use Modules\MembershipApplication\Http\Requests\BatchCompleteRequest;
use Modules\MembershipApplication\Http\Requests\BoardReviewRequest;
+use Modules\MembershipApplication\Http\Requests\CompleteMembershipApplicationRequest;
use Modules\MembershipApplication\Http\Requests\GenerateResultLetterRequest;
use Modules\MembershipApplication\Http\Requests\ManagementReviewRequest;
use Modules\MembershipApplication\Http\Requests\UpdateMembershipApplicationRequest;
@@ -180,18 +181,19 @@ class MembershipApplicationController extends Controller
]);
}
- public function complete(Request $request, MembershipApplication $membershipApplication): JsonResponse
+ public function complete(CompleteMembershipApplicationRequest $request, MembershipApplication $membershipApplication): JsonResponse
{
$this->authorize('complete', $membershipApplication);
$application = $this->membershipApplicationService->complete(
$membershipApplication,
- $request->user()
+ $request->user(),
+ $request->validated('board_meeting_reference'),
);
$message = $application->board_result === BoardResult::Pass->value
- ? 'Permohonan selesai. Akaun ahli telah dicipta dan e-mel dihantar.'
- : 'Permohonan selesai. E-mel keputusan dihantar.';
+ ? 'Permohonan selesai. Surat keputusan dijana, akaun ahli dicipta dan e-mel dihantar.'
+ : 'Permohonan selesai. Surat keputusan dijana dan e-mel dihantar.';
return response()->json([
'success' => true,
@@ -206,7 +208,8 @@ class MembershipApplicationController extends Controller
$result = $this->membershipApplicationService->completeBatch(
$request->validated('application_ids'),
- $request->user()
+ $request->user(),
+ $request->validated('board_meeting_reference'),
);
$succeededCount = count($result['succeeded']);
@@ -245,6 +248,22 @@ class MembershipApplicationController extends Controller
return $this->documentService->downloadDocument($document->id);
}
+ public function deleteDocument(MembershipApplication $membershipApplication, string $documentId): JsonResponse
+ {
+ $this->authorize('update', $membershipApplication);
+
+ $application = $this->membershipApplicationService->deleteDocument(
+ $membershipApplication,
+ $documentId,
+ );
+
+ return response()->json([
+ 'success' => true,
+ 'message' => 'Lampiran pentadbir berjaya dipadam.',
+ 'data' => new MembershipApplicationResource($application),
+ ]);
+ }
+
public function generateResultLetter(GenerateResultLetterRequest $request, MembershipApplication $membershipApplication,): JsonResponse {
$this->authorize('generateResultLetter', $membershipApplication);
diff --git a/be/Modules/MembershipApplication/Http/Requests/BatchCompleteRequest.php b/be/Modules/MembershipApplication/Http/Requests/BatchCompleteRequest.php
index deb9c19..0a4c3ae 100644
--- a/be/Modules/MembershipApplication/Http/Requests/BatchCompleteRequest.php
+++ b/be/Modules/MembershipApplication/Http/Requests/BatchCompleteRequest.php
@@ -19,6 +19,7 @@ class BatchCompleteRequest extends FormRequest
return [
'application_ids' => 'required|array|min:1|max:100',
'application_ids.*' => 'required|uuid|distinct|exists:membership_applications,id',
+ 'board_meeting_reference' => 'required|string|max:255',
];
}
@@ -33,6 +34,8 @@ class BatchCompleteRequest extends FormRequest
'application_ids.max' => 'Maksimum 100 permohonan setiap batch.',
'application_ids.*.exists' => 'Salah satu permohonan tidak dijumpai.',
'application_ids.*.distinct' => 'Permohonan duplikat tidak dibenarkan.',
+ 'board_meeting_reference.required' => 'Rujukan mesyuarat lembaga diperlukan.',
+ 'board_meeting_reference.max' => 'Rujukan mesyuarat lembaga tidak boleh melebihi 255 aksara.',
];
}
}
diff --git a/be/Modules/MembershipApplication/Http/Requests/CompleteMembershipApplicationRequest.php b/be/Modules/MembershipApplication/Http/Requests/CompleteMembershipApplicationRequest.php
new file mode 100644
index 0000000..971bfde
--- /dev/null
+++ b/be/Modules/MembershipApplication/Http/Requests/CompleteMembershipApplicationRequest.php
@@ -0,0 +1,34 @@
+|string>
+ */
+ public function rules(): array
+ {
+ return [
+ 'board_meeting_reference' => 'required|string|max:255',
+ ];
+ }
+
+ /**
+ * @return array
+ */
+ public function messages(): array
+ {
+ return [
+ 'board_meeting_reference.required' => 'Rujukan mesyuarat lembaga diperlukan.',
+ 'board_meeting_reference.max' => 'Rujukan mesyuarat lembaga tidak boleh melebihi 255 aksara.',
+ ];
+ }
+}
diff --git a/be/Modules/MembershipApplication/Http/Requests/StoreMembershipApplicationRequest.php b/be/Modules/MembershipApplication/Http/Requests/StoreMembershipApplicationRequest.php
index 8027dc1..f15abed 100644
--- a/be/Modules/MembershipApplication/Http/Requests/StoreMembershipApplicationRequest.php
+++ b/be/Modules/MembershipApplication/Http/Requests/StoreMembershipApplicationRequest.php
@@ -48,7 +48,7 @@ class StoreMembershipApplicationRequest extends FormRequest
'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',
+ 'documents.employer_letter' => 'required|file|mimes:pdf,jpg,jpeg,png|max:10240',
];
}
@@ -89,6 +89,8 @@ class StoreMembershipApplicationRequest extends FormRequest
'documents.ic_copy.required' => 'Salinan kad pengenalan diperlukan.',
'documents.ic_copy.mimes' => 'Salinan kad pengenalan mestilah PDF, JPG, JPEG atau PNG.',
+ 'documents.employer_letter.required' => 'Surat pengesahan majikan diperlukan.',
+ 'documents.employer_letter.mimes' => 'Surat pengesahan majikan mestilah PDF, JPG, JPEG atau PNG.',
];
}
}
diff --git a/be/Modules/MembershipApplication/Http/Requests/UploadMembershipApplicationDocumentRequest.php b/be/Modules/MembershipApplication/Http/Requests/UploadMembershipApplicationDocumentRequest.php
index 542495f..47198a4 100644
--- a/be/Modules/MembershipApplication/Http/Requests/UploadMembershipApplicationDocumentRequest.php
+++ b/be/Modules/MembershipApplication/Http/Requests/UploadMembershipApplicationDocumentRequest.php
@@ -4,6 +4,7 @@ namespace Modules\MembershipApplication\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
+use Modules\MembershipApplication\Entities\MembershipApplication;
class UploadMembershipApplicationDocumentRequest extends FormRequest
{
@@ -18,7 +19,13 @@ class UploadMembershipApplicationDocumentRequest extends FormRequest
public function rules(): array
{
return [
- 'type' => ['required', Rule::in(['ic_copy', 'photo', 'salary_slip', 'employer_letter'])],
+ 'type' => ['required', Rule::in([
+ 'ic_copy',
+ 'photo',
+ 'salary_slip',
+ 'employer_letter',
+ MembershipApplication::ADMIN_ATTACHMENT_DOCUMENT_TYPE,
+ ])],
'file' => [
'required',
'file',
diff --git a/be/Modules/MembershipApplication/Repositories/MembershipApplicationRepository.php b/be/Modules/MembershipApplication/Repositories/MembershipApplicationRepository.php
index 679ef22..06fccd4 100644
--- a/be/Modules/MembershipApplication/Repositories/MembershipApplicationRepository.php
+++ b/be/Modules/MembershipApplication/Repositories/MembershipApplicationRepository.php
@@ -23,6 +23,7 @@ class MembershipApplicationRepository implements MembershipApplicationRepository
return MembershipApplication::with([
'applicant',
'heirs',
+ 'user:id,member_number',
'references.member:id,name,ic_number',
'documents',
'reviews.reviewer:id,name',
diff --git a/be/Modules/MembershipApplication/Resources/views/emails/failed.blade.php b/be/Modules/MembershipApplication/Resources/views/emails/failed.blade.php
index 62b70a8..2982892 100644
--- a/be/Modules/MembershipApplication/Resources/views/emails/failed.blade.php
+++ b/be/Modules/MembershipApplication/Resources/views/emails/failed.blade.php
@@ -1,18 +1,15 @@
@component('mail::message')
-@if (! empty($logoPath) && file_exists($logoPath))
-
-
-
-@endif
+@include('emails.partials.header')
# Keputusan Permohonan Keahlian
-Assalamualaikum **{{ $name }}**,
+Assalamualaikum **{{ $applicant->name }}**,
-Permohonan keahlian anda (**{{ $applicationNumber }}**) **tidak diluluskan** pada peringkat lembaga.
+Permohonan keahlian anda (**{{ $application->application_number }}**) **tidak diluluskan** pada peringkat lembaga.
+
+Surat keputusan rasmi dilampirkan dalam e-mel ini.
Terima kasih atas minat anda. Untuk sebarang pertanyaan, sila hubungi pejabat koperasi.
-Terima kasih,
-{{ config('app.name') }}
+@include('emails.partials.footer')
@endcomponent
diff --git a/be/Modules/MembershipApplication/Resources/views/emails/passed.blade.php b/be/Modules/MembershipApplication/Resources/views/emails/passed.blade.php
index 3c962c2..b9752cd 100644
--- a/be/Modules/MembershipApplication/Resources/views/emails/passed.blade.php
+++ b/be/Modules/MembershipApplication/Resources/views/emails/passed.blade.php
@@ -1,18 +1,15 @@
@component('mail::message')
-@if (! empty($logoPath) && file_exists($logoPath))
-
-
-
-@endif
+@include('emails.partials.header')
# Keputusan Permohonan Keahlian
-Assalamualaikum **{{ $name }}**,
+Assalamualaikum **{{ $applicant->name }}**,
-Permohonan keahlian anda (**{{ $applicationNumber }}**) **diluluskan** pada peringkat lembaga.
+Permohonan keahlian anda (**{{ $application->application_number }}**) **diluluskan** pada peringkat lembaga.
+
+Surat keputusan rasmi dilampirkan dalam e-mel ini.
Terima kasih atas minat anda. Untuk sebarang pertanyaan, sila hubungi pejabat koperasi.
-Terima kasih,
-{{ config('app.name') }}
+@include('emails.partials.footer')
@endcomponent
diff --git a/be/Modules/MembershipApplication/Resources/views/pdf/approved-letter.blade.php b/be/Modules/MembershipApplication/Resources/views/pdf/approved-letter.blade.php
index cdfc055..8646b8d 100644
--- a/be/Modules/MembershipApplication/Resources/views/pdf/approved-letter.blade.php
+++ b/be/Modules/MembershipApplication/Resources/views/pdf/approved-letter.blade.php
@@ -22,7 +22,7 @@
2. {{ $openingTone }} dimaklumkan bahawa, permohonan tuan untuk menjadi anggota Koperasi
- Permodalan Kelantan Berhad (KoPKB) telah {{ $boardResultLabel }} dalam {{ $boardMeetingReference }}.
+ Permodalan Kelantan Berhad (KoPKB) telah {{ $boardResultLabel }} dalam {{ $boardMeetingReference }}@if ($isPassed && $memberNumber). Nombor keanggotaan tuan adalah {{ $memberNumber }} @endif.
@if ($isPassed)
@@ -67,7 +67,7 @@
KOPERASI PERMODALAN KELANTAN BERHAD
- MUHAMMAD NAFIS BIN ZAINUDDIN
+ MUHAMMAD NAFIS BIN ZAINUDIN
Pengurus Besar
b/p Setiausaha
@endsection
diff --git a/be/Modules/MembershipApplication/Routes/api.php b/be/Modules/MembershipApplication/Routes/api.php
index 5259d36..7c5e34c 100644
--- a/be/Modules/MembershipApplication/Routes/api.php
+++ b/be/Modules/MembershipApplication/Routes/api.php
@@ -38,6 +38,7 @@ Route::middleware(['auth:sanctum', 'single.session'])->prefix('v1')->group(funct
// Membership application upload document routes
Route::post('membership-applications/{membershipApplication}/documents', [MembershipApplicationController::class, 'uploadDocument'])->name('membership-application.upload-document');
+ Route::delete('membership-applications/{membershipApplication}/documents/{documentId}', [MembershipApplicationController::class, 'deleteDocument'])->name('membership-application.delete-document');
// download document routes
Route::get('membership-applications/{membershipApplication}/documents/{documentId}/download', [MembershipApplicationController::class, 'downloadDocument'])->name('membership-application.download-document');
diff --git a/be/Modules/MembershipApplication/Services/MembershipApplicationService.php b/be/Modules/MembershipApplication/Services/MembershipApplicationService.php
index 10c6284..f5a6ba8 100644
--- a/be/Modules/MembershipApplication/Services/MembershipApplicationService.php
+++ b/be/Modules/MembershipApplication/Services/MembershipApplicationService.php
@@ -2,6 +2,7 @@
namespace Modules\MembershipApplication\Services;
+use App\Models\Document;
use App\Services\DocumentService;
use App\Services\LetterService;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
@@ -137,15 +138,38 @@ class MembershipApplicationService
$this->documentService->validateFile($file);
- $application->documentsOfType($type)->get()->each(
- fn ($document) => $this->documentService->deleteDocument($document->id)
- );
+ if ($type !== MembershipApplication::ADMIN_ATTACHMENT_DOCUMENT_TYPE) {
+ $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 deleteDocument(MembershipApplication $application, string $documentId): MembershipApplication
+ {
+ if ($application->status === ApplicationStatus::Completed) {
+ throw ValidationException::withMessages([
+ 'status' => ['Permohonan yang telah selesai tidak boleh dikemaskini.'],
+ ]);
+ }
+
+ $document = $application->documents()->findOrFail($documentId);
+
+ if ($document->type !== MembershipApplication::ADMIN_ATTACHMENT_DOCUMENT_TYPE) {
+ throw ValidationException::withMessages([
+ 'document' => ['Hanya lampiran pentadbir boleh dipadam.'],
+ ]);
+ }
+
+ $this->documentService->deleteDocument($document->id);
+
+ 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([
@@ -230,8 +254,11 @@ class MembershipApplicationService
});
}
- public function complete(MembershipApplication $application, User $processedBy): MembershipApplication
- {
+ public function complete(
+ MembershipApplication $application,
+ User $processedBy,
+ string $boardMeetingReference,
+ ): MembershipApplication {
if ($application->status !== ApplicationStatus::PendingNotification) {
throw ValidationException::withMessages([
'status' => ['Permohonan ini tidak boleh diselesaikan pada masa ini.'],
@@ -244,13 +271,16 @@ class MembershipApplicationService
]);
}
- return DB::transaction(function () use ($application) {
+ return DB::transaction(function () use ($application, $boardMeetingReference) {
if ($application->board_result === BoardResult::Pass->value && ! $application->user_id) {
$user = $this->createMemberFromApplication($application);
$application->update(['user_id' => $user->id]);
+ $application->refresh();
}
- $this->sendResultNotification($application);
+ $resultLetter = $this->storeResultLetter($application, $boardMeetingReference);
+
+ $this->sendResultNotification($application, $resultLetter);
$application->update([
'status' => ApplicationStatus::Completed,
@@ -268,7 +298,7 @@ class MembershipApplicationService
* failed: list
* }
*/
- public function completeBatch(array $applicationIds, User $processedBy): array
+ public function completeBatch(array $applicationIds, User $processedBy, string $boardMeetingReference): array
{
$succeeded = [];
$failed = [];
@@ -287,7 +317,7 @@ class MembershipApplicationService
}
try {
- $succeeded[] = $this->complete($application, $processedBy);
+ $succeeded[] = $this->complete($application, $processedBy, $boardMeetingReference);
} catch (ValidationException $e) {
$failed[] = [
'id' => $id,
@@ -317,40 +347,47 @@ class MembershipApplicationService
}
return DB::transaction(function () use ($application, $boardMeetingReference) {
- $lockedApplication = MembershipApplication::query()
- ->whereKey($application->id)
- ->lockForUpdate()
- ->firstOrFail();
-
- if ($lockedApplication->hasDocumentsOfType(MembershipApplication::RESULT_LETTER_DOCUMENT_TYPE)) {
- throw ValidationException::withMessages([
- 'result_letter' => ['Surat keputusan telah dijana dan tidak boleh dijana semula.'],
- ]);
- }
-
- $application = $this->repository->findByIdWithRelations($lockedApplication->id);
-
- if (! $application || ! $application->applicant) {
- throw ValidationException::withMessages([
- 'application' => ['Maklumat pemohon tidak lengkap.'],
- ]);
- }
-
- $viewData = $this->buildResultLetterViewData($application, $boardMeetingReference);
- $pdf = $this->letterService->renderPdf('membershipapplication::pdf.approved-letter', $viewData);
- $fileName = 'surat-keputusan-'.$application->application_number.'.pdf';
-
- $this->documentService->storeGeneratedDocument(
- $application,
- $pdf,
- $fileName,
- MembershipApplication::RESULT_LETTER_DOCUMENT_TYPE,
- );
+ $this->storeResultLetter($application, $boardMeetingReference);
return $this->repository->findByIdWithRelations($application->id);
});
}
+ protected function storeResultLetter(
+ MembershipApplication $application,
+ string $boardMeetingReference,
+ ): Document {
+ $lockedApplication = MembershipApplication::query()
+ ->whereKey($application->id)
+ ->lockForUpdate()
+ ->firstOrFail();
+
+ if ($lockedApplication->hasDocumentsOfType(MembershipApplication::RESULT_LETTER_DOCUMENT_TYPE)) {
+ throw ValidationException::withMessages([
+ 'result_letter' => ['Surat keputusan telah dijana dan tidak boleh dijana semula.'],
+ ]);
+ }
+
+ $application = $this->repository->findByIdWithRelations($lockedApplication->id);
+
+ if (! $application || ! $application->applicant) {
+ throw ValidationException::withMessages([
+ 'application' => ['Maklumat pemohon tidak lengkap.'],
+ ]);
+ }
+
+ $viewData = $this->buildResultLetterViewData($application, $boardMeetingReference);
+ $pdf = $this->letterService->renderPdf('membershipapplication::pdf.approved-letter', $viewData);
+ $fileName = 'surat-keputusan-'.$application->application_number.'.pdf';
+
+ return $this->documentService->storeGeneratedDocument(
+ $application,
+ $pdf,
+ $fileName,
+ MembershipApplication::RESULT_LETTER_DOCUMENT_TYPE,
+ );
+ }
+
/**
* @return array
*/
@@ -378,12 +415,13 @@ class MembershipApplicationService
): array {
$applicant = $application->applicant;
$isPassed = $application->board_result === BoardResult::Pass->value;
- $monthlyShareInstallment = number_format(
- MembershipApplication::MINIMUM_SHARE_CAPITAL / MembershipApplication::SHARE_INSTALLMENT_MONTHS,
- 2,
- '.',
- '',
- );
+ // $monthlyShareInstallment = number_format(
+ // MembershipApplication::MINIMUM_SHARE_CAPITAL / MembershipApplication::SHARE_INSTALLMENT_MONTHS,
+ // 2,
+ // '.',
+ // '',
+ // );
+ $monthlyShareInstallment = 84;
return [
'application' => $application,
@@ -400,6 +438,7 @@ class MembershipApplicationService
'feeMonthlyContribution' => number_format((float) $applicant->fee_monthly_contribution, 2, '.', ''),
'shareInstallmentMonths' => MembershipApplication::SHARE_INSTALLMENT_MONTHS,
'minimumShareCapital' => number_format(MembershipApplication::MINIMUM_SHARE_CAPITAL, 2, '.', ''),
+ 'memberNumber' => $isPassed ? $application->user?->member_number : null,
];
}
@@ -488,20 +527,30 @@ class MembershipApplicationService
->notify(new MembershipApplicationSubmittedNotification($application));
}
- protected function sendResultNotification(MembershipApplication $application): void
+ protected function sendResultNotification(MembershipApplication $application, Document $resultLetter): void
{
$application->loadMissing('applicant');
$notification = $application->board_result === BoardResult::Pass->value
- ? new MembershipApplicationPassedNotification($application)
- : new MembershipApplicationFailedNotification($application);
+ ? new MembershipApplicationPassedNotification($application, $resultLetter)
+ : new MembershipApplicationFailedNotification($application, $resultLetter);
Notification::route('mail', $application->applicant->email)->notify($notification);
}
protected function generateMemberNumber(): int
{
- return ((int) User::max('member_number')) + 1;
+ $latestUser = User::query()
+ ->whereNotNull('member_number')
+ ->orderByDesc('member_number')
+ ->lockForUpdate()
+ ->first();
+
+ if ($latestUser === null && DB::getDriverName() === 'pgsql') {
+ DB::selectOne('SELECT pg_advisory_xact_lock(?) AS locked', [742891035]);
+ }
+
+ return ((int) ($latestUser?->member_number ?? 0)) + 1;
}
/**
diff --git a/be/database/migrations/0001_01_01_000002_create_jobs_table.php b/be/database/migrations/0001_01_01_000002_create_jobs_table.php
index e8ec6ec..425e705 100644
--- a/be/database/migrations/0001_01_01_000002_create_jobs_table.php
+++ b/be/database/migrations/0001_01_01_000002_create_jobs_table.php
@@ -12,7 +12,7 @@ return new class extends Migration
public function up(): void
{
Schema::create('jobs', function (Blueprint $table) {
- $table->uuid('id')->primary();
+ $table->id();
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
@@ -22,7 +22,7 @@ return new class extends Migration
});
Schema::create('job_batches', function (Blueprint $table) {
- $table->uuid('id')->primary();
+ $table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
@@ -35,7 +35,7 @@ return new class extends Migration
});
Schema::create('failed_jobs', function (Blueprint $table) {
- $table->uuid('id')->primary();
+ $table->id();
$table->string('uuid')->unique();
$table->text('connection');
$table->text('queue');
diff --git a/fe/src/assets/images/logo-kopkb.svg b/fe/src/assets/images/logo-kopkb.svg
deleted file mode 100644
index ef88ccb..0000000
--- a/fe/src/assets/images/logo-kopkb.svg
+++ /dev/null
@@ -1,418 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/fe/src/assets/images/logo.svg b/fe/src/assets/images/logo.svg
index 28c5228..ef88ccb 100644
--- a/fe/src/assets/images/logo.svg
+++ b/fe/src/assets/images/logo.svg
@@ -1,9 +1,418 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fe/src/docs/pages/accordion.vue b/fe/src/docs/pages/accordion.vue
deleted file mode 100644
index 2ed3e06..0000000
--- a/fe/src/docs/pages/accordion.vue
+++ /dev/null
@@ -1,129 +0,0 @@
-
-
-
-
-
-
-
-
-
- Product Information
-
-
- Our flagship product combines cutting-edge technology with
- sleek design. Built with premium materials, it offers
- unparalleled performance and reliability.
-
-
- Key features include advanced processing capabilities, and an
- intuitive user interface designed for both beginners and
- experts.
-
-
-
-
- Shipping Details
-
-
- We offer worldwide shipping through trusted courier partners.
- Standard delivery takes 3-5 business days, while express
- shipping ensures delivery within 1-2 business days.
-
-
- All orders are carefully packaged and fully insured. Track
- your shipment in real-time through our dedicated tracking
- portal.
-
-
-
-
- Return Policy
-
-
- We stand behind our products with a comprehensive 30-day
- return policy. If you're not completely satisfied, simply
- return the item in its original condition.
-
-
- Our hassle-free return process includes free return shipping
- and full refunds processed within 48 hours of receiving the
- returned item.
-
-
-
-
-
-
-
-
- {{ `
-
-
-
- Product Information
-
-
- Our flagship product combines cutting-edge technology with
- sleek design. Built with premium materials, it offers
- unparalleled performance and reliability.
-
-
- Key features include advanced processing capabilities, and an
- intuitive user interface designed for both beginners and
- experts.
-
-
-
-
- Shipping Details
-
-
- We offer worldwide shipping through trusted courier partners.
- Standard delivery takes 3-5 business days, while express
- shipping ensures delivery within 1-2 business days.
-
-
- All orders are carefully packaged and fully insured. Track
- your shipment in real-time through our dedicated tracking
- portal.
-
-
-
-
- Return Policy
-
-
- We stand behind our products with a comprehensive 30-day
- return policy. If you're not completely satisfied, simply
- return the item in its original condition.
-
-
- Our hassle-free return process includes free return shipping
- and full refunds processed within 48 hours of receiving the
- returned item.
-
-
-
-
-
- ` }}
-
-
-
-
-
diff --git a/fe/src/docs/pages/avatar.vue b/fe/src/docs/pages/avatar.vue
deleted file mode 100644
index 5001ebf..0000000
--- a/fe/src/docs/pages/avatar.vue
+++ /dev/null
@@ -1,239 +0,0 @@
-
-
-
-
-
-
-
- PA
-
-
-
-
-
- {{ `
-
- PA
-
-
- ` }}
-
-
-
-
-
-
Installation
-
Install the following dependencies:
-
add @zag-js/vue @zag-js/avatar
-
- Copy and paste the following code into your project.
-
-
- {{ `
-
-
-
-
-
-
-
-
-
-
- ` }}
-
-
- {{ `
-
-
-
-
-
- ` }}
-
-
- {{`
-
-
-
-
-
-
-
-
-
-
- ` }}
-
-
- {{ `
- export { default as AvatarRoot } from "./AvatarRoot.vue";
- export { default as AvatarFallback } from "./AvatarFallback.vue";
- export { default as AvatarImage } from "./AvatarImage.vue";
- ` }}
-
-
- Update the import paths to match your project setup.
-
-
-
-
Usage
-
- {{ `
- import {
- AvatarRoot,
- AvatarFallback,
- AvatarImage,
- } from "@/components/ui/avatar";
- ` }}
-
-
- {{ `
-
- PA
-
-
- ` }}
-
-
-
-
Variants
-
A collection of components you can use.
-
-
-
- PA
-
-
-
-
-
- {{ `
-
- PA
-
-
- ` }}
-
-
-
-
-
-
- PA
-
-
-
-
-
- {{ `
-
- PA
-
-
- ` }}
-
-
-
-
-
-
- PA
-
-
-
-
-
- {{ `
-
- PA
-
-
- ` }}
-
-
-
-
-
diff --git a/fe/src/docs/pages/badge.vue b/fe/src/docs/pages/badge.vue
deleted file mode 100644
index 46f364d..0000000
--- a/fe/src/docs/pages/badge.vue
+++ /dev/null
@@ -1,351 +0,0 @@
-
-
-
-
-
-
- 12%
-
-
-
- {{ `
- 12%
- ` }}
-
-
-
-
-
-
Installation
-
- Copy and paste the following code into your project.
-
-
- {{ `
-
-
-
-
-
-
-
-
-
-
- \{\{ content \}\}
-
-
-
- ` }}
-
-
- {{ `
- export { default as Badge } from "./Badge.vue";
- ` }}
-
-
- Update the import paths to match your project setup.
-
-
-
-
Usage
-
- {{ `
- import { Badge } from "@/components/ui/badge";
- ` }}
-
-
- {{ `
- 12%
- ` }}
-
-
-
-
Variants
-
A collection of components you can use.
-
-
- 12%
- 12%
- 12%
- 12%
- 12%
- 12%
-
-
-
- {{ `
- 12%
- 12%
- 12%
- 12%
- 12%
- 12%
- ` }}
-
-
-
-
-
- 12%
-
-
- 12%
-
-
- 12%
-
-
- 12%
-
-
- 12%
-
-
- 12%
-
-
-
-
-
- {{ `
- 12%
-
-
- 12%
-
-
- 12%
-
-
- 12%
-
-
- 12%
-
-
- 12%
-
-
- ` }}
-
-
-
-
-
- 12%
- 12%
- 12%
- 12%
- 12%
- 12%
-
-
-
- {{ `
- 12%
- 12%
- 12%
- 12%
- 12%
- 12%
- ` }}
-
-
-
-
-
- 12%
-
-
- 12%
-
-
- 12%
-
-
- 12%
-
-
- 12%
-
-
- 12%
-
-
-
-
-
- {{ `
- 12%
-
-
- 12%
-
-
- 12%
-
-
- 12%
-
-
- 12%
-
-
- 12%
-
-
- ` }}
-
-
-
-
-
- 12%
- 12%
- 12%
- 12%
- 12%
- 12%
-
-
-
- {{ `
- 12%
- 12%
- 12%
- 12%
- 12%
- 12%
- ` }}
-
-
-
-
-
- 12%
-
-
- 12%
-
-
- 12%
-
-
- 12%
-
-
- 12%
-
-
- 12%
-
-
-
-
-
- {{ `
- 12%
-
-
- 12%
-
-
- 12%
-
-
- 12%
-
-
- 12%
-
-
- 12%
-
-
- ` }}
-
-
-
-
-
-
- 12%
-
-
- 12%
-
-
- 12%
-
-
- 12%
-
-
- 12%
-
-
- 12%
-
-
-
-
- {{ `
-
- 12%
-
-
- 12%
-
-
- 12%
-
-
- 12%
-
-
- 12%
-
-
- 12%
-
- ` }}
-
-
-
-
-
diff --git a/fe/src/docs/pages/button.vue b/fe/src/docs/pages/button.vue
deleted file mode 100644
index 07dd73d..0000000
--- a/fe/src/docs/pages/button.vue
+++ /dev/null
@@ -1,1743 +0,0 @@
-
-
-
-
-
-
- Button Small
- Button Medium
- Button Large
- Button Extra Large
-
-
-
- {{ `
- Button Small
- Button Medium
- Button Large
- Button Extra Large
- ` }}
-
-
-
-
-
-
Installation
-
- Copy and paste the following code into your project.
-
-
- {{ `
-
-
-
-
-
-
-
-
-
-
- ` }}
-
-
- {{ `
- export { default as Button } from "./Button.vue";
- ` }}
-
-
- Update the import paths to match your project setup.
-
-
-
-
Usage
-
- {{ `
- import { Button } from "@/components/ui/button";
- ` }}
-
-
- {{ `
- Button Medium
- ` }}
-
-
-
-
Variants
-
A collection of components you can use.
-
-
-
-
- Button Small
-
- Button Medium
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
- Button Medium
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
- Button Medium
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
- Button Medium
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
- Button Medium
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
- Button Medium
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
-
- {{ `
-
-
- Button Small
-
- Button Medium
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
- Button Medium
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
- Button Medium
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
- Button Medium
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
- Button Medium
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
- Button Medium
-
- Button Large
-
-
- Button Extra Large
-
-
- ` }}
-
-
-
-
-
-
-
- Button Small
-
- Button Medium
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
-
- {{ `
-
-
- Button Small
-
- Button Medium
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
- ` }}
-
-
-
-
-
-
-
- Button Small
-
- Button Medium
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
-
- {{ `
-
-
- Button Small
-
- Button Medium
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
- ` }}
-
-
-
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
-
- {{ `
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
- ` }}
-
-
-
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
-
- {{ `
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
- ` }}
-
-
-
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
-
- {{ `
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
-
-
- Button Small
-
-
- Button Medium
-
-
- Button Large
-
-
- Button Extra Large
-
-
- ` }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ` }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ` }}
-
-
-
-
-
diff --git a/fe/src/docs/pages/carousel.vue b/fe/src/docs/pages/carousel.vue
deleted file mode 100644
index 15ec67a..0000000
--- a/fe/src/docs/pages/carousel.vue
+++ /dev/null
@@ -1,445 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ index + 1 }}
-
-
-
-
-
-
- {{`
- const images = Array.from(
- { length: 5 },
- (_, i) => \`https://picsum.photos/seed/\${i + 1}/500/300\`
- );
-
-
-
-
-
-
-
-
-
-
-
- \{\{ index + 1 \}\}
-
-
-
- ` }}
-
-
-
-
-
-
Installation
-
Install the following dependencies:
-
add @zag-js/vue @zag-js/carousel
-
- Copy and paste the following code into your project.
-
-
- {{`
-
-
-
-
-
-
-
-
-
-
- ` }}
-
-
- {{ `
-
-
-
-
-
-
-
-
-
-
- ` }}
-
-
- {{ `
-
-
-
-
-
-
-
-
-
-
-
- ` }}
-
-
- {{ `
-
-
-
-
-
-
-
-
-
-
-
- ` }}
-
-
- {{ `
-
-
-
-
-
-
-
-
-
-
- ` }}
-
-
- {{ `
-
-
-
-
-
- ` }}
-
-
- {{ `
-
-
-
-
-
-
-
-
-
-
- ` }}
-
-
- {{ `
-
-
-
-
-
-
-
-
-
-
- ` }}
-
-
- {{ `
- export { default as CarouselRoot } from "./CarouselRoot.vue";
- export { default as CarouselControl } from "./CarouselControl.vue";
- export { default as CarouselPrevTrigger } from "./CarouselPrevTrigger.vue";
- export { default as CarouselNextTrigger } from "./CarouselNextTrigger.vue";
- export { default as CarouselIndicatorGroup } from "./CarouselIndicatorGroup.vue";
- export { default as CarouselIndicator } from "./CarouselIndicator.vue";
- export { default as CarouselItemGroup } from "./CarouselItemGroup.vue";
- export { default as CarouselItem } from "./CarouselItem.vue";
- ` }}
-
-
- Update the import paths to match your project setup.
-
-
-
-
Usage
-
- {{ `
- import {
- CarouselRoot,
- CarouselControl,
- CarouselPrevTrigger,
- CarouselNextTrigger,
- CarouselIndicatorGroup,
- CarouselIndicator,
- CarouselItemGroup,
- CarouselItem,
- } from "@/components/ui/carousel";
- ` }}
-
-
- {{ `
-
-
-
-
-
-
-
-
-
-
- \{\{ index + 1 \}\}
-
-
-
- ` }}
-
-
-
-
Variants
-
A collection of components you can use.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ` }}
-
-
-
-
-
diff --git a/fe/src/docs/pages/checkbox.vue b/fe/src/docs/pages/checkbox.vue
deleted file mode 100644
index b2950c2..0000000
--- a/fe/src/docs/pages/checkbox.vue
+++ /dev/null
@@ -1,235 +0,0 @@
-
-
-
-
-
-
-
-
- Accept terms and conditions
-
-
-
-
- {{ `
-
-
- Accept terms and conditions
-
- ` }}
-
-
-
-
-
-
Installation
-
Install the following dependencies:
-
add @zag-js/vue @zag-js/checkbox
-
- Copy and paste the following code into your project.
-
-
- {{`
-
-
-
-
-
-
-
-
- ` }}
-
-
- {{ `
-
-
-
-
-
-
-
-
-
-
- ` }}
-
-
- {{ `
-
-
-
-
-
-
-
-
-
-
-
-
- ` }}
-
-
- {{ `
-
-
-
-
-
-
-
-
-
-
- ` }}
-
-
- {{ `
-
-
-
-
-
- ` }}
-
-
- {{ `
- export { default as CheckboxRoot } from "./CheckboxRoot.vue";
- export { default as CheckboxLabel } from "./CheckboxLabel.vue";
- export { default as CheckboxControl } from "./CheckboxControl.vue";
- export { default as CheckboxIndicator } from "./CheckboxIndicator.vue";
- export { default as CheckboxHiddenInput } from "./CheckboxHiddenInput.vue";
- ` }}
-
-
- Update the import paths to match your project setup.
-
-
-
-
Usage
-
- {{ `
- import {
- CheckboxRoot,
- CheckboxLabel,
- CheckboxControl,
- } from "@/components/ui/checkbox";
- ` }}
-
-
- {{ `
-
-
- Accept terms and conditions
-
- ` }}
-
-
-
diff --git a/fe/src/docs/pages/input.vue b/fe/src/docs/pages/input.vue
deleted file mode 100644
index 3a0e2bc..0000000
--- a/fe/src/docs/pages/input.vue
+++ /dev/null
@@ -1,74 +0,0 @@
-
-
-
-
-
-
Installation
-
- Copy and paste the following code into your project.
-
-
- {{ `
-
-
-
-
-
- ` }}
-
-
- {{ `
- export { default as Input } from "./Input.vue";
- ` }}
-
-
- Update the import paths to match your project setup.
-
-
-
-
Usage
-
- {{ `
- import { Input } from "@/components/ui/input";
- ` }}
-
-
- {{ `
-
- ` }}
-
-
-
diff --git a/fe/src/docs/pages/map.vue b/fe/src/docs/pages/map.vue
deleted file mode 100644
index f6d5c6c..0000000
--- a/fe/src/docs/pages/map.vue
+++ /dev/null
@@ -1,369 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- {{ `
-
- ` }}
-
-
-
-
-
-
Installation
-
Install the following dependencies:
-
add maplibre-gl
-
- Copy and paste the following code into your project.
-
-
- {{`
- <${""}script setup lang="ts">
-import "maplibre-gl/dist/maplibre-gl.css";
-import maplibregl, { type MapOptions } from "maplibre-gl";
-import { ref, onMounted } from "vue";
-import { Button } from "@/components/ui/button";
-import { Plus, Minus, MapPin, Compass, Expand, X } from "@lucide/vue";
-import type { FeatureCollection, Point } from "geojson";
-import { cn } from "@mykopkb/core/utils/cn";
-import { map } from "@mykopkb/core/styles/map.styles";
-
-const mapRef = ref();
-const mapInstance = ref();
-const isFullscreen = ref(false);
-
-const {
- class: className,
- markers,
- ...props
-} = defineProps<
- Partial & {
- class?: string;
- markers?: FeatureCollection;
- }
->();
-
-onMounted(() => {
- mapInstance.value = new maplibregl.Map({
- ...Object.fromEntries(Object.entries(props).filter(([_, value]) => value)),
- style: "https://tiles.openfreemap.org/styles/positron",
- container: mapRef.value,
- });
-
- markers &&
- mapInstance.value.on("load", () => {
- // Add source with clustering
- mapInstance.value!.addSource("markers", {
- type: "geojson",
- data: markers,
- cluster: true,
- clusterMaxZoom: 14, // Max zoom for clustering
- clusterRadius: 50, // Cluster radius in pixels
- });
-
- // Layer for clusters (circles)
- mapInstance.value!.addLayer({
- id: "clusters",
- type: "circle",
- source: "markers",
- filter: ["has", "point_count"],
- paint: {
- "circle-color": [
- "step",
- ["get", "point_count"],
- "#cccccc", // Color for < 5 points
- 5,
- "#cccccc", // Color for 5-10 points
- 10,
- "#cccccc", // Color for > 10 points
- ],
- "circle-radius": [
- "step",
- ["get", "point_count"],
- 20, // Radius for < 5 points
- 5,
- 30, // Radius for 5-10 points
- 10,
- 40, // Radius for > 10 points
- ],
- },
- });
-
- // Layer for cluster count numbers
- mapInstance.value!.addLayer({
- id: "cluster-count",
- type: "symbol",
- source: "markers",
- filter: ["has", "point_count"],
- layout: {
- "text-field": "{point_count_abbreviated}",
- "text-font": ["Open Sans Bold"],
- "text-size": 12,
- },
- });
-
- // Layer for individual points (unclustered)
- mapInstance.value!.addLayer({
- id: "unclustered-point",
- type: "circle",
- source: "markers",
- filter: ["!", ["has", "point_count"]],
- paint: {
- "circle-color": "#333333",
- "circle-radius": 8,
- "circle-stroke-width": 2,
- "circle-stroke-color": "#ffffff",
- },
- });
-
- // Click on cluster to zoom in
- mapInstance.value!.on("click", "clusters", async (e) => {
- const features = mapInstance.value!.queryRenderedFeatures(e.point, {
- layers: ["clusters"],
- });
-
- const clusterId = features[0]?.properties.cluster_id;
- const source = mapInstance.value!.getSource(
- "markers"
- ) as maplibregl.GeoJSONSource;
-
- try {
- const zoom = await source.getClusterExpansionZoom(clusterId);
-
- mapInstance.value!.easeTo({
- center: (features[0]?.geometry as any).coordinates,
- zoom: zoom,
- });
- } catch (err) {
- console.error("Error getting cluster expansion zoom:", err);
- }
- });
-
- // Click on individual point to show popup
- mapInstance.value!.on("click", "unclustered-point", (e) => {
- const coordinates = (
- e.features![0]?.geometry as any
- ).coordinates.slice();
- const { name, type } = (e.features![0]?.properties || {}) as any;
-
- new maplibregl.Popup()
- .setLngLat(coordinates)
- .setHTML(\`\${name} \${type}
\`)
- .addTo(mapInstance.value!);
- });
-
- // Change cursor on hover cluster/point
- mapInstance.value!.on("mouseenter", "clusters", () => {
- mapInstance.value!.getCanvas().style.cursor = "pointer";
- });
- mapInstance.value!.on("mouseleave", "clusters", () => {
- mapInstance.value!.getCanvas().style.cursor = "";
- });
- mapInstance.value!.on("mouseenter", "unclustered-point", () => {
- mapInstance.value!.getCanvas().style.cursor = "pointer";
- });
- mapInstance.value!.on("mouseleave", "unclustered-point", () => {
- mapInstance.value!.getCanvas().style.cursor = "";
- });
- });
-});
-
-const zoomIn = () => {
- mapInstance.value?.zoomIn();
-};
-
-const zoomOut = () => {
- mapInstance.value?.zoomOut();
-};
-
-const resetNorth = () => {
- mapInstance.value?.easeTo({ bearing: 0, pitch: 0 });
-};
-
-const locateMe = () => {
- if (!navigator.geolocation) {
- alert("Geolocation is not supported by your browser");
- return;
- }
-
- navigator.geolocation.getCurrentPosition(
- (position) => {
- const { longitude, latitude } = position.coords;
- mapInstance.value?.flyTo({
- center: [longitude, latitude],
- zoom: 14,
- });
-
- new maplibregl.Marker({ color: "var(--color-foreground)" })
- .setLngLat([longitude, latitude])
- .addTo(mapInstance.value!);
- },
- (error) => {
- alert("Failed to get location: " + error.message);
- }
- );
-};
-
-const toggleFullscreen = () => {
- if (!mapRef.value) return;
-
- if (!isFullscreen.value) {
- if (mapRef.value.requestFullscreen) {
- mapRef.value.requestFullscreen();
- }
- } else {
- if (document.exitFullscreen) {
- document.exitFullscreen();
- }
- }
- isFullscreen.value = !isFullscreen.value;
-};
-${""}script>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ` }}
-
-
- {{ `
- export { default as Map } from "./Map.vue";
- ` }}
-
-
- Update the import paths to match your project setup.
-
-
-
-
Usage
-
- {{ `
- import { Map } from "@/components/ui/map";
- ` }}
-
-
- {{ `
-
- ` }}
-
-
-
diff --git a/fe/src/docs/pages/table.vue b/fe/src/docs/pages/table.vue
deleted file mode 100644
index df6e3ad..0000000
--- a/fe/src/docs/pages/table.vue
+++ /dev/null
@@ -1,653 +0,0 @@
-
-
-
-
-
-
-
- A list of your recent invoices.
-
-
- Invoice
- Status
- Method
- Amount
-
-
-
-
-
- {{ invoice.invoice }}
-
-
-
- {{ invoice.paymentStatus }}
-
-
- {{ invoice.paymentMethod }}
-
- {{ invoice.totalAmount }}
-
-
-
-
-
- Total
- $2,500.00
-
-
-
-
-
-
- {{ `
-
- A list of your recent invoices.
-
-
- Invoice
- Status
- Method
- Amount
-
-
-
-
-
- \{\{ invoice.invoice \}\}
-
-
-
- \{\{ invoice.paymentStatus \}\}
-
-
- \{\{ invoice.paymentMethod \}\}
-
- \{\{ invoice.totalAmount \}\}
-
-
-
-
-
- Total
- $2,500.00
-
-
-
- ` }}
-
-
-
-
-
-
Installation
-
- Copy and paste the following code into your project.
-
-
- {{`
-
-
-
-
-
-
-
- ` }}
-
-
- {{ `
-
-
-
-
-
-
-
- ` }}
-
-
- {{ `
-
-
-
-
-
-
-
- ` }}
-
-
- {{ `
-
-
-
-
-
-
-
- ` }}
-
-
- {{ `
-
-
-
-
-
-
-
- ` }}
-
-
- {{ `
-
-
-
-
-
-
-
- ` }}
-
-
- {{ `
-
-
-
-
-
-
-
- ` }}
-
-
- {{ `
-
-
-
-
-
-
-
- ` }}
-
-
- {{ `
-
-
-
-
-
-
-
- ` }}
-
-
- {{ `
- export { default as Table } from "./Table.vue";
- export { default as TableContainer } from "./TableContainer.vue";
- export { default as TableHeader } from "./TableHeader.vue";
- export { default as TableBody } from "./TableBody.vue";
- export { default as TableFooter } from "./TableFooter.vue";
- export { default as TableHead } from "./TableHead.vue";
- export { default as TableRow } from "./TableRow.vue";
- export { default as TableCell } from "./TableCell.vue";
- export { default as TableCaption } from "./TableCaption.vue";
- ` }}
-
-
- Update the import paths to match your project setup.
-
-
-
-
Usage
-
- {{ `
- import {
- Table,
- TableHeader,
- TableBody,
- TableFooter,
- TableHead,
- TableRow,
- TableCell,
- TableCaption,
- } from "@/components/ui/table";
- ` }}
-
-
- {{ `
-
- A list of your recent invoices.
-
-
- Invoice
- Status
- Method
- Amount
-
-
-
-
-
- \{\{ invoice.invoice \}\}
-
-
-
- \{\{ invoice.paymentStatus \}\}
-
-
- \{\{ invoice.paymentMethod \}\}
-
- \{\{ invoice.totalAmount \}\}
-
-
-
-
-
- Total
- $2,500.00
-
-
-
- ` }}
-
-
-
-
Variants
-
A collection of components you can use.
-
-
-
- A list of your recent invoices.
-
-
- Invoice
- Status
- Method
- Amount
-
-
-
-
-
- {{ invoice.invoice }}
-
-
-
- {{ invoice.paymentStatus }}
-
-
- {{ invoice.paymentMethod }}
-
- {{ invoice.totalAmount }}
-
-
-
-
-
- Total
- $2,500.00
-
-
-
-
-
-
- {{ `
-
- A list of your recent invoices.
-
-
- Invoice
- Status
- Method
- Amount
-
-
-
-
-
- \{\{ invoice.invoice \}\}
-
-
-
- \{\{ invoice.paymentStatus \}\}
-
-
- \{\{ invoice.paymentMethod \}\}
-
- \{\{ invoice.totalAmount \}\}
-
-
-
-
-
- Total
- $2,500.00
-
-
-
- ` }}
-
-
-
-
-
-
- A list of your recent invoices.
-
-
- Invoice
- Status
- Method
- Amount
-
-
-
-
-
- {{ invoice.invoice }}
-
-
-
- {{ invoice.paymentStatus }}
-
-
- {{ invoice.paymentMethod }}
-
- {{ invoice.totalAmount }}
-
-
-
-
-
- Total
- $2,500.00
-
-
-
-
-
-
- {{ `
-
- A list of your recent invoices.
-
-
- Invoice
- Status
- Method
- Amount
-
-
-
-
-
- \{\{ invoice.invoice \}\}
-
-
-
- \{\{ invoice.paymentStatus \}\}
-
-
- \{\{ invoice.paymentMethod \}\}
-
- \{\{ invoice.totalAmount \}\}
-
-
-
-
-
- Total
- $2,500.00
-
-
-
- ` }}
-
-
-
-
-
-
- A list of your recent invoices.
-
-
- Invoice
- Status
- Method
- Amount
-
-
-
-
-
- {{ invoice.invoice }}
-
-
-
- {{ invoice.paymentStatus }}
-
-
- {{ invoice.paymentMethod }}
-
- {{ invoice.totalAmount }}
-
-
-
-
-
- Total
- $2,500.00
-
-
-
-
-
-
- {{ `
-
- A list of your recent invoices.
-
-
- Invoice
- Status
- Method
- Amount
-
-
-
-
-
- \{\{ invoice.invoice \}\}
-
-
-
- \{\{ invoice.paymentStatus \}\}
-
-
- \{\{ invoice.paymentMethod \}\}
-
- \{\{ invoice.totalAmount \}\}
-
-
-
-
-
- Total
- $2,500.00
-
-
-
- ` }}
-
-
-
-
-
diff --git a/fe/src/main/side-menu.ts b/fe/src/main/side-menu.ts
index 3fd1bab..67fbe26 100644
--- a/fe/src/main/side-menu.ts
+++ b/fe/src/main/side-menu.ts
@@ -1,5 +1,4 @@
import type { Menu } from '@/core/types/menu'
-import { authMenu } from '@/modules/auth'
import { userMenu } from '@/modules/user'
import { roleMenu } from '@/modules/role'
import { membershipApplicationMenu } from '@/modules/membership-application'
@@ -8,6 +7,12 @@ import { activityMenu } from '@/modules/activity'
export type { Menu }
const mainMenu: (string | Menu)[] = [
+ 'Dashboard',
+ {
+ icon: 'CircleGauge',
+ title: 'Dashboards',
+ route_name: 'dashboard-overview-1',
+ },
'Umum',
...activityMenu,
'Teknologi Maklumat',
@@ -15,19 +20,7 @@ const mainMenu: (string | Menu)[] = [
'Pentadbiran',
...membershipApplicationMenu,
...userMenu,
- 'GENERAL REPORTS',
- {
- icon: 'CircleGauge',
- title: 'Dashboards',
- badge: 4,
- sub_menu: [
- {
- icon: 'PanelBottomClose',
- route_name: 'dashboard-overview-1',
- title: 'Overview 1',
- },
- ],
- },
+ 'E-Commerce',
{
icon: 'SquareKanban',
title: 'E-Commerce',
@@ -99,11 +92,6 @@ const mainMenu: (string | Menu)[] = [
],
},
'APPS',
- {
- icon: 'CircleGauge',
- route_name: 'inbox',
- title: 'Inbox',
- },
{
icon: 'CircleGauge',
route_name: 'file-manager',
@@ -115,218 +103,11 @@ const mainMenu: (string | Menu)[] = [
route_name: 'point-of-sale',
title: 'Point of Sale',
},
- {
- icon: 'CircleGauge',
- route_name: 'chat',
- title: 'Chat',
- badge: 3,
- },
- {
- icon: 'CircleGauge',
- route_name: 'post',
- title: 'Post',
- },
- // {
- // icon: 'CircleGauge',
- // route_name: 'calendar',
- // title: 'Calendar',
- // },
- 'PAGES',
- {
- icon: 'CircleGauge',
- title: 'Crud',
- sub_menu: [
- {
- icon: 'CircleGauge',
- route_name: 'crud-data-list',
- title: 'Data List',
- },
- ],
- },
- {
- icon: 'CircleGauge',
- title: 'Users',
- sub_menu: [
- {
- icon: 'CircleGauge',
- route_name: 'users-layout-1',
- title: 'Layout 1',
- },
- {
- icon: 'CircleGauge',
- route_name: 'users-layout-2',
- title: 'Layout 2',
- },
- {
- icon: 'CircleGauge',
- route_name: 'users-layout-3',
- title: 'Layout 3',
- },
- ],
- },
- {
- icon: 'CircleGauge',
- title: 'Pages',
- sub_menu: [
- {
- icon: 'CircleGauge',
- title: 'Wizards',
- sub_menu: [
- {
- icon: 'CircleGauge',
- route_name: 'wizard-layout-1',
- title: 'Layout 1',
- },
- {
- icon: 'CircleGauge',
- route_name: 'wizard-layout-2',
- title: 'Layout 2',
- },
- {
- icon: 'CircleGauge',
- route_name: 'wizard-layout-3',
- title: 'Layout 3',
- },
- ],
- },
- {
- icon: 'CircleGauge',
- title: 'Blog',
- sub_menu: [
- {
- icon: 'CircleGauge',
- route_name: 'blog-layout-1',
- title: 'Layout 1',
- },
- {
- icon: 'CircleGauge',
- route_name: 'blog-layout-2',
- title: 'Layout 2',
- },
- {
- icon: 'CircleGauge',
- route_name: 'blog-layout-3',
- title: 'Layout 3',
- },
- ],
- },
- {
- icon: 'CircleGauge',
- title: 'Pricing',
- sub_menu: [
- {
- icon: 'CircleGauge',
- route_name: 'pricing-layout-1',
- title: 'Layout 1',
- },
- {
- icon: 'CircleGauge',
- route_name: 'pricing-layout-2',
- title: 'Layout 2',
- },
- ],
- },
- {
- icon: 'CircleGauge',
- title: 'Invoice',
- sub_menu: [
- {
- icon: 'CircleGauge',
- route_name: 'invoice-layout-1',
- title: 'Layout 1',
- },
- {
- icon: 'CircleGauge',
- route_name: 'invoice-layout-2',
- title: 'Layout 2',
- },
- ],
- },
- {
- icon: 'CircleGauge',
- title: 'FAQ',
- sub_menu: [
- {
- icon: 'CircleGauge',
- route_name: 'faq-layout-1',
- title: 'Layout 1',
- },
- {
- icon: 'CircleGauge',
- route_name: 'faq-layout-2',
- title: 'Layout 2',
- },
- {
- icon: 'CircleGauge',
- route_name: 'faq-layout-3',
- title: 'Layout 3',
- },
- ],
- },
- ],
- },
'UI COMPONENTS',
- {
- icon: 'CircleGauge',
- title: 'Base',
- sub_menu: [
- {
- icon: 'CircleGauge',
- route_name: 'slot',
- title: 'Slot',
- },
- {
- icon: 'CircleGauge',
- route_name: 'box',
- title: 'Box',
- },
- {
- icon: 'CircleGauge',
- route_name: 'scroll-area',
- title: 'Scroll Area',
- },
- ],
- },
- {
- icon: 'CircleGauge',
- title: 'Navigation',
- sub_menu: [
- {
- icon: 'CircleGauge',
- route_name: 'breadcrumb',
- title: 'Breadcrumb',
- },
- {
- icon: 'CircleGauge',
- route_name: 'menu',
- title: 'Menu',
- },
- {
- icon: 'CircleGauge',
- route_name: 'pagination',
- title: 'Pagination',
- },
- {
- icon: 'CircleGauge',
- route_name: 'tabs',
- title: 'Tabs',
- },
- ],
- },
{
icon: 'CircleGauge',
title: 'Forms',
sub_menu: [
- {
- icon: 'CircleGauge',
- route_name: 'button',
- title: 'Button',
- },
- {
- icon: 'CircleGauge',
- route_name: 'checkbox',
- title: 'Checkbox',
- },
{
icon: 'CircleGauge',
route_name: 'combobox',
@@ -337,21 +118,11 @@ const mainMenu: (string | Menu)[] = [
route_name: 'datepicker',
title: 'Datepicker',
},
- {
- icon: 'CircleGauge',
- route_name: 'field',
- title: 'Field',
- },
{
icon: 'CircleGauge',
route_name: 'input',
title: 'Input',
},
- {
- icon: 'CircleGauge',
- route_name: 'native-select',
- title: 'Native Select',
- },
{
icon: 'CircleGauge',
route_name: 'radio-group',
@@ -379,46 +150,10 @@ const mainMenu: (string | Menu)[] = [
},
],
},
- {
- icon: 'CircleGauge',
- title: 'Data Display',
- sub_menu: [
- {
- icon: 'CircleGauge',
- route_name: 'accordion',
- title: 'Accordion',
- },
- {
- icon: 'CircleGauge',
- route_name: 'avatar',
- title: 'Avatar',
- },
- {
- icon: 'CircleGauge',
- route_name: 'badge',
- title: 'Badge',
- },
- {
- icon: 'CircleGauge',
- route_name: 'carousel',
- title: 'Carousel',
- },
- {
- icon: 'CircleGauge',
- route_name: 'table',
- title: 'Table',
- },
- ],
- },
{
icon: 'CircleGauge',
title: 'Feedback',
sub_menu: [
- {
- icon: 'CircleGauge',
- route_name: 'alert',
- title: 'Alert',
- },
{
icon: 'CircleGauge',
route_name: 'progress-circular',
@@ -471,11 +206,6 @@ const mainMenu: (string | Menu)[] = [
route_name: 'chart',
title: 'Chart',
},
- {
- icon: 'CircleGauge',
- route_name: 'map',
- title: 'Map',
- },
],
},
]
diff --git a/fe/src/modules/auth/pages/AccountPending.vue b/fe/src/modules/auth/pages/AccountPending.vue
index 50a3dc1..9d0cdaf 100644
--- a/fe/src/modules/auth/pages/AccountPending.vue
+++ b/fe/src/modules/auth/pages/AccountPending.vue
@@ -6,8 +6,8 @@ import { Button } from '@/components/ui/button'
import { AlertRoot, AlertTitle, AlertDescription } from '@/components/ui/alert'
import { logout, resolvePostAuthRoute } from '@/modules/auth'
import { useAuthStore } from '@/stores/auth'
-import logoUrl from '@/assets/images/logo-kopkb.svg'
-import illustrationUrl from '@/assets/images/logo-kopkb.svg'
+import logoUrl from '@/assets/images/logo.svg'
+import illustrationUrl from '@/assets/images/logo.svg'
const router = useRouter()
const authStore = useAuthStore()
diff --git a/fe/src/modules/auth/pages/ForgotPassword.vue b/fe/src/modules/auth/pages/ForgotPassword.vue
index 6f90612..fec80d9 100644
--- a/fe/src/modules/auth/pages/ForgotPassword.vue
+++ b/fe/src/modules/auth/pages/ForgotPassword.vue
@@ -6,7 +6,7 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { AlertRoot, AlertTitle, AlertDescription } from '@/components/ui/alert'
import { getForgotPasswordErrorMessage, requestForgotPassword } from '@/modules/auth'
-import illustrationUrl from '@/assets/images/logo-kopkb.svg'
+import illustrationUrl from '@/assets/images/logo.svg'
const router = useRouter()
diff --git a/fe/src/modules/auth/pages/Login.vue b/fe/src/modules/auth/pages/Login.vue
index 1e4f0d5..89b6cad 100644
--- a/fe/src/modules/auth/pages/Login.vue
+++ b/fe/src/modules/auth/pages/Login.vue
@@ -13,7 +13,7 @@ import {
resolvePostAuthRoute,
} from '@/modules/auth'
import { useAuthStore } from '@/stores/auth'
-import illustrationUrl from '@/assets/images/logo-kopkb.svg'
+import illustrationUrl from '@/assets/images/logo.svg'
const router = useRouter()
const route = useRoute()
@@ -125,11 +125,8 @@ const appVersion = import.meta.env.VITE_APP_VERSION
Ingat saya
-
+
Lupa Password?
diff --git a/fe/src/modules/auth/pages/PrivacyPolicy.vue b/fe/src/modules/auth/pages/PrivacyPolicy.vue
index 6af2536..7c5b2ad 100644
--- a/fe/src/modules/auth/pages/PrivacyPolicy.vue
+++ b/fe/src/modules/auth/pages/PrivacyPolicy.vue
@@ -1,8 +1,6 @@
@@ -113,9 +160,10 @@ onMounted(async () => {
-
-
-
+
+
+
+
-
-
+
+
{{ authStore.userName || '-' }}
+
+ {{ authStore.userMemberType }}
+
+
+ {{ statusLabel }}
+
+ No. {{ authStore.userMemberNumber }}
+
+
-
-
Maklumat Hubungan
-
+
+
+
+
Maklumat Hubungan
+
-
+
{{ displayValue(authStore.user?.email) }}
-
+
{{ displayValue(authStore.user?.phone_number) }}
-
+
{{ displayValue(authStore.user?.ic_number) }}
-
-
-
-
-
-
-
-
-
-
-
- Kad Digital
-
+
+
+
+ {{ displayValue(authStore.userPosition) }}
-
-
-
No. Anggota
-
- {{ displayValue(authStore.userMemberNumber) }}
-
+
Jawatan
+
+
+
+ {{ displayValue(companyName) }}
-
-
-
-
{{ authStore.userName || '-' }}
-
Nama
-
-
-
{{ displayValue(authStore.userMemberType) }}
-
Jenis Anggota
-
+
Syarikat
+
+
+
+ {{ formatDateLabel(authStore.userJoinDate) }}
+
Tarikh Sertai
+
+
+
+
+
-
-
- Profil
+
+
+
+ Profil
-
- Pekerjaan
+
+
+ Pekerjaan
-
- Bank
+
+
+ Bank
-
- Pewaris
+
+
+ Penama
-
- Kata Laluan
+
+
+ Kata Laluan
@@ -230,7 +294,7 @@ onMounted(async () => {
-
+
diff --git a/fe/src/modules/user/pages/UserHeirTab.vue b/fe/src/modules/user/pages/UserHeirTab.vue
index 37cdce6..d764516 100644
--- a/fe/src/modules/user/pages/UserHeirTab.vue
+++ b/fe/src/modules/user/pages/UserHeirTab.vue
@@ -13,16 +13,12 @@ defineProps<{
-
Pewaris
-
Senarai pewaris pengguna.
+
Penama
+
Senarai penama pengguna.
-
+
{{ heir.name }}
Utama
@@ -34,11 +30,8 @@ defineProps<{
-
- Tiada pewaris direkodkan.
+
+ Tiada penama direkodkan.
diff --git a/fe/src/modules/user/pages/UserProfileView.vue b/fe/src/modules/user/pages/UserProfileView.vue
index fcf3b61..5532a6c 100644
--- a/fe/src/modules/user/pages/UserProfileView.vue
+++ b/fe/src/modules/user/pages/UserProfileView.vue
@@ -146,7 +146,7 @@ onMounted(() => {
Bank
- Pewaris
+ Penama
diff --git a/fe/src/themes/Layout.vue b/fe/src/themes/Layout.vue
index ff5156f..b931079 100644
--- a/fe/src/themes/Layout.vue
+++ b/fe/src/themes/Layout.vue
@@ -81,8 +81,6 @@ onMounted(() => {
-
-
-import fakers from '@/utils/faker'
-import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
-import { Badge } from '@/components/ui/badge'
-import { Box } from '@/components/ui/box'
-import { Button } from '@/components/ui/button'
-import { Input } from '@/components/ui/input'
-import { Lucide } from '@/components/ui/lucide'
-import { MenuRoot, MenuTrigger, MenuPositioner, MenuContent, MenuItem } from '@/components/ui/menu'
-import { TabsRoot, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
-import { Textarea } from '@/components/ui/textarea'
-
-
-
-
-
-
Chat
-
- Start New Chat
-
-
-
-
-
-
-
-
-
-
-
-
-
- Create Group
-
-
- Settings
-
-
-
-
-
-
-
-
-
-
-
-
- Chats
- Friends
- Profile
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Invite Friends
-
-
-
-
-
-
-
-
- {{
- fakers[0]!['users'][0]!['name'].substring(0, 2)
- }}
-
-
-
-
- {{ fakers[0]!['users'][0]!['name'] }}
-
-
Tailwind Admin Dashboard Template
-
-
-
-
-
-
Country
-
New York City, USA
-
-
-
-
-
-
Phone
-
+32 19 23 62 24 34
-
-
-
-
-
-
Email
-
- {{ fakers[0]!['users'][0]!['email'] }}
-
-
-
-
-
-
-
Joined Date
-
{{ fakers[0]!['dates'][0] }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{
- fakers[0]!['users'][0]!['name'].substring(0, 2)
- }}
-
-
-
-
- {{ fakers[0]!['users'][0]!['name'] }}
-
-
- Hey, I am using chat •
- Online
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Share Contact
-
-
- Settings
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Recent Emojis
-
-
-
- Smileys & People
-
-
-
- Animals & Nature
-
-
-
- Food & Drink
-
-
-
- Activities
-
-
-
- Travel & Places
-
-
-
- Objects
-
-
-
- Symbols
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{
- fakers[0]!['users'][0]!['name'].substring(0, 2)
- }}
-
-
-
-
- Hey, {{ fakers[0]!['users'][0]!['name'] }}!
-
-
Please select a chat to start messaging.
-
-
-
-
-
-
-
-
-
-
-
diff --git a/fe/src/views/DashboardOverview1.vue b/fe/src/views/DashboardOverview1.vue
index d04c855..92dd774 100644
--- a/fe/src/views/DashboardOverview1.vue
+++ b/fe/src/views/DashboardOverview1.vue
@@ -351,7 +351,7 @@ const salesReportDate = ref([
-
@@ -378,7 +378,7 @@ const salesReportDate = ref
([
-
@@ -445,16 +445,14 @@ const salesReportDate = ref
([
+ class="w-full bg-primary/5 mb-7 border-primary/10 shadow-lg shadow-black/10 relative rounded-3xl border h-52 overflow-hidden before:bg-noise before:absolute before:inset-0 before:opacity-30 after:bg-accent after:absolute after:inset-0 after:opacity-30 after:blur-2xl">
+ alt="MyKOPKB" />
-
Welcome to Midone Admin!
+
Selamat Datang ke Sistem MyKOPKB
- Premium admin dashboard template for all kinds
- of projects. With a unique and modern design, Midone offers the perfect
- foundation to build professional applications with ease.
+ Sistem MyKOPKB adalah sistem yang membantu pengguna untuk menguruskan permohonan keahlian KOPKB.
@@ -472,9 +470,9 @@ const salesReportDate = ref
([
+ class="w-full bg-primary/5 mb-7 border-primary/10 shadow-lg shadow-black/10 relative rounded-3xl border h-52 overflow-hidden before:bg-noise before:absolute before:inset-0 before:opacity-30 after:bg-accent after:absolute after:inset-0 after:opacity-30 after:blur-2xl">
+ alt="MyKOPKB" />
Example Request Information
diff --git a/fe/src/views/Inbox.vue b/fe/src/views/Inbox.vue
deleted file mode 100644
index 3fb0b1a..0000000
--- a/fe/src/views/Inbox.vue
+++ /dev/null
@@ -1,266 +0,0 @@
-
-
-
-
-
-
Inbox
-
-
-
- Compose
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Start a Video Call
-
-
-
-
-
-
-
-
-
-
-
-
-
- Contacts
-
-
- Settings
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- All
- None
- Read
- Unread
- Starred
- Unstarred
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ faker['users'][0]!['name'].substring(0, 2) }}
-
-
-
- {{ faker['users'][0]!['name'] }}
-
-
-
-
- {{ faker['news'][0]!['superShortContent'] }}
-
- {{ faker['news'][0]!['shortContent'] }}
-
-
- {{ faker['times'][0] }}
-
-
-
-
-
-
4.41 GB (25%) of 17 GB used Manage
-
Last account activity: 36 minutes ago
-
-
-
-
-
-
diff --git a/fe/src/views/Post.vue b/fe/src/views/Post.vue
deleted file mode 100644
index 2f6f3e9..0000000
--- a/fe/src/views/Post.vue
+++ /dev/null
@@ -1,481 +0,0 @@
-
-
-
-
-
Add New Post
-
-
-
-
-
- English
-
-
-
-
-
-
-
-
- English
-
-
-
- Indonesian
-
-
-
-
-
-
- Preview
-
-
-
-
-
-
- Save
-
-
-
-
-
-
- As New Post
-
-
- As Draft
-
-
- Export to PDF
-
-
- Export to Word
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Content
-
-
- Meta Title
-
-
- Keywords
-
-
-
-
-
- Text Content
-
-
-
-
-
-
-
- Caption & Images
-
-
-
- Caption
-
-
-
- Upload Image
-
-
-
-
-
-
-
-
-
-
-
- Remove this image?
-
-
-
-
-
-
- Upload a file
- or drag and drop
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Written By
-
-
-
-
- {{
- fakers[0]!['users'][0]!['name'].substring(0, 2)
- }}
-
-
- {{ fakers[0]!['users'][0]!['name'] }}
-
-
-
-
-
-
-
-
-
- {{ faker['users'][0]!['name'] }}
-
-
-
-
-
-
- Post Date
-
-
-
-
- Clear
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ weekDay.short }}
-
-
-
-
-
-
-
- {{ day.day }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{
- month.label
- }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{
- year.label
- }}
-
-
-
-
-
-
-
-
-
-
-
-
- Categories
- (categoryValue = details.value)"
- @input-value-change="
- ({ inputValue }) => {
- const filtered = categoryData.filter((item) =>
- item.label.toLowerCase().includes(inputValue.toLowerCase()),
- )
- categoryOptions = filtered.length > 0 ? filtered : categoryData
- }
- "
- class="w-full"
- multiple
- >
-
-
-
-
-
-
- Categories
-
- {{ item.label }}
-
-
-
-
-
-
- Tags
- (tagValue = details.value)"
- @input-value-change="
- ({ inputValue }) => {
- const filtered = tagData.filter((item) =>
- item.label.toLowerCase().includes(inputValue.toLowerCase()),
- )
- tagOptions = filtered.length > 0 ? filtered : tagData
- }
- "
- class="w-full"
- multiple
- >
-
-
-
-
-
-
- Tags
-
- {{ item.label }}
-
-
-
-
-
-
- Published
-
-
-
-
-
- Show Author Name
-
-
-
-
-
-
-
-
-
diff --git a/fe/src/views/UsersLayout1.vue b/fe/src/views/UsersLayout1.vue
deleted file mode 100644
index 019d4da..0000000
--- a/fe/src/views/UsersLayout1.vue
+++ /dev/null
@@ -1,153 +0,0 @@
-
-
-
-
-
Users Layout
-
-
-
Add New User
-
-
-
-
-
-
-
-
-
-
-
-
-
- Add Group
-
-
- Send Message
-
-
-
-
-
Showing 1 to 10 of 150 entries
-
-
-
-
-
-
-
- IM
-
-
-
-
-
-
-
-
-
-
-
- Facebook
-
-
-
-
-
-
-
-
-
- Twitter
-
-
-
-
-
-
-
-
-
- Linked In
-
-
-
-
-
-
-
Message
-
View Profile
-
-
-
-
-
-
-
- Previous
-
-
-
- {{ page.value }}
-
-
-
-
- Next
-
-
- 10
- 25
- 35
- 50
-
-
-
-
-
-
diff --git a/fe/src/views/UsersLayout2.vue b/fe/src/views/UsersLayout2.vue
deleted file mode 100644
index 442552e..0000000
--- a/fe/src/views/UsersLayout2.vue
+++ /dev/null
@@ -1,103 +0,0 @@
-
-
-
-
-
Users Layout
-
-
-
Add New User
-
-
-
-
-
-
-
-
-
-
-
-
-
- Add Group
-
-
- Send Message
-
-
-
-
-
Showing 1 to 10 of 150 entries
-
-
-
-
-
-
-
- IM
-
-
-
-
- View Profile
- Message
-
-
-
-
-
-
-
-
- Previous
-
-
-
- {{ page.value }}
-
-
-
-
- Next
-
-
- 10
- 25
- 35
- 50
-
-
-
-
-
-
diff --git a/fe/src/views/UsersLayout3.vue b/fe/src/views/UsersLayout3.vue
deleted file mode 100644
index 1393694..0000000
--- a/fe/src/views/UsersLayout3.vue
+++ /dev/null
@@ -1,133 +0,0 @@
-
-
-
-
-
Users Layout
-
-
-
Add New User
-
-
-
-
-
-
-
-
-
-
-
-
-
- Add Group
-
-
- Send Message
-
-
-
-
-
Showing 1 to 10 of 150 entries
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Edit
-
-
- Delete
-
-
-
-
-
-
-
{{ faker['news'][0]!['shortContent'] }}
-
-
- {{ faker['users'][0]!['email'] }}
-
-
-
- {{ faker['users'][0]!['name'] }}
-
-
-
- Message
- View Profile
-
-
-
-
-
-
-
- Previous
-
-
-
- {{ page.value }}
-
-
-
-
- Next
-
-
- 10
- 25
- 35
- 50
-
-
-
-
-
-
diff --git a/fe/src/views/WizardLayout1.vue b/fe/src/views/WizardLayout1.vue
deleted file mode 100644
index e0ca24a..0000000
--- a/fe/src/views/WizardLayout1.vue
+++ /dev/null
@@ -1,70 +0,0 @@
-
-
-
-
-
Wizard Layout
-
-
-
-
- 1
-
- 2
-
-
- 3
-
-
-
-
Setup Your Account
-
- To start off, please enter your username, email address and password.
-
-
-
-
-
-
diff --git a/fe/src/views/WizardLayout2.vue b/fe/src/views/WizardLayout2.vue
deleted file mode 100644
index 09e9646..0000000
--- a/fe/src/views/WizardLayout2.vue
+++ /dev/null
@@ -1,85 +0,0 @@
-
-
-
-
-
Wizard Layout
-
-
-
-
-
-
1
-
Create New Account
-
-
-
- 2
-
-
Setup Your Profile
-
-
-
- 3
-
-
Setup Your Business Details
-
-
-
- 4
-
-
Setup Billing Account
-
-
-
- 5
-
-
Finalize your purchase
-
-
-
-
-
-
diff --git a/fe/src/views/WizardLayout3.vue b/fe/src/views/WizardLayout3.vue
deleted file mode 100644
index 61180b8..0000000
--- a/fe/src/views/WizardLayout3.vue
+++ /dev/null
@@ -1,88 +0,0 @@
-
-
-
-
-
Wizard Layout
-
-
-
-
-
-
1
-
- Create New Account
-
-
-
-
- 2
-
-
Setup Your Profile
-
-
-
- 3
-
-
Setup Your Business Details
-
-
-
- 4
-
-
Setup Billing Account
-
-
-
- 5
-
-
Finalize your purchase
-
-
-
-
-
-
--
2.52.0
From ac8f00175a8e57099024aa3fd729a0b22fee805e Mon Sep 17 00:00:00 2001
From: ISMAIL MASSERAN
Date: Mon, 6 Jul 2026 09:43:58 +0800
Subject: [PATCH 2/4] DONE: digital card with profile display: WIP: membership
condition on letter, is_first_time column in users table
---
be/.env.development | 5 +-
be/.env.production | 5 +-
be/Modules/Auth/Entities/User.php | 50 ++++
.../Http/Controllers/SessionController.php | 1 +
.../Auth/Services/AuthSessionService.php | 2 +
be/Modules/Auth/Transformers/UserResource.php | 1 +
be/Modules/User/Config/config.php | 3 +
...dd_public_profile_token_to_users_table.php | 34 +++
...rofile_token_expires_at_to_users_table.php | 35 +++
.../MemberDigitalCardController.php | 61 +++++
.../PublicMemberProfileController.php | 37 +++
.../User/Providers/UserServiceProvider.php | 10 +
.../Views/digital-card/back.blade.php | 166 ++++++++++++++
.../Views/digital-card/front.blade.php | 214 ++++++++++++++++++
be/Modules/User/Routes/api.php | 10 +
.../Services/MemberDigitalCardService.php | 141 ++++++++++++
.../Services/PublicMemberProfileService.php | 25 ++
.../PublicMemberProfileResource.php | 26 +++
fe/package.json | 2 +
fe/pnpm-lock.yaml | 164 ++++++++++++++
fe/src/modules/auth/types/auth.types.ts | 1 +
.../profile/components/MemberDigitalCard.vue | 186 +++++++++++++++
.../components/MemberDigitalCardBack.vue | 104 +++++++++
.../components/MemberDigitalCardFlip.vue | 34 +++
.../components/MemberDigitalCardFront.vue | 97 ++++++++
fe/src/modules/profile/index.ts | 2 +-
.../profile/pages/PublicMemberProfile.vue | 140 ++++++++++++
fe/src/modules/profile/routes.ts | 9 +
.../services/member-digital-card.service.ts | 35 +++
.../services/public-member-profile.service.ts | 12 +
.../types/public-member-profile.types.ts | 16 ++
.../utils/member-digital-card.utils.ts | 27 +++
fe/src/router/index.ts | 98 +-------
fe/vite.config.ts | 8 +
34 files changed, 1662 insertions(+), 99 deletions(-)
create mode 100644 be/Modules/User/Database/Migrations/2026_07_05_100000_add_public_profile_token_to_users_table.php
create mode 100644 be/Modules/User/Database/Migrations/2026_07_05_100001_add_public_profile_token_expires_at_to_users_table.php
create mode 100644 be/Modules/User/Http/Controllers/MemberDigitalCardController.php
create mode 100644 be/Modules/User/Http/Controllers/PublicMemberProfileController.php
create mode 100644 be/Modules/User/Resources/Views/digital-card/back.blade.php
create mode 100644 be/Modules/User/Resources/Views/digital-card/front.blade.php
create mode 100644 be/Modules/User/Services/MemberDigitalCardService.php
create mode 100644 be/Modules/User/Services/PublicMemberProfileService.php
create mode 100644 be/Modules/User/Transformers/PublicMemberProfileResource.php
create mode 100644 fe/src/modules/profile/components/MemberDigitalCard.vue
create mode 100644 fe/src/modules/profile/components/MemberDigitalCardBack.vue
create mode 100644 fe/src/modules/profile/components/MemberDigitalCardFlip.vue
create mode 100644 fe/src/modules/profile/components/MemberDigitalCardFront.vue
create mode 100644 fe/src/modules/profile/pages/PublicMemberProfile.vue
create mode 100644 fe/src/modules/profile/services/member-digital-card.service.ts
create mode 100644 fe/src/modules/profile/services/public-member-profile.service.ts
create mode 100644 fe/src/modules/profile/types/public-member-profile.types.ts
create mode 100644 fe/src/modules/profile/utils/member-digital-card.utils.ts
diff --git a/be/.env.development b/be/.env.development
index b5c5094..74b36c6 100644
--- a/be/.env.development
+++ b/be/.env.development
@@ -95,4 +95,7 @@ API_ALLOWED_USER_AGENTS=
ENABLE_API_KEY_AUTH=true
API_VALID_KEYS=86f58825e5c1e0872d8786092d47e3bd,6461fc5390660c55dc47bdfcd85ed9ae
-API_LOG_KEY_USAGE=true
\ No newline at end of file
+API_LOG_KEY_USAGE=true
+
+PUBLIC_PROFILE_TOKEN_TTL_DAYS=7
+FRONTEND_URL=https://mykopkb.koppkb.com
\ No newline at end of file
diff --git a/be/.env.production b/be/.env.production
index 8cfae54..80a9c56 100644
--- a/be/.env.production
+++ b/be/.env.production
@@ -102,4 +102,7 @@ API_ALLOWED_USER_AGENTS=
ENABLE_API_KEY_AUTH=true
API_VALID_KEYS=86f58825e5c1e0872d8786092d47e3bd,6461fc5390660c55dc47bdfcd85ed9ae
-API_LOG_KEY_USAGE=true
\ No newline at end of file
+API_LOG_KEY_USAGE=true
+
+PUBLIC_PROFILE_TOKEN_TTL_DAYS=7
+FRONTEND_URL=https://mykopkb.koppkb.com
\ No newline at end of file
diff --git a/be/Modules/Auth/Entities/User.php b/be/Modules/Auth/Entities/User.php
index 3c094b5..ac1422e 100644
--- a/be/Modules/Auth/Entities/User.php
+++ b/be/Modules/Auth/Entities/User.php
@@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
+use Illuminate\Support\Str;
use Laravel\Sanctum\HasApiTokens;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity;
@@ -51,6 +52,7 @@ class User extends Authenticatable
'marriage_status',
'member_number',
'member_type',
+ 'public_profile_token',
'join_date',
'birth_date',
'birth_place',
@@ -67,8 +69,55 @@ class User extends Authenticatable
'two_factor_secret',
'two_factor_recovery_codes',
'two_factor_confirmed_at',
+ 'public_profile_token',
];
+ protected static function booted(): void
+ {
+ static::creating(function (User $user) {
+ $expiryDays = self::publicProfileTokenTtlDays();
+
+ if (empty($user->public_profile_token)) {
+ $user->public_profile_token = Str::random(48);
+ }
+
+ if (empty($user->public_profile_token_expires_at)) {
+ $user->public_profile_token_expires_at = now()->addDays($expiryDays);
+ }
+ });
+ }
+
+ public static function publicProfileTokenTtlDays(): int
+ {
+ return (int) config('user.public_profile_token_ttl_days', 90);
+ }
+
+ public function isPublicProfileTokenExpired(): bool
+ {
+ return $this->public_profile_token_expires_at !== null
+ && $this->public_profile_token_expires_at->isPast();
+ }
+
+ public function ensurePublicProfileToken(): string
+ {
+ $expiryDays = self::publicProfileTokenTtlDays();
+
+ if (blank($this->public_profile_token) || $this->isPublicProfileTokenExpired()) {
+ $this->forceFill([
+ 'public_profile_token' => Str::random(48),
+ 'public_profile_token_expires_at' => now()->addDays($expiryDays),
+ ])->save();
+
+ return $this->public_profile_token;
+ }
+
+ $this->forceFill([
+ 'public_profile_token_expires_at' => now()->addDays($expiryDays),
+ ])->save();
+
+ return $this->public_profile_token;
+ }
+
/**
* Get the attributes that should be cast.
*
@@ -84,6 +133,7 @@ class User extends Authenticatable
'marriage_status' => 'string',
'member_number' => 'integer',
'member_type' => 'string',
+ 'public_profile_token_expires_at' => 'datetime',
'join_date' => 'date',
'birth_date' => 'date',
'birth_place' => 'string',
diff --git a/be/Modules/Auth/Http/Controllers/SessionController.php b/be/Modules/Auth/Http/Controllers/SessionController.php
index 34a0652..cfcde33 100644
--- a/be/Modules/Auth/Http/Controllers/SessionController.php
+++ b/be/Modules/Auth/Http/Controllers/SessionController.php
@@ -24,6 +24,7 @@ class SessionController extends Controller
], 401);
}
+ $user->ensurePublicProfileToken();
$user->load(['roles.permissions']);
return response()->json([
diff --git a/be/Modules/Auth/Services/AuthSessionService.php b/be/Modules/Auth/Services/AuthSessionService.php
index bca4178..8cd803c 100644
--- a/be/Modules/Auth/Services/AuthSessionService.php
+++ b/be/Modules/Auth/Services/AuthSessionService.php
@@ -12,6 +12,8 @@ class AuthSessionService
{
public function createAuthResponse(User $user, string $message, int $status = 200): JsonResponse
{
+ $user->ensurePublicProfileToken();
+
if (config('app.env') !== 'local') {
$user->tokens()->delete();
}
diff --git a/be/Modules/Auth/Transformers/UserResource.php b/be/Modules/Auth/Transformers/UserResource.php
index 98518ef..bc89bde 100644
--- a/be/Modules/Auth/Transformers/UserResource.php
+++ b/be/Modules/Auth/Transformers/UserResource.php
@@ -36,6 +36,7 @@ class UserResource extends JsonResource
'marriage_status' => $this->marriage_status,
'member_number' => $this->member_number,
'member_type' => $this->member_type,
+ 'public_profile_token' => $this->public_profile_token,
'join_date' => $this->join_date,
'birth_date' => $this->birth_date,
'birth_place' => $this->birth_place,
diff --git a/be/Modules/User/Config/config.php b/be/Modules/User/Config/config.php
index 566de5a..57bdafc 100644
--- a/be/Modules/User/Config/config.php
+++ b/be/Modules/User/Config/config.php
@@ -2,4 +2,7 @@
return [
'name' => 'User',
+ 'public_profile_token_ttl_days' => (int) env('PUBLIC_PROFILE_TOKEN_TTL_DAYS', 90),
+ 'frontend_url' => rtrim(env('FRONTEND_URL', env('APP_URL', 'http://localhost')), '/'),
+ 'digital_card_logo_path' => env('DIGITAL_CARD_LOGO_PATH', public_path('images/logo/logo-kopkb.svg')),
];
diff --git a/be/Modules/User/Database/Migrations/2026_07_05_100000_add_public_profile_token_to_users_table.php b/be/Modules/User/Database/Migrations/2026_07_05_100000_add_public_profile_token_to_users_table.php
new file mode 100644
index 0000000..51eee6d
--- /dev/null
+++ b/be/Modules/User/Database/Migrations/2026_07_05_100000_add_public_profile_token_to_users_table.php
@@ -0,0 +1,34 @@
+string('public_profile_token', 64)->nullable()->unique()->after('member_type');
+ });
+
+ User::withTrashed()
+ ->whereNull('public_profile_token')
+ ->cursor()
+ ->each(function (User $user) {
+ $user->forceFill([
+ 'public_profile_token' => Str::random(48),
+ ])->saveQuietly();
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('users', function (Blueprint $table) {
+ $table->dropUnique(['public_profile_token']);
+ $table->dropColumn('public_profile_token');
+ });
+ }
+};
diff --git a/be/Modules/User/Database/Migrations/2026_07_05_100001_add_public_profile_token_expires_at_to_users_table.php b/be/Modules/User/Database/Migrations/2026_07_05_100001_add_public_profile_token_expires_at_to_users_table.php
new file mode 100644
index 0000000..cd5ccc4
--- /dev/null
+++ b/be/Modules/User/Database/Migrations/2026_07_05_100001_add_public_profile_token_expires_at_to_users_table.php
@@ -0,0 +1,35 @@
+timestamp('public_profile_token_expires_at')->nullable()->after('public_profile_token');
+ });
+
+ $expiryDays = (int) config('user.public_profile_token_ttl_days', 90);
+
+ User::withTrashed()
+ ->whereNotNull('public_profile_token')
+ ->whereNull('public_profile_token_expires_at')
+ ->cursor()
+ ->each(function (User $user) use ($expiryDays) {
+ $user->forceFill([
+ 'public_profile_token_expires_at' => now()->addDays($expiryDays),
+ ])->saveQuietly();
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('users', function (Blueprint $table) {
+ $table->dropColumn('public_profile_token_expires_at');
+ });
+ }
+};
diff --git a/be/Modules/User/Http/Controllers/MemberDigitalCardController.php b/be/Modules/User/Http/Controllers/MemberDigitalCardController.php
new file mode 100644
index 0000000..c0dcabc
--- /dev/null
+++ b/be/Modules/User/Http/Controllers/MemberDigitalCardController.php
@@ -0,0 +1,61 @@
+validate([
+ 'side' => 'sometimes|in:depan,belakang',
+ ]);
+
+ $user = $request->user();
+ $side = $validated['side'] ?? 'depan';
+
+ if ($user->status !== 'active') {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'Kad digital hanya tersedia untuk anggota aktif.',
+ 'code' => 'digital_card_inactive',
+ 'data' => null,
+ ], 403);
+ }
+
+ try {
+ $png = $this->memberDigitalCardService->renderPng($user, $side);
+ $memberNumber = $user->member_number ?: 'anggota';
+ $filename = sprintf('kad-digital-%s-%s.png', $memberNumber, $side);
+
+ return response($png, Response::HTTP_OK, [
+ 'Content-Type' => 'image/png',
+ 'Content-Disposition' => 'attachment; filename="'.$filename.'"',
+ 'Cache-Control' => 'no-store, private',
+ ]);
+ } catch (\Throwable $exception) {
+ Log::error('Failed to render member digital card.', [
+ 'user_id' => $user->id,
+ 'side' => $side,
+ 'message' => $exception->getMessage(),
+ ]);
+
+ return response()->json([
+ 'success' => false,
+ 'message' => 'Gagal menjana kad digital. Sila cuba lagi.',
+ 'code' => 'digital_card_render_failed',
+ 'data' => null,
+ ], 500);
+ }
+ }
+}
diff --git a/be/Modules/User/Http/Controllers/PublicMemberProfileController.php b/be/Modules/User/Http/Controllers/PublicMemberProfileController.php
new file mode 100644
index 0000000..c25357e
--- /dev/null
+++ b/be/Modules/User/Http/Controllers/PublicMemberProfileController.php
@@ -0,0 +1,37 @@
+publicMemberProfileService->findMemberByToken($token);
+
+ if (! $member) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'Anggota tidak dijumpai atau tidak sah.',
+ 'code' => 'public_profile_not_found',
+ 'data' => null,
+ ], 404);
+ }
+
+ $member = $this->publicMemberProfileService->loadVerifiableRelations($member);
+
+ return response()->json([
+ 'success' => true,
+ 'message' => 'Anggota disahkan.',
+ 'data' => new PublicMemberProfileResource($member),
+ ]);
+ }
+}
diff --git a/be/Modules/User/Providers/UserServiceProvider.php b/be/Modules/User/Providers/UserServiceProvider.php
index 9c28f9e..e9f7dba 100644
--- a/be/Modules/User/Providers/UserServiceProvider.php
+++ b/be/Modules/User/Providers/UserServiceProvider.php
@@ -5,6 +5,8 @@ namespace Modules\User\Providers;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;
+use Illuminate\Support\Facades\RateLimiter;
+use Illuminate\Cache\RateLimiting\Limit;
use Modules\Auth\Entities\User;
use Modules\User\Policies\UserPolicy;
use Modules\User\Entities\Address;
@@ -34,6 +36,7 @@ class UserServiceProvider extends ServiceProvider
*/
public function boot(): void
{
+ $this->registerRateLimiters();
$this->registerCommands();
$this->registerCommandSchedules();
$this->registerTranslations();
@@ -96,6 +99,13 @@ class UserServiceProvider extends ServiceProvider
Gate::policy(Heir::class, HeirPolicy::class);
}
+ protected function registerRateLimiters(): void
+ {
+ RateLimiter::for('public-member-profile', function ($request) {
+ return Limit::perMinute(60)->by($request->ip());
+ });
+ }
+
/**
* Register commands in the format of Command::class
*/
diff --git a/be/Modules/User/Resources/Views/digital-card/back.blade.php b/be/Modules/User/Resources/Views/digital-card/back.blade.php
new file mode 100644
index 0000000..fd0f376
--- /dev/null
+++ b/be/Modules/User/Resources/Views/digital-card/back.blade.php
@@ -0,0 +1,166 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Imbas untuk sahkan profil anggota MyKOPKB.
+
+
+
No. Anggota
+
{{ $memberNumber }}
+
+
+
+
+
+
+
+
+
diff --git a/be/Modules/User/Resources/Views/digital-card/front.blade.php b/be/Modules/User/Resources/Views/digital-card/front.blade.php
new file mode 100644
index 0000000..9352c56
--- /dev/null
+++ b/be/Modules/User/Resources/Views/digital-card/front.blade.php
@@ -0,0 +1,214 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ @if ($avatarDataUri)
+
+ @else
+
{{ $avatarInitials }}
+ @endif
+
+
+
+
+
+
+
+
No. Anggota
+
{{ $memberNumber }}
+
+
+
+
Unit
+
{{ $companyName }}
+
+
+
+
+
+
+
+
diff --git a/be/Modules/User/Routes/api.php b/be/Modules/User/Routes/api.php
index 28c0793..36d1c39 100644
--- a/be/Modules/User/Routes/api.php
+++ b/be/Modules/User/Routes/api.php
@@ -8,6 +8,14 @@ use Modules\User\Http\Controllers\EmploymentController;
use Modules\User\Http\Controllers\BankController;
use Modules\User\Http\Controllers\BankDetailController;
use Modules\User\Http\Controllers\HeirController;
+use Modules\User\Http\Controllers\MemberDigitalCardController;
+use Modules\User\Http\Controllers\PublicMemberProfileController;
+
+Route::prefix('v1/public')->group(function () {
+ Route::middleware('throttle:public-member-profile')
+ ->get('members/{token}', [PublicMemberProfileController::class, 'show'])
+ ->name('user.public.member-profile');
+});
Route::middleware(['auth:sanctum', 'single.session'])->prefix('v1')->group(function () {
Route::get('users/deleted', [UserController::class, 'deletedIndex'])->name('users.deleted.index');
@@ -28,6 +36,8 @@ Route::middleware(['auth:sanctum', 'single.session'])->prefix('v1')->group(funct
// User profile management routes
Route::post('/profile', [UserController::class, 'updateProfile']);
Route::put('/profile/password', [UserController::class, 'updatePassword']);
+ Route::get('/profile/digital-card', [MemberDigitalCardController::class, 'download'])
+ ->name('profile.digital-card.download');
// Impersonation routes
Route::get('/impersonate/take/{id}', [ImpersonateController::class, 'take'])->name('impersonate');
diff --git a/be/Modules/User/Services/MemberDigitalCardService.php b/be/Modules/User/Services/MemberDigitalCardService.php
new file mode 100644
index 0000000..de68afd
--- /dev/null
+++ b/be/Modules/User/Services/MemberDigitalCardService.php
@@ -0,0 +1,141 @@
+prepareViewData($user))->render();
+
+ return $this->makeBrowsershot($html)->screenshot();
+ }
+
+ /**
+ * @return array
+ */
+ protected function prepareViewData(User $user): array
+ {
+ $user = app(PublicMemberProfileService::class)->loadVerifiableRelations($user);
+ $user->ensurePublicProfileToken();
+
+ $currentEmployment = $user->employments->firstWhere('is_current', true)
+ ?? $user->employments->first();
+
+ $logoPath = config('user.digital_card_logo_path');
+ $profileUrl = rtrim(config('user.frontend_url'), '/').'/v/'.$user->public_profile_token;
+
+ return [
+ 'memberNumber' => $this->displayCardValue($user->member_number),
+ 'memberName' => $user->name ?: '-',
+ 'memberType' => $this->displayCardValue($user->member_type),
+ 'companyName' => $this->displayCardValue($currentEmployment?->company_name),
+ 'avatarDataUri' => $this->resolveStorageDataUri($user->image_url),
+ 'avatarInitials' => $this->avatarInitials($user->name),
+ 'logoDataUri' => $this->resolveFileDataUri($logoPath),
+ 'profileUrl' => $profileUrl,
+ 'qrDataUri' => $this->generateQrDataUri($profileUrl),
+ ];
+ }
+
+ protected function makeBrowsershot(string $html): Browsershot
+ {
+ $browsershot = Browsershot::html($html)
+ ->setNodeModulePath(config('browsershot.node_module_path'))
+ ->timeout(config('browsershot.timeout'))
+ ->windowSize(self::CARD_WIDTH, self::CARD_HEIGHT)
+ ->deviceScaleFactor(2)
+ ->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 generateQrDataUri(string $content): string
+ {
+ $renderer = new ImageRenderer(
+ new RendererStyle(220, 1),
+ new SvgImageBackEnd
+ );
+ $writer = new Writer($renderer);
+ $svg = $writer->writeString($content);
+
+ return 'data:image/svg+xml;base64,'.base64_encode($svg);
+ }
+
+ protected function resolveFileDataUri(?string $path): ?string
+ {
+ if (empty($path) || ! file_exists($path)) {
+ return null;
+ }
+
+ $mime = mime_content_type($path) ?: 'image/svg+xml';
+
+ return 'data:'.$mime.';base64,'.base64_encode(file_get_contents($path));
+ }
+
+ protected function resolveStorageDataUri(?string $path): ?string
+ {
+ if (blank($path)) {
+ return null;
+ }
+
+ $fullPath = Storage::disk('public')->path($path);
+
+ return $this->resolveFileDataUri($fullPath);
+ }
+
+ protected function avatarInitials(?string $name): string
+ {
+ $trimmed = trim((string) $name);
+
+ if ($trimmed === '') {
+ return '--';
+ }
+
+ return strtoupper(mb_substr($trimmed, 0, 2));
+ }
+
+ protected function displayCardValue(mixed $value): string
+ {
+ if ($value === null || $value === '') {
+ return '-';
+ }
+
+ $trimmed = trim((string) $value);
+
+ return $trimmed !== '' ? $trimmed : '-';
+ }
+}
diff --git a/be/Modules/User/Services/PublicMemberProfileService.php b/be/Modules/User/Services/PublicMemberProfileService.php
new file mode 100644
index 0000000..2a0c468
--- /dev/null
+++ b/be/Modules/User/Services/PublicMemberProfileService.php
@@ -0,0 +1,25 @@
+where('public_profile_token', $token)
+ ->where('status', 'active')
+ ->first();
+ }
+
+ public function loadVerifiableRelations(User $member): User
+ {
+ $member->load(['employments' => function ($query) {
+ $query->orderByDesc('is_current')->orderByDesc('start_date');
+ }]);
+
+ return $member;
+ }
+}
diff --git a/be/Modules/User/Transformers/PublicMemberProfileResource.php b/be/Modules/User/Transformers/PublicMemberProfileResource.php
new file mode 100644
index 0000000..283f9d3
--- /dev/null
+++ b/be/Modules/User/Transformers/PublicMemberProfileResource.php
@@ -0,0 +1,26 @@
+employments->firstWhere('is_current', true)
+ ?? $this->employments->first();
+
+ return [
+ 'name' => $this->name,
+ 'member_number' => $this->member_number,
+ 'member_type' => $this->member_type,
+ 'status' => $this->status,
+ 'image_url' => $this->image_url ? Storage::disk('public')->url($this->image_url) : null,
+ 'company_name' => $currentEmployment?->company_name,
+ 'verified_at' => now()->toIso8601String(),
+ ];
+ }
+}
diff --git a/fe/package.json b/fe/package.json
index 6d389fe..b54979a 100644
--- a/fe/package.json
+++ b/fe/package.json
@@ -57,6 +57,7 @@
"lodash": "^4.17.21",
"maplibre-gl": "^5.18.0",
"pinia": "^3.0.4",
+ "qrcode": "^1.5.4",
"sweetalert2": "^11.26.25",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.1.18",
@@ -71,6 +72,7 @@
"@types/jsdom": "^27.0.0",
"@types/lodash": "^4.17.21",
"@types/node": "^24.10.4",
+ "@types/qrcode": "^1.5.6",
"@vitejs/plugin-vue": "^6.0.3",
"@vitest/eslint-plugin": "^1.6.4",
"@vue/eslint-config-prettier": "^10.2.0",
diff --git a/fe/pnpm-lock.yaml b/fe/pnpm-lock.yaml
index ef1bc00..c4f6589 100644
--- a/fe/pnpm-lock.yaml
+++ b/fe/pnpm-lock.yaml
@@ -119,6 +119,9 @@ importers:
pinia:
specifier: ^3.0.4
version: 3.0.4(typescript@5.9.3)(vue@3.5.35(typescript@5.9.3))
+ qrcode:
+ specifier: ^1.5.4
+ version: 1.5.4
sweetalert2:
specifier: ^11.26.25
version: 11.26.25
@@ -156,6 +159,9 @@ importers:
'@types/node':
specifier: ^24.10.4
version: 24.12.4
+ '@types/qrcode':
+ specifier: ^1.5.6
+ version: 1.5.6
'@vitejs/plugin-vue':
specifier: ^6.0.3
version: 6.0.7(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0))(vue@3.5.35(typescript@5.9.3))
@@ -1041,6 +1047,9 @@ packages:
'@types/pako@2.0.4':
resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==}
+ '@types/qrcode@1.5.6':
+ resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==}
+
'@types/raf@3.4.3':
resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==}
@@ -1588,6 +1597,10 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
+ camelcase@5.3.1:
+ resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
+ engines: {node: '>=6'}
+
caniuse-lite@1.0.30001793:
resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==}
@@ -1637,6 +1650,9 @@ packages:
resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==}
engines: {node: '>=20'}
+ cliui@6.0.0:
+ resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
+
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
@@ -1758,6 +1774,10 @@ packages:
supports-color:
optional: true
+ decamelize@1.2.0:
+ resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
+ engines: {node: '>=0.10.0'}
+
decimal.js@10.6.0:
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
@@ -1784,6 +1804,9 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
+ dijkstrajs@1.0.3:
+ resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
+
dompurify@3.4.7:
resolution: {integrity: sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA==}
@@ -2055,6 +2078,10 @@ packages:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
+ find-up@4.1.0:
+ resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
+ engines: {node: '>=8'}
+
find-up@5.0.0:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
@@ -2109,6 +2136,10 @@ packages:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
+ get-caller-file@2.0.5:
+ resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
+ engines: {node: 6.* || 8.* || >= 10.*}
+
get-east-asian-width@1.6.0:
resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==}
engines: {node: '>=18'}
@@ -2501,6 +2532,10 @@ packages:
resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==}
engines: {node: '>=20.0.0'}
+ locate-path@5.0.0:
+ resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
+ engines: {node: '>=8'}
+
locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
@@ -2679,14 +2714,26 @@ packages:
ospath@1.2.2:
resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==}
+ p-limit@2.3.0:
+ resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
+ engines: {node: '>=6'}
+
p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
+ p-locate@4.1.0:
+ resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
+ engines: {node: '>=8'}
+
p-locate@5.0.0:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
+ p-try@2.2.0:
+ resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
+ engines: {node: '>=6'}
+
package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
@@ -2769,6 +2816,10 @@ packages:
typescript:
optional: true
+ pngjs@5.0.0:
+ resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
+ engines: {node: '>=10.13.0'}
+
postcss-selector-parser@7.1.1:
resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==}
engines: {node: '>=4'}
@@ -2833,6 +2884,11 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
+ qrcode@1.5.4:
+ resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
+ engines: {node: '>=10.13.0'}
+ hasBin: true
+
qs@6.15.2:
resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==}
engines: {node: '>=0.6'}
@@ -2856,10 +2912,17 @@ packages:
request-progress@3.0.0:
resolution: {integrity: sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==}
+ require-directory@2.1.1:
+ resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
+ engines: {node: '>=0.10.0'}
+
require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
+ require-main-filename@2.0.0:
+ resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
+
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
@@ -2916,6 +2979,9 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ set-blocking@2.0.0:
+ resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
+
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@@ -3393,6 +3459,9 @@ packages:
resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==}
engines: {node: '>=20'}
+ which-module@2.0.1:
+ resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
+
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
@@ -3420,6 +3489,10 @@ packages:
resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==}
engines: {node: '>=0.8'}
+ wrap-ansi@6.2.0:
+ resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
+ engines: {node: '>=8'}
+
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
@@ -3467,9 +3540,20 @@ packages:
xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
+ y18n@4.0.3:
+ resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
+
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+ yargs-parser@18.1.3:
+ resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
+ engines: {node: '>=6'}
+
+ yargs@15.4.1:
+ resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
+ engines: {node: '>=8'}
+
yauzl@3.3.2:
resolution: {integrity: sha512-Md9ankxxN23wncAN8s7+Tn3Co52zLUPMtnrLAbVCnfG5d2tKBFfmygYSgXlqFgXObtzIgqkx7aNgDBpso9+4qA==}
engines: {node: '>=12'}
@@ -4215,6 +4299,10 @@ snapshots:
'@types/pako@2.0.4': {}
+ '@types/qrcode@1.5.6':
+ dependencies:
+ '@types/node': 24.12.4
+
'@types/raf@3.4.3':
optional: true
@@ -4978,6 +5066,8 @@ snapshots:
callsites@3.1.0: {}
+ camelcase@5.3.1: {}
+
caniuse-lite@1.0.30001793: {}
canvg@3.0.11:
@@ -5033,6 +5123,12 @@ snapshots:
slice-ansi: 8.0.0
string-width: 8.2.1
+ cliui@6.0.0:
+ dependencies:
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wrap-ansi: 6.2.0
+
clsx@2.1.1: {}
codepage@1.15.0: {}
@@ -5172,6 +5268,8 @@ snapshots:
optionalDependencies:
supports-color: 8.1.1
+ decamelize@1.2.0: {}
+
decimal.js@10.6.0: {}
deep-is@0.1.4: {}
@@ -5189,6 +5287,8 @@ snapshots:
detect-libc@2.1.2: {}
+ dijkstrajs@1.0.3: {}
+
dompurify@3.4.7:
optionalDependencies:
'@types/trusted-types': 2.0.7
@@ -5505,6 +5605,11 @@ snapshots:
dependencies:
to-regex-range: 5.0.1
+ find-up@4.1.0:
+ dependencies:
+ locate-path: 5.0.0
+ path-exists: 4.0.0
+
find-up@5.0.0:
dependencies:
locate-path: 6.0.0
@@ -5554,6 +5659,8 @@ snapshots:
gensync@1.0.0-beta.2: {}
+ get-caller-file@2.0.5: {}
+
get-east-asian-width@1.6.0: {}
get-intrinsic@1.3.0:
@@ -5928,6 +6035,10 @@ snapshots:
rfdc: 1.4.1
wrap-ansi: 9.0.2
+ locate-path@5.0.0:
+ dependencies:
+ p-locate: 4.1.0
+
locate-path@6.0.0:
dependencies:
p-locate: 5.0.0
@@ -6107,14 +6218,24 @@ snapshots:
ospath@1.2.2: {}
+ p-limit@2.3.0:
+ dependencies:
+ p-try: 2.2.0
+
p-limit@3.1.0:
dependencies:
yocto-queue: 0.1.0
+ p-locate@4.1.0:
+ dependencies:
+ p-limit: 2.3.0
+
p-locate@5.0.0:
dependencies:
p-limit: 3.1.0
+ p-try@2.2.0: {}
+
package-json-from-dist@1.0.1: {}
pako@2.1.0: {}
@@ -6177,6 +6298,8 @@ snapshots:
optionalDependencies:
typescript: 5.9.3
+ pngjs@5.0.0: {}
+
postcss-selector-parser@7.1.1:
dependencies:
cssesc: 3.0.0
@@ -6225,6 +6348,12 @@ snapshots:
punycode@2.3.1: {}
+ qrcode@1.5.4:
+ dependencies:
+ dijkstrajs: 1.0.3
+ pngjs: 5.0.0
+ yargs: 15.4.1
+
qs@6.15.2:
dependencies:
side-channel: 1.1.0
@@ -6250,8 +6379,12 @@ snapshots:
dependencies:
throttleit: 1.0.1
+ require-directory@2.1.1: {}
+
require-from-string@2.0.2: {}
+ require-main-filename@2.0.0: {}
+
resolve-from@4.0.0: {}
resolve-protobuf-schema@2.1.0:
@@ -6323,6 +6456,8 @@ snapshots:
semver@7.8.1: {}
+ set-blocking@2.0.0: {}
+
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
@@ -6781,6 +6916,8 @@ snapshots:
tr46: 6.0.0
webidl-conversions: 8.0.1
+ which-module@2.0.1: {}
+
which@2.0.2:
dependencies:
isexe: 2.0.0
@@ -6800,6 +6937,12 @@ snapshots:
word@0.3.0: {}
+ wrap-ansi@6.2.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
@@ -6843,8 +6986,29 @@ snapshots:
xmlchars@2.2.0: {}
+ y18n@4.0.3: {}
+
yallist@3.1.1: {}
+ yargs-parser@18.1.3:
+ dependencies:
+ camelcase: 5.3.1
+ decamelize: 1.2.0
+
+ yargs@15.4.1:
+ dependencies:
+ cliui: 6.0.0
+ decamelize: 1.2.0
+ find-up: 4.1.0
+ get-caller-file: 2.0.5
+ require-directory: 2.1.1
+ require-main-filename: 2.0.0
+ set-blocking: 2.0.0
+ string-width: 4.2.3
+ which-module: 2.0.1
+ y18n: 4.0.3
+ yargs-parser: 18.1.3
+
yauzl@3.3.2:
dependencies:
pend: 1.2.0
diff --git a/fe/src/modules/auth/types/auth.types.ts b/fe/src/modules/auth/types/auth.types.ts
index d985f1c..b46120d 100644
--- a/fe/src/modules/auth/types/auth.types.ts
+++ b/fe/src/modules/auth/types/auth.types.ts
@@ -37,6 +37,7 @@ export interface AuthUser {
image_url: string | null
member_number: number | null
member_type: string | null
+ public_profile_token: string | null
status: string
gender: string | null
marriage_status: string | null
diff --git a/fe/src/modules/profile/components/MemberDigitalCard.vue b/fe/src/modules/profile/components/MemberDigitalCard.vue
new file mode 100644
index 0000000..6e2b4a9
--- /dev/null
+++ b/fe/src/modules/profile/components/MemberDigitalCard.vue
@@ -0,0 +1,186 @@
+
+
+
+
+
+
+
+
+
+ Klik kad untuk paparan penuh
+
+
+
+
+
+ {{ isFlipped ? 'Papar depan kad' : 'Imbas kod QR' }}
+
+
+
+
+ {{ downloading ? 'Menyimpan...' : 'Simpan kad' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ isFlipped ? 'Papar depan kad' : 'Imbas kod QR' }}
+
+
+
+
+ {{ downloading ? 'Menyimpan...' : 'Simpan kad' }}
+
+
+
+
+ Klik di luar kad untuk tutup
+
+
+
+
+
diff --git a/fe/src/modules/profile/components/MemberDigitalCardBack.vue b/fe/src/modules/profile/components/MemberDigitalCardBack.vue
new file mode 100644
index 0000000..bc49c86
--- /dev/null
+++ b/fe/src/modules/profile/components/MemberDigitalCardBack.vue
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Belakang · Kod QR
+
+
+
+
+
+
+
+
+ {{ qrError ? 'Kod QR tidak tersedia.' : 'Memuatkan...' }}
+
+
+
+
+
+
+ Imbas untuk sahkan profil anggota MyKOPKB.
+
+
+
+
+ No. Anggota
+
+
+ {{ displayCardValue(memberNumber) }}
+
+
+
+
+
+
+
+ Koperasi Permodalan Kelantan Berhad (KOPKB)
+
+
+
+
+
diff --git a/fe/src/modules/profile/components/MemberDigitalCardFlip.vue b/fe/src/modules/profile/components/MemberDigitalCardFlip.vue
new file mode 100644
index 0000000..ba3c43f
--- /dev/null
+++ b/fe/src/modules/profile/components/MemberDigitalCardFlip.vue
@@ -0,0 +1,34 @@
+
+
+
+
+
diff --git a/fe/src/modules/profile/components/MemberDigitalCardFront.vue b/fe/src/modules/profile/components/MemberDigitalCardFront.vue
new file mode 100644
index 0000000..7929541
--- /dev/null
+++ b/fe/src/modules/profile/components/MemberDigitalCardFront.vue
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
+
+
+ {{ avatarFallback }}
+
+
+
+
+
+
+
+ Kad Digital
+
+
+
+
+
+
+ No. Anggota
+
+
+ {{ displayCardValue(memberNumber) }}
+
+
+
+
+
+ Unit
+
+
+ {{ displayCardValue(companyName) }}
+
+
+
+
+
+
+
+ {{ memberName || '-' }}
+
+
+ Nama
+
+
+
+
+ {{ displayCardValue(memberType) }}
+
+
+ Jenis Anggota
+
+
+
+
+
+
diff --git a/fe/src/modules/profile/index.ts b/fe/src/modules/profile/index.ts
index 6213d79..b69430f 100644
--- a/fe/src/modules/profile/index.ts
+++ b/fe/src/modules/profile/index.ts
@@ -1,2 +1,2 @@
-export { profileLayoutRoutes } from './routes'
+export { profileLayoutRoutes, profilePublicRoutes } from './routes'
export { profileMenu } from './menu'
diff --git a/fe/src/modules/profile/pages/PublicMemberProfile.vue b/fe/src/modules/profile/pages/PublicMemberProfile.vue
new file mode 100644
index 0000000..60722bb
--- /dev/null
+++ b/fe/src/modules/profile/pages/PublicMemberProfile.vue
@@ -0,0 +1,140 @@
+
+
+
+
+
+
+
+
Maklumat Anggota
+
+
+
+ Pengesahan gagal
+ {{ error }}
+
+
+
+ Memuatkan maklumat anggota...
+
+
+
+
+
+
Disahkan
+
+ MyKOPKB
+
+
+
+
+
+ {{ avatarFallback }}
+
+
+
+
{{ member.name }}
+
{{ displayValue(member.member_type) }}
+
+
+
+
+
+
+
+
+
No. Anggota
+
+ {{ displayValue(member.member_number) }}
+
+
+
+
+
+
+
+
Syarikat
+
+ {{ displayValue(member.company_name) }}
+
+
+
+
+
+
+
+
Status
+
+ {{ displayValue(member.status) }}
+
+
+
+
+
+ Disahkan pada {{ new Date(member.verified_at).toLocaleString('ms-MY') }}
+
+
+
+
+
+
diff --git a/fe/src/modules/profile/routes.ts b/fe/src/modules/profile/routes.ts
index 22e4a07..5d02d5a 100644
--- a/fe/src/modules/profile/routes.ts
+++ b/fe/src/modules/profile/routes.ts
@@ -1,5 +1,14 @@
import type { RouteRecordRaw } from 'vue-router'
+export const profilePublicRoutes: RouteRecordRaw[] = [
+ {
+ path: '/v/:token',
+ name: 'public-member-profile',
+ component: () => import('./pages/PublicMemberProfile.vue'),
+ meta: { public: true, module: 'profile' },
+ },
+]
+
export const profileLayoutRoutes: RouteRecordRaw[] = [
{
path: 'profile-overview-2',
diff --git a/fe/src/modules/profile/services/member-digital-card.service.ts b/fe/src/modules/profile/services/member-digital-card.service.ts
new file mode 100644
index 0000000..05c7b92
--- /dev/null
+++ b/fe/src/modules/profile/services/member-digital-card.service.ts
@@ -0,0 +1,35 @@
+import { saveAs } from 'file-saver'
+import axios from 'axios'
+import { api } from '@/core/services/api'
+import { buildCardDownloadFileName } from '../utils/member-digital-card.utils'
+
+export async function downloadMemberDigitalCard(
+ memberNumber: string | number | null | undefined,
+ side: 'depan' | 'belakang',
+) {
+ try {
+ const { data } = await api.get('/v1/profile/digital-card', {
+ params: { side },
+ responseType: 'blob',
+ })
+
+ saveAs(data, buildCardDownloadFileName(memberNumber, side))
+ } catch (error) {
+ if (axios.isAxiosError(error) && error.response?.data instanceof Blob) {
+ const text = await error.response.data.text()
+
+ try {
+ const payload = JSON.parse(text) as { message?: string }
+ throw new Error(payload.message ?? 'Gagal menyimpan kad.')
+ } catch (parseError) {
+ if (parseError instanceof SyntaxError) {
+ throw error
+ }
+
+ throw parseError
+ }
+ }
+
+ throw error
+ }
+}
diff --git a/fe/src/modules/profile/services/public-member-profile.service.ts b/fe/src/modules/profile/services/public-member-profile.service.ts
new file mode 100644
index 0000000..bbc7158
--- /dev/null
+++ b/fe/src/modules/profile/services/public-member-profile.service.ts
@@ -0,0 +1,12 @@
+import { api } from '@/core/services/api'
+import type { PublicMemberProfileApiResponse } from '../types/public-member-profile.types'
+
+export async function getPublicMemberProfile(
+ token: string,
+): Promise {
+ const { data } = await api.get(
+ `/v1/public/members/${encodeURIComponent(token)}`,
+ )
+
+ return data
+}
diff --git a/fe/src/modules/profile/types/public-member-profile.types.ts b/fe/src/modules/profile/types/public-member-profile.types.ts
new file mode 100644
index 0000000..4f694d4
--- /dev/null
+++ b/fe/src/modules/profile/types/public-member-profile.types.ts
@@ -0,0 +1,16 @@
+export interface PublicMemberProfile {
+ name: string
+ member_number: number | null
+ member_type: string | null
+ status: string
+ image_url: string | null
+ company_name: string | null
+ verified_at: string
+}
+
+export interface PublicMemberProfileApiResponse {
+ success: boolean
+ message?: string
+ code?: 'public_profile_not_found' | 'public_profile_token_expired'
+ data: PublicMemberProfile | null
+}
diff --git a/fe/src/modules/profile/utils/member-digital-card.utils.ts b/fe/src/modules/profile/utils/member-digital-card.utils.ts
new file mode 100644
index 0000000..81f23ed
--- /dev/null
+++ b/fe/src/modules/profile/utils/member-digital-card.utils.ts
@@ -0,0 +1,27 @@
+export function displayCardValue(value: string | number | null | undefined) {
+ if (value === null || value === undefined || value === '') return '-'
+ return String(value).trim() || '-'
+}
+
+export function buildCardDownloadFileName(
+ memberNumber: string | number | null | undefined,
+ side: 'depan' | 'belakang',
+) {
+ const number = memberNumber ?? 'anggota'
+ return `kad-digital-${number}-${side}.png`
+}
+
+export function toProxiedStorageUrl(url: string | null | undefined): string | null | undefined {
+ if (!url) return url
+
+ try {
+ const parsed = new URL(url, window.location.origin)
+ if (parsed.pathname.startsWith('/storage/')) {
+ return `${parsed.pathname}${parsed.search}`
+ }
+ } catch {
+ // Keep original URL when parsing fails.
+ }
+
+ return url
+}
diff --git a/fe/src/router/index.ts b/fe/src/router/index.ts
index c251694..3fe8fe2 100644
--- a/fe/src/router/index.ts
+++ b/fe/src/router/index.ts
@@ -5,7 +5,7 @@ import { hasAnyActiveRolePermission } from '@/core/utils/activeRolePermission'
import { getRouteRequiredPermissions } from '@/core/utils/routePermission'
import { authPublicRoutes, resolvePostAuthRoute } from '@/modules/auth'
import { membershipApplicationPublicRoutes, membershipApplicationLayoutRoutes } from '@/modules/membership-application'
-import { profileLayoutRoutes } from '@/modules/profile'
+import { profileLayoutRoutes, profilePublicRoutes } from '@/modules/profile'
import { pinia } from '@/pinia'
import { useAuthStore } from '@/stores/auth'
import { userLayoutRoutes } from '@/modules/user'
@@ -70,11 +70,6 @@ const router = createRouter({
name: 'reviews',
component: () => import('../views/Reviews.vue'),
},
- {
- path: 'inbox',
- name: 'inbox',
- component: () => import('../views/Inbox.vue'),
- },
{
path: 'file-manager',
name: 'file-manager',
@@ -85,51 +80,11 @@ const router = createRouter({
name: 'point-of-sale',
component: () => import('../views/PointOfSale.vue'),
},
- {
- path: 'chat',
- name: 'chat',
- component: () => import('../views/Chat.vue'),
- },
- {
- path: 'post',
- name: 'post',
- component: () => import('../views/Post.vue'),
- },
{
path: 'crud-form',
name: 'crud-form',
component: () => import('../views/CrudForm.vue'),
},
- {
- path: 'users-layout-1',
- name: 'users-layout-1',
- component: () => import('../views/UsersLayout1.vue'),
- },
- {
- path: 'users-layout-2',
- name: 'users-layout-2',
- component: () => import('../views/UsersLayout2.vue'),
- },
- {
- path: 'users-layout-3',
- name: 'users-layout-3',
- component: () => import('../views/UsersLayout3.vue'),
- },
- {
- path: 'wizard-layout-1',
- name: 'wizard-layout-1',
- component: () => import('../views/WizardLayout1.vue'),
- },
- {
- path: 'wizard-layout-2',
- name: 'wizard-layout-2',
- component: () => import('../views/WizardLayout2.vue'),
- },
- {
- path: 'wizard-layout-3',
- name: 'wizard-layout-3',
- component: () => import('../views/WizardLayout3.vue'),
- },
{
path: 'blog-layout-1',
name: 'blog-layout-1',
@@ -191,26 +146,6 @@ const router = createRouter({
component: DocsLayout,
children: [
// Documentation
- {
- path: "accordion",
- name: "accordion",
- component: () => import("../docs/pages/accordion.vue"),
- },
- {
- path: "alert",
- name: "alert",
- component: () => import("../docs/pages/alert.vue"),
- },
- {
- path: "avatar",
- name: "avatar",
- component: () => import("../docs/pages/avatar.vue"),
- },
- {
- path: "badge",
- name: "badge",
- component: () => import("../docs/pages/badge.vue"),
- },
{
path: "box",
name: "box",
@@ -221,26 +156,11 @@ const router = createRouter({
name: "breadcrumb",
component: () => import("../docs/pages/breadcrumb.vue"),
},
- {
- path: "button",
- name: "button",
- component: () => import("../docs/pages/button.vue"),
- },
- {
- path: "carousel",
- name: "carousel",
- component: () => import("../docs/pages/carousel.vue"),
- },
{
path: "chart",
name: "chart",
component: () => import("../docs/pages/chart.vue"),
},
- {
- path: "checkbox",
- name: "checkbox",
- component: () => import("../docs/pages/checkbox.vue"),
- },
{
path: "combobox",
name: "combobox",
@@ -261,16 +181,6 @@ const router = createRouter({
name: "field",
component: () => import("../docs/pages/field.vue"),
},
- {
- path: "input",
- name: "input",
- component: () => import("../docs/pages/input.vue"),
- },
- {
- path: "map",
- name: "map",
- component: () => import("../docs/pages/map.vue"),
- },
{
path: "menu",
name: "menu",
@@ -331,11 +241,6 @@ const router = createRouter({
name: "switch",
component: () => import("../docs/pages/switch.vue"),
},
- {
- path: "table",
- name: "table",
- component: () => import("../docs/pages/table.vue"),
- },
{
path: "tabs",
name: "tabs",
@@ -362,6 +267,7 @@ const router = createRouter({
},
...authPublicRoutes,
...membershipApplicationPublicRoutes,
+ ...profilePublicRoutes,
{
path: '/error-page',
name: 'error-page',
diff --git a/fe/vite.config.ts b/fe/vite.config.ts
index 8717bea..2f64aec 100644
--- a/fe/vite.config.ts
+++ b/fe/vite.config.ts
@@ -18,4 +18,12 @@ export default defineConfig({
'@mykopkb/core': fileURLToPath(new URL('./src/components/ui', import.meta.url))
},
},
+ server: {
+ proxy: {
+ '/storage': {
+ target: process.env.VITE_API_BASE_URL || 'http://localhost',
+ changeOrigin: true,
+ },
+ },
+ },
})
--
2.52.0
From cc2492c3fc2eba80bf21697ccded8f688a02c483 Mon Sep 17 00:00:00 2001
From: ISMAIL MASSERAN
Date: Mon, 6 Jul 2026 11:08:32 +0800
Subject: [PATCH 3/4] DONE: fix error on pagination where it cannot redirect to
next pages, add onboarding_completed_at for new registered user, move
dashboard page design into modules folder
---
README.md | 5 +-
be/Modules/Auth/Entities/User.php | 2 +
be/Modules/Auth/Transformers/UserResource.php | 1 +
...ershipApplicationSubmittedNotification.php | 13 +-
.../views/emails/submitted.blade.php | 9 +-
.../Services/MembershipApplicationService.php | 16 +-
...onboarding_completed_at_to_users_table.php | 22 ++
.../User/Http/Controllers/UserController.php | 11 +
be/Modules/User/Routes/api.php | 1 +
be/Modules/User/Services/UserService.php | 11 +
be/config/mail.php | 2 +-
.../ui/pagination/PaginationRoot.vue | 30 +-
.../components/ui/styles/pagination.styles.ts | 4 +-
fe/src/components/ui/usage/DataTable.vue | 33 +--
fe/src/main/side-menu.ts | 7 +-
fe/src/modules/auth/types/auth.types.ts | 1 +
fe/src/modules/dashboard/index.ts | 1 +
fe/src/modules/dashboard/menu.ts | 9 +
.../pages/DashboardOverview-backup.vue} | 41 ++-
.../dashboard/pages/DashboardOverview.vue | 258 ++++++++++++++++++
fe/src/modules/dashboard/routes.ts | 10 +
.../profile/services/profile.service.ts | 5 +
fe/src/router/index.ts | 7 +-
23 files changed, 421 insertions(+), 78 deletions(-)
create mode 100644 be/Modules/User/Database/Migrations/2026_07_06_100000_add_onboarding_completed_at_to_users_table.php
create mode 100644 fe/src/modules/dashboard/index.ts
create mode 100644 fe/src/modules/dashboard/menu.ts
rename fe/src/{views/DashboardOverview1.vue => modules/dashboard/pages/DashboardOverview-backup.vue} (95%)
create mode 100644 fe/src/modules/dashboard/pages/DashboardOverview.vue
create mode 100644 fe/src/modules/dashboard/routes.ts
diff --git a/README.md b/README.md
index 24a70ef..532fca3 100644
--- a/README.md
+++ b/README.md
@@ -76,4 +76,7 @@ SELECT setval('public.permissions_id_seq', COALESCE((SELECT MAX(id) FROM permiss
## DO NOT RUN THIS BECAUSE IT WILL REMOVE ALL THE UNTAGGED FILES IN LOCAL
```bash
git reset --hard development # moves the current branch (e.g., main) to point exactly where development
-```
\ No newline at end of file
+```
+
+## Public endpoint to apply membership:
+## http://localhost:5173/apply-membership
\ No newline at end of file
diff --git a/be/Modules/Auth/Entities/User.php b/be/Modules/Auth/Entities/User.php
index ac1422e..a20c45f 100644
--- a/be/Modules/Auth/Entities/User.php
+++ b/be/Modules/Auth/Entities/User.php
@@ -56,6 +56,7 @@ class User extends Authenticatable
'join_date',
'birth_date',
'birth_place',
+ 'onboarding_completed_at',
];
/**
@@ -137,6 +138,7 @@ class User extends Authenticatable
'join_date' => 'date',
'birth_date' => 'date',
'birth_place' => 'string',
+ 'onboarding_completed_at' => 'datetime',
];
}
diff --git a/be/Modules/Auth/Transformers/UserResource.php b/be/Modules/Auth/Transformers/UserResource.php
index bc89bde..b3cc612 100644
--- a/be/Modules/Auth/Transformers/UserResource.php
+++ b/be/Modules/Auth/Transformers/UserResource.php
@@ -40,6 +40,7 @@ class UserResource extends JsonResource
'join_date' => $this->join_date,
'birth_date' => $this->birth_date,
'birth_place' => $this->birth_place,
+ 'onboarding_completed_at' => $this->onboarding_completed_at,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
'deleted_at' => $this->deleted_at,
diff --git a/be/Modules/MembershipApplication/Emails/MembershipApplicationSubmittedNotification.php b/be/Modules/MembershipApplication/Emails/MembershipApplicationSubmittedNotification.php
index 4b77632..97d5f68 100644
--- a/be/Modules/MembershipApplication/Emails/MembershipApplicationSubmittedNotification.php
+++ b/be/Modules/MembershipApplication/Emails/MembershipApplicationSubmittedNotification.php
@@ -2,6 +2,7 @@
namespace Modules\MembershipApplication\Emails;
+use App\Notifications\Concerns\BuildsMailMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
@@ -9,6 +10,7 @@ use Modules\MembershipApplication\Entities\MembershipApplication;
class MembershipApplicationSubmittedNotification extends Notification
{
+ use BuildsMailMessage;
use Queueable;
public function __construct(
@@ -24,9 +26,10 @@ class MembershipApplicationSubmittedNotification extends Notification
{
$this->application->loadMissing(['applicant', 'heirs', 'documents', 'references.member']);
- return (new MailMessage)
- ->subject('Pengesahan Permohonan Keahlian')
- ->markdown('membershipapplication::emails.submitted', [
+ return $this->mailMessage(
+ subject: 'Pengesahan Permohonan Keahlian',
+ view: 'membershipapplication::emails.submitted',
+ data: [
'application' => $this->application,
'applicant' => $this->application->applicant,
'heirs' => $this->application->heirs,
@@ -37,7 +40,7 @@ class MembershipApplicationSubmittedNotification extends Notification
'salary_slip' => 'Slip Gaji',
'employer_letter' => 'Surat Pengesahan Majikan',
],
- 'logoPath' => public_path('images/logo-kopkb.svg'),
- ]);
+ ],
+ );
}
}
diff --git a/be/Modules/MembershipApplication/Resources/views/emails/submitted.blade.php b/be/Modules/MembershipApplication/Resources/views/emails/submitted.blade.php
index 7a75b9e..5782b9b 100644
--- a/be/Modules/MembershipApplication/Resources/views/emails/submitted.blade.php
+++ b/be/Modules/MembershipApplication/Resources/views/emails/submitted.blade.php
@@ -1,9 +1,5 @@
@component('mail::message')
-@if (! empty($logoPath) && file_exists($logoPath))
-
-
-
-@endif
+@include('emails.partials.header')
# Pengesahan Permohonan Keahlian
@@ -74,6 +70,5 @@ Tiada dokumen.
Anda akan menerima emel apabila keputusan permohonan tersedia.
-Terima kasih,
-{{ config('app.name') }}
+@include('emails.partials.footer')
@endcomponent
diff --git a/be/Modules/MembershipApplication/Services/MembershipApplicationService.php b/be/Modules/MembershipApplication/Services/MembershipApplicationService.php
index f5a6ba8..887bc7f 100644
--- a/be/Modules/MembershipApplication/Services/MembershipApplicationService.php
+++ b/be/Modules/MembershipApplication/Services/MembershipApplicationService.php
@@ -415,13 +415,13 @@ class MembershipApplicationService
): array {
$applicant = $application->applicant;
$isPassed = $application->board_result === BoardResult::Pass->value;
- // $monthlyShareInstallment = number_format(
- // MembershipApplication::MINIMUM_SHARE_CAPITAL / MembershipApplication::SHARE_INSTALLMENT_MONTHS,
- // 2,
- // '.',
- // '',
- // );
- $monthlyShareInstallment = 84;
+ $stockMonthlyContribution = (float) $applicant->stock_monthly_contribution;
+ $monthlyShareInstallment = number_format(
+ $stockMonthlyContribution < 84 ? 84 : $stockMonthlyContribution,
+ 2,
+ '.',
+ '',
+ );
return [
'application' => $application,
@@ -434,7 +434,7 @@ class MembershipApplicationService
? 'KELULUSAN KEANGGOTAAN KOPERASI PERMODALAN KELANTAN BERHAD'
: 'KEPUTUSAN PERMOHONAN KEANGGOTAAN KOPERASI PERMODALAN KELANTAN BERHAD',
'monthlyShareInstallment' => $monthlyShareInstallment,
- 'stockMonthlyContribution' => number_format((float) $applicant->stock_monthly_contribution, 2, '.', ''),
+ 'stockMonthlyContribution' => number_format($stockMonthlyContribution, 2, '.', ''),
'feeMonthlyContribution' => number_format((float) $applicant->fee_monthly_contribution, 2, '.', ''),
'shareInstallmentMonths' => MembershipApplication::SHARE_INSTALLMENT_MONTHS,
'minimumShareCapital' => number_format(MembershipApplication::MINIMUM_SHARE_CAPITAL, 2, '.', ''),
diff --git a/be/Modules/User/Database/Migrations/2026_07_06_100000_add_onboarding_completed_at_to_users_table.php b/be/Modules/User/Database/Migrations/2026_07_06_100000_add_onboarding_completed_at_to_users_table.php
new file mode 100644
index 0000000..9ef1af9
--- /dev/null
+++ b/be/Modules/User/Database/Migrations/2026_07_06_100000_add_onboarding_completed_at_to_users_table.php
@@ -0,0 +1,22 @@
+timestamp('onboarding_completed_at')->nullable()->after('birth_place');
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('users', function (Blueprint $table) {
+ $table->dropColumn('onboarding_completed_at');
+ });
+ }
+};
diff --git a/be/Modules/User/Http/Controllers/UserController.php b/be/Modules/User/Http/Controllers/UserController.php
index 2526ff6..3e3ffb3 100644
--- a/be/Modules/User/Http/Controllers/UserController.php
+++ b/be/Modules/User/Http/Controllers/UserController.php
@@ -127,6 +127,17 @@ class UserController extends BaseCrudController
]);
}
+ public function completeOnboarding(Request $request): JsonResponse
+ {
+ $user = $this->userService->completeOnboarding($request->user());
+
+ return response()->json([
+ 'success' => true,
+ 'data' => new UserResource($user),
+ 'message' => 'Onboarding completed.',
+ ]);
+ }
+
public function updatePassword(Request $request): JsonResponse
{
$user = $request->user();
diff --git a/be/Modules/User/Routes/api.php b/be/Modules/User/Routes/api.php
index 36d1c39..c4fc38b 100644
--- a/be/Modules/User/Routes/api.php
+++ b/be/Modules/User/Routes/api.php
@@ -35,6 +35,7 @@ Route::middleware(['auth:sanctum', 'single.session'])->prefix('v1')->group(funct
// User profile management routes
Route::post('/profile', [UserController::class, 'updateProfile']);
+ Route::post('/profile/onboarding/complete', [UserController::class, 'completeOnboarding']);
Route::put('/profile/password', [UserController::class, 'updatePassword']);
Route::get('/profile/digital-card', [MemberDigitalCardController::class, 'download'])
->name('profile.digital-card.download');
diff --git a/be/Modules/User/Services/UserService.php b/be/Modules/User/Services/UserService.php
index 90f20fb..0033e64 100644
--- a/be/Modules/User/Services/UserService.php
+++ b/be/Modules/User/Services/UserService.php
@@ -121,6 +121,17 @@ class UserService
/**
* @return array{}|array{error: string}
*/
+ public function completeOnboarding(User $user): User
+ {
+ if ($user->onboarding_completed_at === null) {
+ $user->update(['onboarding_completed_at' => now()]);
+ }
+
+ $user->load(['roles.permissions']);
+
+ return $user;
+ }
+
public function updatePassword(User $user, string $currentPassword, string $newPassword): array
{
if (! Hash::check($currentPassword, $user->password)) {
diff --git a/be/config/mail.php b/be/config/mail.php
index c875aaf..86d1427 100644
--- a/be/config/mail.php
+++ b/be/config/mail.php
@@ -124,6 +124,6 @@ return [
|
*/
- 'logo_path' => env('MAIL_LOGO_PATH', public_path('images/logo/logo-kopkb-letterhead.svg')),
+ 'logo_path' => public_path('images/logo/logo-kopkb-letterhead.svg'),
];
diff --git a/fe/src/components/ui/pagination/PaginationRoot.vue b/fe/src/components/ui/pagination/PaginationRoot.vue
index 4e01884..a1c85e9 100644
--- a/fe/src/components/ui/pagination/PaginationRoot.vue
+++ b/fe/src/components/ui/pagination/PaginationRoot.vue
@@ -7,30 +7,28 @@ import { normalizeProps, useMachine } from "@zag-js/vue";
import { computed, provide } from "vue";
import { paginationRoot } from "@mykopkb/core/styles/pagination.styles";
-const {
- class: className,
- asChild = false,
- count,
- pageSize,
- siblingCount,
- ...props
-} = defineProps & { class?: string; asChild?: boolean }>();
+const props = defineProps & { class?: string; asChild?: boolean }>();
-const service = useMachine(pagination.machine, {
- ...props,
- count,
- pageSize,
- siblingCount,
- id: crypto.randomUUID(),
+const paginationId = crypto.randomUUID();
+
+const machineProps = computed(() => {
+ const { class: _class, asChild: _asChild, id: _id, ...rest } = props;
+
+ return {
+ ...rest,
+ id: paginationId,
+ };
});
+
+const service = useMachine(pagination.machine, machineProps);
const api = computed(() => pagination.connect(service, normalizeProps));
provide("paginationApi", api);
-
-
+
+
diff --git a/fe/src/components/ui/styles/pagination.styles.ts b/fe/src/components/ui/styles/pagination.styles.ts
index c5c3b28..1e0683e 100644
--- a/fe/src/components/ui/styles/pagination.styles.ts
+++ b/fe/src/components/ui/styles/pagination.styles.ts
@@ -1,7 +1,7 @@
export const paginationRoot = 'flex gap-1'
export const paginationItem = [
- 'h-10 px-4 py-2 inline-flex items-center justify-center rounded-xl cursor-pointer hover:bg-foreground/5',
- 'data-[selected]:border data-[selected]:bg-background data-[selected]:border-foreground/10 data-[selected]:font-medium data-[selected]:shadow-md/5',
+ 'h-10 min-w-10 px-3 py-2 inline-flex items-center justify-center rounded-xl cursor-pointer text-foreground hover:bg-foreground/5',
+ 'data-[selected]:border data-[selected]:bg-primary/10 data-[selected]:border-primary/30 data-[selected]:text-primary data-[selected]:font-semibold data-[selected]:shadow-md/5',
'data-[disabled]:opacity-70',
]
export const paginationPrevTrigger = paginationItem
diff --git a/fe/src/components/ui/usage/DataTable.vue b/fe/src/components/ui/usage/DataTable.vue
index 3db6a24..fe74d00 100644
--- a/fe/src/components/ui/usage/DataTable.vue
+++ b/fe/src/components/ui/usage/DataTable.vue
@@ -94,7 +94,6 @@ const props = withDefaults(defineProps<{
items: TableItem[];
loading?: boolean;
itemsPerPage?: number;
- page?: number;
itemKey?: string;
pagination?: PaginationData | null;
showPagination?: boolean;
@@ -106,7 +105,6 @@ const props = withDefaults(defineProps<{
headers: () => [],
loading: false,
itemsPerPage: 10,
- page: 1,
itemKey: 'id',
pagination: null,
showPagination: false,
@@ -122,7 +120,6 @@ const itemsPerPageModel = defineModel('items-per-page');
// Emits
const emit = defineEmits<{
'update:sort-by': [value: SortConfig[]];
- 'update:page': [value: number];
'update:items-per-page': [value: number];
'update:column-widths': [value: Record];
'export-error': [error: Error];
@@ -449,7 +446,6 @@ const setItemsPerPageValue = (details: { value: string[] }) => {
const handlePageChange = (details: { page: number }) => {
pageModel.value = details.page;
- emit('update:page', details.page);
};
const getItemKey = (item: TableItem, index: number) => {
@@ -461,12 +457,6 @@ const currentPage = computed(
() => pageModel.value ?? props.pagination?.current_page ?? 1,
);
-const isFirstPage = computed(() => currentPage.value <= 1);
-
-const isLastPage = computed(
- () => currentPage.value >= (props.pagination?.last_page ?? 1),
-);
-
const slots = useSlots();
const showTableToolbar = computed(() => props.exportable || !!slots.toolbar);
@@ -851,28 +841,21 @@ watch(sortBy, (newSort) => {
-
-
-
-
-
+
+
+
-
-
- {{ pageItem.value }}
-
+
+ {{ pageItem.value }}
-
-
-
-
+
+
diff --git a/fe/src/main/side-menu.ts b/fe/src/main/side-menu.ts
index 67fbe26..f8e3c94 100644
--- a/fe/src/main/side-menu.ts
+++ b/fe/src/main/side-menu.ts
@@ -3,16 +3,13 @@ import { userMenu } from '@/modules/user'
import { roleMenu } from '@/modules/role'
import { membershipApplicationMenu } from '@/modules/membership-application'
import { activityMenu } from '@/modules/activity'
+import { dashboardMenu } from '@/modules/dashboard/menu'
export type { Menu }
const mainMenu: (string | Menu)[] = [
'Dashboard',
- {
- icon: 'CircleGauge',
- title: 'Dashboards',
- route_name: 'dashboard-overview-1',
- },
+ ...dashboardMenu,
'Umum',
...activityMenu,
'Teknologi Maklumat',
diff --git a/fe/src/modules/auth/types/auth.types.ts b/fe/src/modules/auth/types/auth.types.ts
index b46120d..5a46f42 100644
--- a/fe/src/modules/auth/types/auth.types.ts
+++ b/fe/src/modules/auth/types/auth.types.ts
@@ -44,6 +44,7 @@ export interface AuthUser {
join_date: string | null
birth_date: string | null
birth_place: string | null
+ onboarding_completed_at: string | null
roles?: Array
}
diff --git a/fe/src/modules/dashboard/index.ts b/fe/src/modules/dashboard/index.ts
new file mode 100644
index 0000000..6fa27f6
--- /dev/null
+++ b/fe/src/modules/dashboard/index.ts
@@ -0,0 +1 @@
+export { dashboardLayoutRoutes } from './routes'
diff --git a/fe/src/modules/dashboard/menu.ts b/fe/src/modules/dashboard/menu.ts
new file mode 100644
index 0000000..a452801
--- /dev/null
+++ b/fe/src/modules/dashboard/menu.ts
@@ -0,0 +1,9 @@
+import type { Menu } from '@/core/types/menu'
+
+export const dashboardMenu: Menu[] = [
+ {
+ icon: 'CircleGauge',
+ route_name: 'dashboard-overview',
+ title: 'Dashboard',
+ }
+]
diff --git a/fe/src/views/DashboardOverview1.vue b/fe/src/modules/dashboard/pages/DashboardOverview-backup.vue
similarity index 95%
rename from fe/src/views/DashboardOverview1.vue
rename to fe/src/modules/dashboard/pages/DashboardOverview-backup.vue
index 92dd774..f17bbe5 100644
--- a/fe/src/views/DashboardOverview1.vue
+++ b/fe/src/modules/dashboard/pages/DashboardOverview-backup.vue
@@ -1,8 +1,10 @@
@@ -435,7 +465,7 @@ const salesReportDate = ref([
- (onboardingDialog = details.open)">
+
@@ -503,6 +533,11 @@ const salesReportDate = ref
([
Previous
+
+ Selesai
+
+
diff --git a/fe/src/modules/dashboard/pages/DashboardOverview.vue b/fe/src/modules/dashboard/pages/DashboardOverview.vue
new file mode 100644
index 0000000..f03d278
--- /dev/null
+++ b/fe/src/modules/dashboard/pages/DashboardOverview.vue
@@ -0,0 +1,258 @@
+
+
+
+
+
+
+ Sistem Dalam Pembangunan
+
+ {{ appName }} {{ appVersion }} masih dalam fasa pembangunan. Beberapa fungsi papan pemuka
+ belum tersedia. Sila gunakan modul sedia ada melalui menu sisi atau pautan di bawah.
+
+
+
+
+
+
+
+
+
Beta
+
+ {{ greeting }}{{ authStore.userName ? `, ${authStore.userName}` : '' }}
+
+
+ Selamat datang ke {{ appName }}. Papan pemuka utama sedang dibangunkan — buat masa ini,
+ anda boleh mula dengan mengemas kini profil atau mengurus permohonan keahlian.
+
+
+
+
+
+
+
+
Modul Tersedia
+
+
+
+
+
+
+
+ {{ module.title }}
+
+ {{ module.description }}
+
+
+
+ Pergi ke {{ module.title }}
+
+
+
+
+
+
+
+
+
+
Akan Datang
+
+
+
+
+ Ciri-ciri berikut sedang dirancang untuk papan pemuka ini:
+
+
+
+
+
+
+
+
+
Petua
+
+ Pastikan profil anda lengkap dan terkini. Maklumat yang
+ tepat membantu proses kelulusan berjalan lebih lancar.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Selamat Datang ke Sistem MyKOPKB
+
+ Sistem MyKOPKB adalah sistem integrasi bagi ahli Koperasi Permodalan Kelantan Berhad (KOPKB).
+
+
+
+
+ Seterusnya
+
+
+
+
+
+
+
+
+
+
+
+
Mulakan dengan Profil Anda
+
+ Lengkapkan maklumat peribadi, pekerjaan dan butiran bank.
+
+
+
+
+ Kembali
+
+
+ Selesai
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fe/src/modules/dashboard/routes.ts b/fe/src/modules/dashboard/routes.ts
new file mode 100644
index 0000000..4a41593
--- /dev/null
+++ b/fe/src/modules/dashboard/routes.ts
@@ -0,0 +1,10 @@
+import type { RouteRecordRaw } from 'vue-router'
+
+export const dashboardLayoutRoutes: RouteRecordRaw[] = [
+ {
+ path: '/',
+ name: 'dashboard-overview',
+ component: () => import('./pages/DashboardOverview.vue'),
+ meta: { title: 'Dashboard Overview', module: 'dashboard' },
+ },
+]
diff --git a/fe/src/modules/profile/services/profile.service.ts b/fe/src/modules/profile/services/profile.service.ts
index 9ef0f69..46dff45 100644
--- a/fe/src/modules/profile/services/profile.service.ts
+++ b/fe/src/modules/profile/services/profile.service.ts
@@ -63,3 +63,8 @@ export async function updatePassword(payload: UpdatePasswordPayload): Promise('/v1/profile/password', payload)
return data
}
+
+export async function completeOnboarding(): Promise {
+ const { data } = await api.post('/v1/profile/onboarding/complete')
+ return data
+}
diff --git a/fe/src/router/index.ts b/fe/src/router/index.ts
index 3fe8fe2..049f848 100644
--- a/fe/src/router/index.ts
+++ b/fe/src/router/index.ts
@@ -11,6 +11,7 @@ import { useAuthStore } from '@/stores/auth'
import { userLayoutRoutes } from '@/modules/user'
import { roleLayoutRoutes } from '@/modules/role'
import { activityLayoutRoutes } from '@/modules/activity'
+import { dashboardLayoutRoutes } from '@/modules/dashboard'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@@ -20,11 +21,7 @@ const router = createRouter({
path: '/',
component: Layout,
children: [
- {
- path: '/',
- name: 'dashboard-overview-1',
- component: () => import('../views/DashboardOverview1.vue'),
- },
+ ...dashboardLayoutRoutes,
{
path: 'categories',
name: 'categories',
--
2.52.0
From 501545959f344c2f7c3db99fad78194ded3cfe00 Mon Sep 17 00:00:00 2001
From: ISMAIL MASSERAN
Date: Mon, 6 Jul 2026 12:50:14 +0800
Subject: [PATCH 4/4] DONE: add bos signature on letter, remove unused files or
templates files, update logo to mykopkb, remove theme switcher use gradient
colour, add external system module on frontend
---
.../Controllers/ActivityLogController.php | 11 +-
.../Providers/ActivityLogServiceProvider.php | 5 +
.../views/pdf/approved-letter.blade.php | 6 +-
be/Modules/User/Config/config.php | 2 +-
be/app/Services/LetterService.php | 13 +-
be/config/letter.php | 8 +
be/config/mail.php | 2 +-
be/public/images/logo/logo-kopkb.svg | 418 ----
be/public/images/logo/logo.svg | 649 ++++++
be/resources/assets/signatures/signature.svg | 22 +
.../views/pdf/layouts/letter.blade.php | 6 +
.../pdf/preview-approved-membership.blade.php | 14 +-
fe/src/.DS_Store | Bin 8196 -> 8196 bytes
fe/src/assets/images/logo.svg | 1065 ++++++----
fe/src/assets/images/logonew-mykopkb.svg | 185 --
.../theme-switcher/ThemeSwitcher.vue | 16 +-
fe/src/components/ui/usage/DataTable.vue | 32 +-
fe/src/docs/DocsLayout.vue | 11 -
fe/src/docs/pages/alert.vue | 639 ------
fe/src/docs/pages/box.vue | 199 --
fe/src/docs/pages/breadcrumb.vue | 181 --
fe/src/docs/pages/chart.vue | 235 ---
fe/src/docs/pages/combobox.vue | 1111 -----------
fe/src/docs/pages/datepicker.vue | 1771 -----------------
fe/src/docs/pages/dialog.vue | 560 ------
fe/src/docs/pages/field.vue | 484 -----
fe/src/docs/pages/menu.vue | 831 --------
fe/src/docs/pages/pagination.vue | 298 ---
fe/src/docs/pages/popover.vue | 447 -----
fe/src/docs/pages/progress-circular.vue | 255 ---
fe/src/docs/pages/progress-linear.vue | 227 ---
fe/src/docs/pages/radio-group.vue | 325 ---
fe/src/docs/pages/scroll-area.vue | 259 ---
fe/src/docs/pages/select.vue | 886 ---------
fe/src/docs/pages/sheet.vue | 508 -----
fe/src/docs/pages/slider.vue | 493 -----
fe/src/docs/pages/slot.vue | 196 --
fe/src/docs/pages/switch.vue | 230 ---
fe/src/docs/pages/tabs.vue | 361 ----
fe/src/docs/pages/textarea.vue | 69 -
fe/src/docs/pages/toast.vue | 371 ----
fe/src/docs/pages/tooltip.vue | 287 ---
fe/src/main.ts | 2 +
fe/src/main/side-menu.ts | 195 +-
.../composables/useActivityLogList.ts | 74 +
fe/src/modules/activity-log/index.ts | 2 +
fe/src/modules/activity-log/menu.ts | 10 +
.../activity-log/pages/ActivityLogList.vue | 105 +
fe/src/modules/activity-log/routes.ts | 14 +
.../services/activity-log.service.ts | 17 +
.../activity-log/types/activity-log.types.ts | 26 +
.../activity-log/utils/activity-log.utils.ts | 6 +
fe/src/modules/auth/pages/TermsConditions.vue | 10 +-
.../composables/useExternalSystemDetail.ts | 41 +
.../composables/useExternalSystemList.ts | 48 +
.../data/dummy-external-systems.ts | 38 +
fe/src/modules/external-system/index.ts | 2 +
fe/src/modules/external-system/menu.ts | 9 +
.../pages/ExternalSystemDetail.vue | 115 ++
.../pages/ExternalSystemList.vue | 104 +
fe/src/modules/external-system/routes.ts | 22 +
.../types/external-system.types.ts | 20 +
.../utils/external-system.utils.ts | 74 +
.../profile/pages/ProfileOverview2.vue | 2 +-
.../profile/pages/PublicMemberProfile.vue | 2 +-
fe/src/router/index.ts | 248 +--
fe/src/themes/Enigma/SideMenu/SideMenu.vue | 10 +-
fe/src/themes/Layout.vue | 23 +-
fe/src/utils/applyAppearance.ts | 24 +
fe/src/views/AddProduct.vue | 913 ---------
fe/src/views/BlogLayout1.vue | 207 --
fe/src/views/BlogLayout2.vue | 207 --
fe/src/views/BlogLayout3.vue | 236 ---
fe/src/views/Categories.vue | 180 --
fe/src/views/CrudForm.vue | 141 --
fe/src/views/ErrorPage.vue | 33 -
fe/src/views/FaqLayout1.vue | 271 ---
fe/src/views/FaqLayout2.vue | 232 ---
fe/src/views/FaqLayout3.vue | 271 ---
fe/src/views/FileManager.vue | 235 ---
fe/src/views/ForbiddenPage.vue | 44 -
fe/src/views/InvoiceLayout1.vue | 137 --
fe/src/views/InvoiceLayout2.vue | 142 --
fe/src/views/PointOfSale.vue | 282 ---
fe/src/views/PricingLayout1.vue | 131 --
fe/src/views/PricingLayout2.vue | 279 ---
fe/src/views/ProductGrid.vue | 152 --
fe/src/views/ProductList.vue | 178 --
fe/src/views/Reviews.vue | 192 --
fe/src/views/SellerDetail.vue | 261 ---
fe/src/views/SellerList.vue | 207 --
fe/src/views/TransactionDetail.vue | 209 --
fe/src/views/TransactionList.vue | 207 --
93 files changed, 2187 insertions(+), 18091 deletions(-)
delete mode 100644 be/public/images/logo/logo-kopkb.svg
create mode 100644 be/public/images/logo/logo.svg
create mode 100644 be/resources/assets/signatures/signature.svg
delete mode 100644 fe/src/assets/images/logonew-mykopkb.svg
delete mode 100644 fe/src/docs/DocsLayout.vue
delete mode 100644 fe/src/docs/pages/alert.vue
delete mode 100644 fe/src/docs/pages/box.vue
delete mode 100644 fe/src/docs/pages/breadcrumb.vue
delete mode 100644 fe/src/docs/pages/chart.vue
delete mode 100644 fe/src/docs/pages/combobox.vue
delete mode 100644 fe/src/docs/pages/datepicker.vue
delete mode 100644 fe/src/docs/pages/dialog.vue
delete mode 100644 fe/src/docs/pages/field.vue
delete mode 100644 fe/src/docs/pages/menu.vue
delete mode 100644 fe/src/docs/pages/pagination.vue
delete mode 100644 fe/src/docs/pages/popover.vue
delete mode 100644 fe/src/docs/pages/progress-circular.vue
delete mode 100644 fe/src/docs/pages/progress-linear.vue
delete mode 100644 fe/src/docs/pages/radio-group.vue
delete mode 100644 fe/src/docs/pages/scroll-area.vue
delete mode 100644 fe/src/docs/pages/select.vue
delete mode 100644 fe/src/docs/pages/sheet.vue
delete mode 100644 fe/src/docs/pages/slider.vue
delete mode 100644 fe/src/docs/pages/slot.vue
delete mode 100644 fe/src/docs/pages/switch.vue
delete mode 100644 fe/src/docs/pages/tabs.vue
delete mode 100644 fe/src/docs/pages/textarea.vue
delete mode 100644 fe/src/docs/pages/toast.vue
delete mode 100644 fe/src/docs/pages/tooltip.vue
create mode 100644 fe/src/modules/activity-log/composables/useActivityLogList.ts
create mode 100644 fe/src/modules/activity-log/index.ts
create mode 100644 fe/src/modules/activity-log/menu.ts
create mode 100644 fe/src/modules/activity-log/pages/ActivityLogList.vue
create mode 100644 fe/src/modules/activity-log/routes.ts
create mode 100644 fe/src/modules/activity-log/services/activity-log.service.ts
create mode 100644 fe/src/modules/activity-log/types/activity-log.types.ts
create mode 100644 fe/src/modules/activity-log/utils/activity-log.utils.ts
create mode 100644 fe/src/modules/external-system/composables/useExternalSystemDetail.ts
create mode 100644 fe/src/modules/external-system/composables/useExternalSystemList.ts
create mode 100644 fe/src/modules/external-system/data/dummy-external-systems.ts
create mode 100644 fe/src/modules/external-system/index.ts
create mode 100644 fe/src/modules/external-system/menu.ts
create mode 100644 fe/src/modules/external-system/pages/ExternalSystemDetail.vue
create mode 100644 fe/src/modules/external-system/pages/ExternalSystemList.vue
create mode 100644 fe/src/modules/external-system/routes.ts
create mode 100644 fe/src/modules/external-system/types/external-system.types.ts
create mode 100644 fe/src/modules/external-system/utils/external-system.utils.ts
create mode 100644 fe/src/utils/applyAppearance.ts
delete mode 100644 fe/src/views/AddProduct.vue
delete mode 100644 fe/src/views/BlogLayout1.vue
delete mode 100644 fe/src/views/BlogLayout2.vue
delete mode 100644 fe/src/views/BlogLayout3.vue
delete mode 100644 fe/src/views/Categories.vue
delete mode 100644 fe/src/views/CrudForm.vue
delete mode 100644 fe/src/views/ErrorPage.vue
delete mode 100644 fe/src/views/FaqLayout1.vue
delete mode 100644 fe/src/views/FaqLayout2.vue
delete mode 100644 fe/src/views/FaqLayout3.vue
delete mode 100644 fe/src/views/FileManager.vue
delete mode 100644 fe/src/views/ForbiddenPage.vue
delete mode 100644 fe/src/views/InvoiceLayout1.vue
delete mode 100644 fe/src/views/InvoiceLayout2.vue
delete mode 100644 fe/src/views/PointOfSale.vue
delete mode 100644 fe/src/views/PricingLayout1.vue
delete mode 100644 fe/src/views/PricingLayout2.vue
delete mode 100644 fe/src/views/ProductGrid.vue
delete mode 100644 fe/src/views/ProductList.vue
delete mode 100644 fe/src/views/Reviews.vue
delete mode 100644 fe/src/views/SellerDetail.vue
delete mode 100644 fe/src/views/SellerList.vue
delete mode 100644 fe/src/views/TransactionDetail.vue
delete mode 100644 fe/src/views/TransactionList.vue
diff --git a/be/Modules/ActivityLog/Http/Controllers/ActivityLogController.php b/be/Modules/ActivityLog/Http/Controllers/ActivityLogController.php
index 0a1e8d7..fdbbe69 100644
--- a/be/Modules/ActivityLog/Http/Controllers/ActivityLogController.php
+++ b/be/Modules/ActivityLog/Http/Controllers/ActivityLogController.php
@@ -3,17 +3,22 @@
namespace Modules\ActivityLog\Http\Controllers;
use App\Http\Controllers\Controller;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\Request;
use Spatie\Activitylog\Models\Activity;
use App\Services\ActivityLogger;
class ActivityLogController extends Controller
{
+ use AuthorizesRequests;
+
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
+ $this->authorize('viewAny', Activity::class);
+
// ActivityLogger::logView(new Activity, 'Viewed activity logs');
$perPage = $request->get('per_page', 10);
@@ -27,8 +32,10 @@ class ActivityLogController extends Controller
$q->where('description', 'ILIKE', "%{$search}%")
->orWhere('subject_type', 'ILIKE', "%{$search}%")
->orWhere('event', 'ILIKE', "%{$search}%")
- ->orWhere('causer.name', 'ILIKE', "%{$search}%")
- ->orWhere('causer.email', 'ILIKE', "%{$search}%");
+ ->orWhereHas('causer', function ($causerQuery) use ($search) {
+ $causerQuery->where('name', 'ILIKE', "%{$search}%")
+ ->orWhere('email', 'ILIKE', "%{$search}%");
+ });
});
}
diff --git a/be/Modules/ActivityLog/Providers/ActivityLogServiceProvider.php b/be/Modules/ActivityLog/Providers/ActivityLogServiceProvider.php
index d1fad58..69101d9 100644
--- a/be/Modules/ActivityLog/Providers/ActivityLogServiceProvider.php
+++ b/be/Modules/ActivityLog/Providers/ActivityLogServiceProvider.php
@@ -3,7 +3,10 @@
namespace Modules\ActivityLog\Providers;
use Illuminate\Support\Facades\Blade;
+use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;
+use Modules\ActivityLog\Policies\ActivityLogPolicy;
+use Spatie\Activitylog\Models\Activity;
use Nwidart\Modules\Traits\PathNamespace;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
@@ -21,6 +24,8 @@ class ActivityLogServiceProvider extends ServiceProvider
*/
public function boot(): void
{
+ Gate::policy(Activity::class, ActivityLogPolicy::class);
+
$this->registerCommands();
$this->registerCommandSchedules();
$this->registerTranslations();
diff --git a/be/Modules/MembershipApplication/Resources/views/pdf/approved-letter.blade.php b/be/Modules/MembershipApplication/Resources/views/pdf/approved-letter.blade.php
index 8646b8d..21c1fea 100644
--- a/be/Modules/MembershipApplication/Resources/views/pdf/approved-letter.blade.php
+++ b/be/Modules/MembershipApplication/Resources/views/pdf/approved-letter.blade.php
@@ -66,7 +66,11 @@
@section('letter-signature')
KOPERASI PERMODALAN KELANTAN BERHAD
-
+
+ @if (! empty($signatureUrl))
+
+ @endif
+
MUHAMMAD NAFIS BIN ZAINUDIN
Pengurus Besar
b/p Setiausaha
diff --git a/be/Modules/User/Config/config.php b/be/Modules/User/Config/config.php
index 57bdafc..6484b21 100644
--- a/be/Modules/User/Config/config.php
+++ b/be/Modules/User/Config/config.php
@@ -4,5 +4,5 @@ return [
'name' => 'User',
'public_profile_token_ttl_days' => (int) env('PUBLIC_PROFILE_TOKEN_TTL_DAYS', 90),
'frontend_url' => rtrim(env('FRONTEND_URL', env('APP_URL', 'http://localhost')), '/'),
- 'digital_card_logo_path' => env('DIGITAL_CARD_LOGO_PATH', public_path('images/logo/logo-kopkb.svg')),
+ 'digital_card_logo_path' => env('DIGITAL_CARD_LOGO_PATH', public_path('images/logo/logo.svg')),
];
diff --git a/be/app/Services/LetterService.php b/be/app/Services/LetterService.php
index ba27182..a7f9c81 100644
--- a/be/app/Services/LetterService.php
+++ b/be/app/Services/LetterService.php
@@ -56,11 +56,14 @@ class LetterService
public function prepareViewData(array $overrides = []): array
{
$logoPath = $overrides['logoPath'] ?? config('mail.logo_path');
+ $signaturePath = $overrides['signaturePath'] ?? config('letter.signature_path');
return array_merge([
'letterLayout' => LetterLayout::resolve(),
'logoPath' => $logoPath,
- 'logoUrl' => $this->resolveLogoDataUri($logoPath),
+ 'logoUrl' => $this->resolveImageDataUri($logoPath),
+ 'signaturePath' => $signaturePath,
+ 'signatureUrl' => $this->resolveImageDataUri($signaturePath),
'organizationAddress' => $overrides['organizationAddress'] ?? null,
'organizationContact' => $overrides['organizationContact'] ?? null,
], $overrides);
@@ -97,14 +100,14 @@ class LetterService
return $browsershot;
}
- protected function resolveLogoDataUri(?string $logoPath): ?string
+ protected function resolveImageDataUri(?string $path): ?string
{
- if (empty($logoPath) || ! file_exists($logoPath)) {
+ if (empty($path) || ! file_exists($path)) {
return null;
}
- $mime = mime_content_type($logoPath) ?: 'image/svg+xml';
+ $mime = mime_content_type($path) ?: 'image/svg+xml';
- return 'data:'.$mime.';base64,'.base64_encode(file_get_contents($logoPath));
+ return 'data:'.$mime.';base64,'.base64_encode(file_get_contents($path));
}
}
diff --git a/be/config/letter.php b/be/config/letter.php
index dad463b..4b232b4 100644
--- a/be/config/letter.php
+++ b/be/config/letter.php
@@ -82,4 +82,12 @@ return [
'black' => '#000000',
],
+ /*
+ |--------------------------------------------------------------------------
+ | Assets
+ |--------------------------------------------------------------------------
+ */
+
+ 'signature_path' => resource_path('assets/signatures/signature.svg'),
+
];
diff --git a/be/config/mail.php b/be/config/mail.php
index 86d1427..3b53d7f 100644
--- a/be/config/mail.php
+++ b/be/config/mail.php
@@ -124,6 +124,6 @@ return [
|
*/
- 'logo_path' => public_path('images/logo/logo-kopkb-letterhead.svg'),
+ 'logo_path' => public_path('images/logo/logo.svg'),
];
diff --git a/be/public/images/logo/logo-kopkb.svg b/be/public/images/logo/logo-kopkb.svg
deleted file mode 100644
index ef88ccb..0000000
--- a/be/public/images/logo/logo-kopkb.svg
+++ /dev/null
@@ -1,418 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/be/public/images/logo/logo.svg b/be/public/images/logo/logo.svg
new file mode 100644
index 0000000..5498014
--- /dev/null
+++ b/be/public/images/logo/logo.svg
@@ -0,0 +1,649 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/be/resources/assets/signatures/signature.svg b/be/resources/assets/signatures/signature.svg
new file mode 100644
index 0000000..aa1cf66
--- /dev/null
+++ b/be/resources/assets/signatures/signature.svg
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
diff --git a/be/resources/views/pdf/layouts/letter.blade.php b/be/resources/views/pdf/layouts/letter.blade.php
index e649a3a..35f34c6 100644
--- a/be/resources/views/pdf/layouts/letter.blade.php
+++ b/be/resources/views/pdf/layouts/letter.blade.php
@@ -239,6 +239,12 @@
height: var(--letter-signature-space-height);
}
+ .signature-space-img {
+ height: var(--letter-signature-space-height);
+ width: auto;
+ display: block;
+ }
+
.signatory-name {
font-weight: bold;
text-transform: uppercase;
diff --git a/be/resources/views/pdf/preview-approved-membership.blade.php b/be/resources/views/pdf/preview-approved-membership.blade.php
index fd2819b..620b66f 100644
--- a/be/resources/views/pdf/preview-approved-membership.blade.php
+++ b/be/resources/views/pdf/preview-approved-membership.blade.php
@@ -27,19 +27,19 @@
2. Sukacita dimaklumkan bahawa, permohonan tuan untuk menjadi anggota Koperasi
- Permodalan Kelantan Berhad(KoPKB) telah {{ membership_applications.board_result }} dalam {{ User manual fills before generate }}
+ Permodalan Kelantan Berhad(KoPKB) telah dalam
3. Pihak Koperasi akan membuat potongan gaji melalui majikan tuan sebagaiman ketetapan berikut:-
-