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',