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)) -
- {{ config('app.name') }} -
-@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)) -
- {{ config('app.name') }} -
-@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 @@
Dengan hormatnya,
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 @@ - - -
- - - - -
- 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 @@ - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - 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 - 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 }} @@ -129,87 +177,103 @@ 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) }}
-
-
-