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

This commit is contained in:
ISMAIL MASSERAN
2026-07-06 11:08:32 +08:00
parent ac8f00175a
commit cc2492c3fc
23 changed files with 421 additions and 78 deletions
+1
View File
@@ -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<AuthRole & { permissions?: AuthPermission[] }>
}
+1
View File
@@ -0,0 +1 @@
export { dashboardLayoutRoutes } from './routes'
+9
View File
@@ -0,0 +1,9 @@
import type { Menu } from '@/core/types/menu'
export const dashboardMenu: Menu[] = [
{
icon: 'CircleGauge',
route_name: 'dashboard-overview',
title: 'Dashboard',
}
]
@@ -0,0 +1,552 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { today, getLocalTimeZone } from '@internationalized/date'
import phoneIllustration from '@/assets/images/phone-illustration.svg'
import womanIllustration from '@/assets/images/woman-illustration.svg'
import { completeOnboarding } from '@/modules/profile/services/profile.service'
import { useAuthStore } from '@/stores/auth'
import { Badge } from '@/components/ui/badge'
import { Box } from '@/components/ui/box'
import {
CarouselRoot,
CarouselPrevTrigger,
CarouselNextTrigger,
CarouselItemGroup,
CarouselItem,
} from '@/components/ui/carousel'
import {
DatePickerRoot,
DatePickerControl,
DatePickerInput,
DatePickerTrigger,
DatePickerPositioner,
DatePickerContent,
DatePickerYearSelect,
DatePickerMonthSelect,
DatePickerView,
DatePickerViewControl,
DatePickerPrevTrigger,
DatePickerNextTrigger,
DatePickerRangeText,
DatePickerTable,
DatePickerTableHead,
DatePickerTableRow,
DatePickerTableHeader,
DatePickerTableBody,
DatePickerTableCell,
DatePickerTableCellTrigger,
DatePickerContext,
} from '@/components/ui/datepicker'
import { DialogRoot, DialogContent, DialogCloseTrigger } from '@/components/ui/dialog'
import { Field, FieldLabel, FieldGroup } from '@/components/ui/field'
import { Input } from '@/components/ui/input'
import { Lucide } from '@/components/ui/lucide'
import {
MenuRoot,
MenuTrigger,
MenuPositioner,
MenuContent,
MenuCheckboxItem,
} from '@/components/ui/menu'
import { Line1, Line2, Pie1, Donut1, Donut2 } from '@/components/chart-presets'
import { Transactions } from '@/components/transactions'
import { RecentActivities } from '@/components/recent-activities'
import { DailyNotes } from '@/components/daily-notes'
import { Schedules } from '@/components/schedules'
import { OfficialStores } from '@/components/official-stores'
import { WeeklyBestSellers } from '@/components/weekly-best-sellers'
import { WeeklyTopProducts } from '@/components/weekly-top-products'
const authStore = useAuthStore()
const pc = ref(false)
const electronic = ref(false)
const smartphone = ref(false)
const photography = ref(false)
const sport = ref(false)
const onboardingDialog = ref(false)
const salesReportDate = ref<any[]>([
today(getLocalTimeZone()).subtract({ days: 7 }),
today(getLocalTimeZone()),
])
onMounted(() => {
onboardingDialog.value = !authStore.user?.onboarding_completed_at
})
async function markOnboardingComplete() {
if (authStore.user?.onboarding_completed_at) {
return
}
try {
const res = await completeOnboarding()
if (res.success) {
authStore.setUserProfile(res.data)
}
} catch {
// Will retry on next visit if the request failed
}
}
function handleOnboardingOpenChange(details: { open: boolean }) {
onboardingDialog.value = details.open
if (!details.open) {
void markOnboardingComplete()
}
}
</script>
<template>
<div>
<div class="grid grid-cols-12 gap-6">
<div class="col-span-12 2xl:col-span-9">
<div class="grid grid-cols-12 gap-6">
<!-- BEGIN: General Report -->
<div class="col-span-12">
<div class="flex h-10 items-center">
<h2 class="me-5 truncate text-lg font-medium">General Report</h2>
<a class="text-primary ms-auto flex items-center gap-3" href="">
<Lucide icon="RefreshCcw" /> Refresh
</a>
</div>
<div class="mt-5 grid grid-cols-12 gap-6">
<div class="col-span-12 sm:col-span-6 xl:col-span-3">
<Box raised="single">
<div class="flex">
<Lucide class="h-7 w-7 stroke-1 drop-shadow [--color:var(--color-primary)]" icon="CircleGauge" />
<div class="ms-auto">
<Badge variant="success" look="outline" content="12% Higher than last month">
12%
<Lucide class="ms-0.5" icon="ChevronUp" />
</Badge>
</div>
</div>
<div class="mt-6 text-2xl font-medium leading-8">$724,091.47</div>
<div class="mt-1.5 text-xs uppercase opacity-70">Item Sales</div>
</Box>
</div>
<div class="col-span-12 sm:col-span-6 xl:col-span-3">
<Box raised="single">
<div class="flex">
<Lucide class="h-7 w-7 stroke-1 [--color:var(--color-pending)]" icon="PanelBottomClose" />
<div class="ms-auto">
<Badge variant="success" look="outline" content="9% Higher than last month">
9%
<Lucide class="ms-0.5" icon="ChevronUp" />
</Badge>
</div>
</div>
<div class="mt-6 text-2xl font-medium leading-8">21,546</div>
<div class="mt-1.5 text-xs uppercase opacity-70">New Orders</div>
</Box>
</div>
<div class="col-span-12 sm:col-span-6 xl:col-span-3">
<Box raised="single">
<div class="flex">
<Lucide class="h-7 w-7 stroke-1 [--color:var(--color-warning)]" icon="Disc3" />
<div class="ms-auto">
<Badge variant="danger" look="outline" content="7% Lower than last month">
7%
<Lucide class="ms-0.5" icon="ChevronDown" />
</Badge>
</div>
</div>
<div class="mt-6 text-2xl font-medium leading-8">1,524,091</div>
<div class="mt-1.5 text-xs uppercase opacity-70">Total Products</div>
</Box>
</div>
<div class="col-span-12 sm:col-span-6 xl:col-span-3">
<Box raised="single">
<div class="flex">
<Lucide class="h-7 w-7 stroke-1 [--color:var(--color-danger)]" icon="Album" />
<div class="ms-auto">
<Badge variant="success" look="outline" content="41% Higher than last month">
41%
<Lucide class="ms-0.5" icon="ChevronUp" />
</Badge>
</div>
</div>
<div class="mt-6 text-2xl font-medium leading-8">42,924,091</div>
<div class="mt-1.5 text-xs uppercase opacity-70">Unique Visitors</div>
</Box>
</div>
</div>
</div>
<!-- END: General Report -->
<!-- BEGIN: Sales Report -->
<div class="col-span-12 mt-5 lg:col-span-6">
<div class="block h-10 items-center sm:flex">
<h2 class="me-5 truncate text-lg font-medium">Sales Report</h2>
<div class="relative mt-3 sm:ms-auto sm:mt-0">
<DatePickerRoot selection-mode="range" :num-of-months="2" :value="salesReportDate"
@value-change="(details) => (salesReportDate = details.value)">
<DatePickerControl>
<DatePickerInput :index="0" />
<DatePickerTrigger />
</DatePickerControl>
<DatePickerPositioner>
<DatePickerContent>
<DatePickerYearSelect />
<DatePickerMonthSelect />
<DatePickerViewControl>
<DatePickerPrevTrigger />
<DatePickerRangeText />
<DatePickerNextTrigger />
</DatePickerViewControl>
<DatePickerView view="day" class="flex-row">
<DatePickerContext v-slot="{ datePicker }">
<DatePickerTable>
<DatePickerTableHead>
<DatePickerTableRow>
<DatePickerTableHeader v-for="(weekDay, id) in datePicker?.weekDays" :key="id">
{{ weekDay.short }}
</DatePickerTableHeader>
</DatePickerTableRow>
</DatePickerTableHead>
<DatePickerTableBody>
<DatePickerTableRow v-for="(week, id) in datePicker?.weeks" :key="id">
<DatePickerTableCell v-for="(day, id) in week" :key="id" :value="day">
<DatePickerTableCellTrigger>
{{ day.day }}
</DatePickerTableCellTrigger>
</DatePickerTableCell>
</DatePickerTableRow>
</DatePickerTableBody>
</DatePickerTable>
</DatePickerContext>
<DatePickerContext v-slot="{ datePicker }">
<DatePickerTable>
<DatePickerTableHead>
<DatePickerTableRow>
<DatePickerTableHeader v-for="(weekDay, id) in datePicker?.weekDays" :key="id">
{{ weekDay.short }}
</DatePickerTableHeader>
</DatePickerTableRow>
</DatePickerTableHead>
<DatePickerTableBody>
<DatePickerTableRow v-for="(week, id) in datePicker?.getOffset({
months: 1,
}).weeks" :key="id">
<DatePickerTableCell v-for="(day, id) in week" :key="id" :value="day"
:visible-range="datePicker?.getOffset({ months: 1 }).visibleRange">
<DatePickerTableCellTrigger>
{{ day.day }}
</DatePickerTableCellTrigger>
</DatePickerTableCell>
</DatePickerTableRow>
</DatePickerTableBody>
</DatePickerTable>
</DatePickerContext>
</DatePickerView>
</DatePickerContent>
</DatePickerPositioner>
</DatePickerRoot>
</div>
</div>
<Box class="mt-12 p-5 sm:mt-5">
<div class="flex flex-col md:flex-row md:items-center">
<div class="flex">
<div>
<div class="text-lg font-medium">$24,100,21</div>
<div class="mt-1.5 text-xs uppercase opacity-70">This Month</div>
</div>
<div class="border-foreground/20 mx-4 h-12 w-px border border-r border-dotted xl:mx-6"></div>
<div class="text-foreground/80">
<div class="text-lg font-medium">$21,023,01</div>
<div class="mt-1.5 text-xs uppercase opacity-70">Last Month</div>
</div>
</div>
<MenuRoot class="w-auto mt-5 md:ms-auto md:mt-0">
<MenuTrigger>Filter by Category</MenuTrigger>
<MenuPositioner>
<MenuContent>
<MenuCheckboxItem :checked="pc" :onCheckedChange="(checked) => (pc = checked)" value="checked">
PC & Laptop
</MenuCheckboxItem>
<MenuCheckboxItem :checked="smartphone" :onCheckedChange="(checked) => (smartphone = checked)"
value="checked">
Smartphone
</MenuCheckboxItem>
<MenuCheckboxItem :checked="electronic" :onCheckedChange="(checked) => (electronic = checked)"
value="checked">
Electronic
</MenuCheckboxItem>
<MenuCheckboxItem :checked="photography" :onCheckedChange="(checked) => (photography = checked)"
value="checked">
Photography
</MenuCheckboxItem>
<MenuCheckboxItem :checked="sport" :onCheckedChange="(checked) => (sport = checked)"
value="checked">
Sport
</MenuCheckboxItem>
</MenuContent>
</MenuPositioner>
</MenuRoot>
</div>
<Line1 class="mt-6 h-[275px]" />
</Box>
</div>
<!-- END: Sales Report -->
<!-- BEGIN: Weekly Top Seller -->
<div class="col-span-12 mt-5 sm:col-span-6 lg:col-span-3">
<div class="flex h-10 items-center">
<h2 class="me-5 truncate text-lg font-medium">Weekly Top Seller</h2>
<a class="text-primary ms-auto truncate" href=""> Show More </a>
</div>
<Box class="mt-5 p-5">
<div class="mt-3">
<Pie1 class="h-[213px]" />
</div>
<div class="mx-auto mt-8 w-52 sm:w-auto">
<div class="flex items-center">
<div
class="bg-(--color)/20 border-(--color)/60 me-3 size-2 rounded-full border [--color:var(--color-primary)]">
</div>
<span class="truncate">17 - 30 Years old</span>
<span class="ms-auto">62%</span>
</div>
<div class="mt-4 flex items-center">
<div
class="bg-(--color)/20 border-(--color)/60 me-3 size-2 rounded-full border [--color:var(--color-pending)]">
</div>
<span class="truncate">31 - 50 Years old</span>
<span class="ms-auto">33%</span>
</div>
<div class="mt-4 flex items-center">
<div
class="bg-(--color)/20 border-(--color)/60 me-3 size-2 rounded-full border [--color:var(--color-warning)]">
</div>
<span class="truncate">&gt;= 50 Years old</span>
<span class="ms-auto">10%</span>
</div>
</div>
</Box>
</div>
<!-- END: Weekly Top Seller -->
<!-- BEGIN: Sales Report -->
<div class="col-span-12 mt-5 sm:col-span-6 lg:col-span-3">
<div class="flex h-10 items-center">
<h2 class="me-5 truncate text-lg font-medium">Sales Report</h2>
<a class="text-primary ms-auto truncate" href=""> Show More </a>
</div>
<Box class="mt-5 p-5">
<div class="mt-3">
<Donut1 class="h-[213px]" />
</div>
<div class="mx-auto mt-8 w-52 sm:w-auto">
<div class="flex items-center">
<div
class="bg-(--color)/20 border-(--color)/60 me-3 size-2 rounded-full border [--color:var(--color-primary)]">
</div>
<span class="truncate">17 - 30 Years old</span>
<span class="ms-auto">62%</span>
</div>
<div class="mt-4 flex items-center">
<div
class="bg-(--color)/20 border-(--color)/60 me-3 size-2 rounded-full border [--color:var(--color-pending)]">
</div>
<span class="truncate">31 - 50 Years old</span>
<span class="ms-auto">33%</span>
</div>
<div class="mt-4 flex items-center">
<div
class="bg-(--color)/20 border-(--color)/60 me-3 size-2 rounded-full border [--color:var(--color-warning)]">
</div>
<span class="truncate">&gt;= 50 Years old</span>
<span class="ms-auto">10%</span>
</div>
</div>
</Box>
</div>
<!-- END: Sales Report -->
<!-- BEGIN: Official Store -->
<div class="col-span-12 mt-6 xl:col-span-8">
<OfficialStores />
</div>
<!-- END: Official Store -->
<!-- BEGIN: Weekly Best Sellers -->
<div class="col-span-12 mt-6 xl:col-span-4">
<WeeklyBestSellers />
</div>
<!-- END: Weekly Best Sellers -->
<!-- BEGIN: General Report -->
<div class="col-span-12 mt-8 grid grid-cols-12 gap-6">
<div class="col-span-12 sm:col-span-6 2xl:col-span-3">
<Box class="p-5">
<div class="flex items-center">
<div class="w-2/4 flex-none">
<div class="truncate text-lg font-medium">Target Sales</div>
<div class="mt-1 opacity-70">300 Sales</div>
</div>
<div class="relative ms-auto flex-none">
<Donut2 class="w-[90px] h-[90px]" />
<div class="absolute inset-s-0 top-0 flex h-full w-full items-center justify-center font-medium">
20%
</div>
</div>
</div>
</Box>
</div>
<div class="col-span-12 sm:col-span-6 2xl:col-span-3">
<Box class="p-5">
<div class="flex items-center">
<div class="me-3 truncate text-lg font-medium">Social Media</div>
<Badge variant="secondary" look="outline" class="ms-auto whitespace-nowrap">137 Sales</Badge>
</div>
<div class="mt-1">
<Line2 class="-ms-1 h-[58px]" />
</div>
</Box>
</div>
<div class="col-span-12 sm:col-span-6 2xl:col-span-3">
<Box class="p-5">
<div class="flex items-center">
<div class="w-2/4 flex-none">
<div class="truncate text-lg font-medium">New Products</div>
<div class="mt-1 opacity-70">1450 Products</div>
</div>
<div class="relative ms-auto flex-none">
<Donut2 class="w-[90px] h-[90px]" />
<div class="absolute inset-s-0 top-0 flex h-full w-full items-center justify-center font-medium">
45%
</div>
</div>
</div>
</Box>
</div>
<div class="col-span-12 sm:col-span-6 2xl:col-span-3">
<Box class="p-5">
<div class="flex items-center">
<div class="me-3 truncate text-lg font-medium">Posted Ads</div>
<Badge variant="secondary" look="outline" class="ms-auto whitespace-nowrap">180 Campaign</Badge>
</div>
<div class="mt-1">
<Line2 class="-ms-1 h-[58px]" />
</div>
</Box>
</div>
</div>
<!-- END: General Report -->
<!-- BEGIN: Weekly Top Products -->
<div class="col-span-12 mt-6">
<WeeklyTopProducts />
</div>
<!-- END: Weekly Top Products -->
</div>
</div>
<div class="col-span-12 2xl:col-span-3">
<div class="-mb-10 h-full pb-10 2xl:border-l border-foreground/15">
<div class="grid grid-cols-12 gap-x-6 gap-y-6 2xl:gap-x-0 2xl:pl-6">
<!-- BEGIN: Transactions -->
<div class="col-span-12 md:col-span-6 xl:col-span-4 2xl:col-span-12">
<Transactions />
</div>
<!-- END: Transactions -->
<!-- BEGIN: Recent Activities -->
<div class="col-span-12 mt-3 md:col-span-6 xl:col-span-4 2xl:col-span-12">
<RecentActivities />
</div>
<!-- END: Recent Activities -->
<!-- BEGIN: Daily Notes -->
<div
class="col-span-12 mt-3 md:col-span-6 xl:col-span-12 xl:col-start-1 xl:row-start-1 2xl:col-start-auto 2xl:row-start-auto">
<DailyNotes />
</div>
<!-- END: Daily Notes -->
<!-- BEGIN: Schedules -->
<div
class="col-span-12 mt-3 md:col-span-6 xl:col-span-4 xl:col-start-1 xl:row-start-2 2xl:col-span-12 2xl:col-start-auto 2xl:row-start-auto">
<Schedules />
</div>
<!-- END: Schedules -->
</div>
</div>
</div>
</div>
<!-- BEGIN: Onboarding Dialog -->
<DialogRoot :open="onboardingDialog" @openChange="handleOnboardingOpenChange">
<DialogContent class="sm:max-w-xl">
<DialogCloseTrigger />
<div class="overflow-hidden">
<div class="-my-5 -mx-10">
<CarouselRoot :default-page="0" :slide-count="2" class="border-0">
<CarouselItemGroup>
<CarouselItem class="border-transparent bg-none bg-transparent" :index="0">
<div class="relative mx-3 flex flex-col items-center gap-1 px-3.5 pb-20">
<div
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">
<img class="absolute inset-0 mx-auto mt-10 w-2/5 scale-125" :src="phoneIllustration"
alt="MyKOPKB" />
</div>
<div class="px-8">
<div class="text-center text-xl font-medium">Selamat Datang ke Sistem MyKOPKB</div>
<div class="mt-3 text-center text-base leading-relaxed opacity-70">
Sistem MyKOPKB adalah sistem yang membantu pengguna untuk menguruskan permohonan keahlian KOPKB.
</div>
</div>
<div class="absolute inset-x-0 bottom-0 flex place-content-between px-5">
<a class="text-danger flex items-center gap-3 font-medium cursor-pointer"
@click.prevent="onboardingDialog = false">
Skip Intro
</a>
<CarouselNextTrigger class="text-primary flex items-center gap-3 font-medium me-0 px-5">
Next
<Lucide icon="MoveRight" />
</CarouselNextTrigger>
</div>
</div>
</CarouselItem>
<CarouselItem class="border-transparent bg-none bg-transparent" :index="1">
<div class="relative mx-3 flex flex-col items-center gap-1 px-3.5 pb-20">
<div
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">
<img class="absolute inset-0 mx-auto mt-10 w-2/5 scale-125" :src="womanIllustration"
alt="MyKOPKB" />
</div>
<div class="w-full">
<div class="text-center text-xl font-medium">Example Request Information</div>
<div class="mt-3 text-center text-base leading-relaxed opacity-70">
Your premium admin dashboard template.
</div>
<div class="mt-8">
<FieldGroup class="px-5">
<div class="grid grid-cols-2 gap-5">
<Field>
<FieldLabel>Full Name</FieldLabel>
<Input type="text" placeholder="John Doe" />
</Field>
<Field>
<FieldLabel>Event</FieldLabel>
<NativeSelect>
<NativeSelectOption>Corporate Event</NativeSelectOption>
<NativeSelectOption>Wedding</NativeSelectOption>
<NativeSelectOption>Birthday</NativeSelectOption>
<NativeSelectOption>Other</NativeSelectOption>
</NativeSelect>
</Field>
</div>
</FieldGroup>
</div>
</div>
<div class="absolute inset-x-0 bottom-0 flex place-content-between px-5">
<CarouselPrevTrigger class="text-primary flex items-center gap-3 font-medium ms-0 px-5">
<Lucide icon="MoveLeft" /> Previous
</CarouselPrevTrigger>
<a class="text-primary flex items-center gap-3 font-medium cursor-pointer me-0 px-5"
@click.prevent="onboardingDialog = false">
Selesai
<Lucide icon="MoveRight" />
</a>
</div>
</div>
</CarouselItem>
</CarouselItemGroup>
</CarouselRoot>
</div>
</div>
</DialogContent>
</DialogRoot>
<!-- END: Onboarding Dialog -->
</div>
</template>
@@ -0,0 +1,258 @@
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'
import { RouterLink, useRouter } from 'vue-router'
import phoneIllustration from '@/assets/images/phone-illustration.svg'
import womanIllustration from '@/assets/images/woman-illustration.svg'
import { AlertRoot, AlertTitle, AlertDescription } from '@/components/ui/alert'
import { Badge } from '@/components/ui/badge'
import { Box } from '@/components/ui/box'
import { Button } from '@/components/ui/button'
import {
CarouselRoot,
CarouselPrevTrigger,
CarouselNextTrigger,
CarouselItemGroup,
CarouselItem,
} from '@/components/ui/carousel'
import { DialogRoot, DialogContent, DialogCloseTrigger } from '@/components/ui/dialog'
import { Lucide, type Icon } from '@/components/ui/lucide'
import { usePermissions } from '@/composables/usePermissions'
import { completeOnboarding } from '@/modules/profile/services/profile.service'
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
const router = useRouter()
const { hasPermission } = usePermissions()
const onboardingDialog = ref(false)
const appName = import.meta.env.VITE_APP_NAME
const appVersion = import.meta.env.VITE_APP_VERSION
const greeting = computed(() => {
const hour = new Date().getHours()
if (hour < 12) return 'Selamat pagi'
if (hour < 18) return 'Selamat petang'
return 'Selamat malam'
})
const availableModules = computed(() => [
{
title: 'Profil',
description: 'Kemas kini maklumat peribadi, pekerjaan, bank dan waris.',
route: 'profile-overview-2',
icon: 'User' as Icon,
visible: true,
},
{
title: 'Permohonan Keahlian',
description: 'Urus dan semak permohonan keahlian KOPKB.',
route: 'list-membership-applications',
icon: 'ClipboardList' as Icon,
visible: hasPermission('lihat permohonan keahlian'),
},
{
title: 'Aktiviti',
description: 'Lihat dan urus aktiviti serta laporan berkaitan.',
route: 'list-activities',
icon: 'CalendarDays' as Icon,
visible: hasPermission('lihat aktiviti'),
},
{
title: 'Senarai Pengguna',
description: 'Pantau dan urus akaun pengguna sistem.',
route: 'list-users',
icon: 'Users' as Icon,
visible: hasPermission('lihat pengguna'),
},
].filter((module) => module.visible))
const upcomingFeatures = [
'Ringkasan statistik keahlian',
'Carta dan laporan bulanan',
'Notifikasi dan aktiviti terkini',
'Papan pemuka pentadbir',
]
onMounted(() => {
onboardingDialog.value = !authStore.user?.onboarding_completed_at
})
async function markOnboardingComplete() {
if (authStore.user?.onboarding_completed_at) {
return
}
try {
const res = await completeOnboarding()
if (res.success) {
authStore.setUserProfile(res.data)
}
} catch {
// Will retry on next visit if the request failed
}
}
async function finishOnboarding() {
onboardingDialog.value = false
await markOnboardingComplete()
await router.push({ name: 'profile-overview-2' })
}
function handleOnboardingOpenChange(details: { open: boolean }) {
onboardingDialog.value = details.open
if (!details.open) {
void markOnboardingComplete()
}
}
</script>
<template>
<div>
<AlertRoot class="mb-6" look="outline" variant="primary">
<Lucide class="mr-2 size-4 shrink-0" icon="Construction" />
<AlertTitle>Sistem Dalam Pembangunan</AlertTitle>
<AlertDescription>
{{ appName }} {{ appVersion }} masih dalam fasa pembangunan. Beberapa fungsi papan pemuka
belum tersedia. Sila gunakan modul sedia ada melalui menu sisi atau pautan di bawah.
</AlertDescription>
</AlertRoot>
<div class="grid grid-cols-12 gap-6">
<div class="col-span-12">
<Box class="relative overflow-hidden p-6 sm:p-8">
<div class="pointer-events-none absolute -right-10 -top-10 size-40 rounded-full bg-primary/10 blur-2xl" />
<div class="relative">
<Badge variant="pending" class="mb-4">Beta</Badge>
<h2 class="text-2xl font-semibold">
{{ greeting }}{{ authStore.userName ? `, ${authStore.userName}` : '' }}
</h2>
<p class="mt-2 max-w-2xl text-base leading-relaxed opacity-70">
Selamat datang ke {{ appName }}. Papan pemuka utama sedang dibangunkan buat masa ini,
anda boleh mula dengan mengemas kini profil atau mengurus permohonan keahlian.
</p>
</div>
</Box>
</div>
<div class="col-span-12 lg:col-span-7">
<div class="mb-4 flex h-10 items-center">
<h3 class="text-lg font-medium">Modul Tersedia</h3>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<Box v-for="module in availableModules" :key="module.route"
class="flex h-full flex-col p-5 transition-colors hover:bg-foreground/2">
<div class="mb-4 flex size-10 items-center justify-center rounded-xl bg-primary/10 text-primary">
<Lucide :icon="module.icon" class="size-5" />
</div>
<div class="text-base font-medium">{{ module.title }}</div>
<p class="mt-2 flex-1 text-sm leading-relaxed opacity-70">
{{ module.description }}
</p>
<Button as-child variant="ghost" class="mt-5 w-full border border-foreground/15">
<RouterLink :to="{ name: module.route }">
Pergi ke {{ module.title }}
<Lucide icon="ArrowRight" class="size-4" />
</RouterLink>
</Button>
</Box>
</div>
</div>
<div class="col-span-12 lg:col-span-5">
<div class="mb-4 flex h-10 items-center">
<h3 class="text-lg font-medium">Akan Datang</h3>
</div>
<Box class="p-5">
<p class="text-sm leading-relaxed opacity-70">
Ciri-ciri berikut sedang dirancang untuk papan pemuka ini:
</p>
<ul class="mt-4 space-y-3">
<li v-for="feature in upcomingFeatures" :key="feature" class="flex items-start gap-3 text-sm">
<Lucide icon="CircleDashed" class="mt-0.5 size-4 shrink-0 text-primary/70" />
<span>{{ feature }}</span>
</li>
</ul>
</Box>
<Box class="mt-4 p-5">
<div class="flex items-start gap-3">
<Lucide icon="Lightbulb" class="mt-0.5 size-4 shrink-0 text-warning" />
<div>
<div class="text-sm font-medium">Petua</div>
<p class="mt-1 text-sm leading-relaxed opacity-70">
Pastikan profil anda lengkap dan terkini. Maklumat yang
tepat membantu proses kelulusan berjalan lebih lancar.
</p>
</div>
</div>
</Box>
</div>
</div>
<!-- BEGIN: Onboarding Dialog -->
<DialogRoot :open="onboardingDialog" @openChange="handleOnboardingOpenChange">
<DialogContent class="sm:max-w-xl">
<DialogCloseTrigger />
<div class="overflow-hidden">
<div class="-my-5 -mx-10">
<CarouselRoot :default-page="0" :slide-count="2" class="border-0">
<CarouselItemGroup>
<CarouselItem class="border-transparent bg-none bg-transparent" :index="0">
<div class="relative mx-3 flex flex-col items-center gap-1 px-3.5 pb-20">
<div
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">
<img class="absolute inset-0 mx-auto mt-10 w-2/5 scale-125" :src="phoneIllustration"
alt="MyKOPKB" />
</div>
<div class="px-8">
<div class="text-center text-xl font-medium">Selamat Datang ke Sistem MyKOPKB</div>
<div class="mt-3 text-center text-base leading-relaxed opacity-70">
Sistem MyKOPKB adalah sistem integrasi bagi ahli Koperasi Permodalan Kelantan Berhad (KOPKB).
</div>
</div>
<div class="absolute inset-x-0 bottom-0 flex justify-end px-5">
<CarouselNextTrigger class="text-primary flex items-center gap-3 font-medium me-0 px-5">
Seterusnya
<Lucide icon="MoveRight" />
</CarouselNextTrigger>
</div>
</div>
</CarouselItem>
<CarouselItem class="border-transparent bg-none bg-transparent" :index="1">
<div class="relative mx-3 flex flex-col items-center gap-1 px-3.5 pb-20">
<div
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">
<img class="absolute inset-0 mx-auto mt-10 w-2/5 scale-125" :src="womanIllustration"
alt="MyKOPKB" />
</div>
<div class="px-8">
<div class="text-center text-xl font-medium">Mulakan dengan Profil Anda</div>
<div class="mt-3 text-center text-base leading-relaxed opacity-70">
Lengkapkan maklumat peribadi, pekerjaan dan butiran bank.
</div>
</div>
<div class="absolute inset-x-0 bottom-0 flex place-content-between px-5">
<CarouselPrevTrigger class="text-primary flex items-center gap-3 font-medium ms-0 px-5">
<Lucide icon="MoveLeft" /> Kembali
</CarouselPrevTrigger>
<a class="inline-flex items-center gap-2 rounded-lg bg-primary px-6 py-3 font-semibold text-white shadow-md transition-all duration-200 hover:bg-primary/90 hover:shadow-lg active:scale-95 cursor-pointer"
@click.prevent="finishOnboarding">
<span>Selesai</span>
<Lucide icon="Check" class="h-5 w-5" />
</a>
</div>
</div>
</CarouselItem>
</CarouselItemGroup>
</CarouselRoot>
</div>
</div>
</DialogContent>
</DialogRoot>
<!-- END: Onboarding Dialog -->
</div>
</template>
+10
View File
@@ -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' },
},
]
@@ -63,3 +63,8 @@ export async function updatePassword(payload: UpdatePasswordPayload): Promise<Up
const { data } = await api.put<UpdatePasswordResponse>('/v1/profile/password', payload)
return data
}
export async function completeOnboarding(): Promise<UpdateProfileResponse> {
const { data } = await api.post<UpdateProfileResponse>('/v1/profile/onboarding/complete')
return data
}