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)) -
- {{ config('app.name') }} -
-@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/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 @@