DONE: layout letter with letterhead and footer, notification for newly...
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import Swal from 'sweetalert2'
|
||||
import fakers from '@/utils/faker'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Box } from '@/components/ui/box'
|
||||
import { TabsRoot, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
|
||||
import { MenuRoot, MenuTrigger, MenuPositioner, MenuContent, MenuItem } from '@/components/ui/menu'
|
||||
@@ -17,51 +16,34 @@ import {
|
||||
CarouselItemGroup,
|
||||
CarouselItem,
|
||||
} from '@/components/ui/carousel'
|
||||
import { Field, FieldGroup, FieldLabel } from '@/components/ui/field'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Lucide } from '@/components/ui/lucide'
|
||||
import { FileIcon } from '@/components/ui/file-icon'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import logoUrl from '@/assets/images/logo-kopkb.svg'
|
||||
import { getApiErrorMessage } from '@/core/utils/getApiErrorMessage'
|
||||
import { updateProfile, uploadProfileImage } from '@/modules/profile/services/profile.service'
|
||||
import { uploadProfileImage } from '@/modules/profile/services/profile.service'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import ChangePassword from './ChangePassword.vue'
|
||||
import ProfileTab from './ProfileTab.vue'
|
||||
import EmploymentTab from './EmploymentTab.vue'
|
||||
import BankDetailTab from './BankDetailTab.vue'
|
||||
import HeirTab from './HeirTab.vue'
|
||||
import ChangePasswordTab from './ChangePasswordTab.vue'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const saving = ref(false)
|
||||
const uploadingImage = ref(false)
|
||||
const imageInputRef = ref<HTMLInputElement | null>(null)
|
||||
const imagePreviewUrl = ref<string | null>(null)
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
ic_number: '',
|
||||
position: '',
|
||||
phone_number: '',
|
||||
})
|
||||
|
||||
const displayValue = (value: string | null | undefined) => value?.trim() || '-'
|
||||
|
||||
const statusLabel = computed(() => {
|
||||
const status = authStore.user?.status
|
||||
if (!status) return '-'
|
||||
return status.charAt(0).toUpperCase() + status.slice(1)
|
||||
})
|
||||
const displayValue = (value: string | number | null | undefined) => {
|
||||
if (value === null || value === undefined || value === '') return '-'
|
||||
return String(value).trim() || '-'
|
||||
}
|
||||
|
||||
const avatarSrc = computed(
|
||||
() => imagePreviewUrl.value ?? authStore.userImageUrl ?? undefined,
|
||||
)
|
||||
|
||||
function syncFormFromUser() {
|
||||
const user = authStore.user
|
||||
if (!user) return
|
||||
|
||||
form.name = user.name ?? ''
|
||||
form.ic_number = user.ic_number ?? ''
|
||||
form.position = user.position ?? ''
|
||||
form.phone_number = user.phone_number ?? ''
|
||||
}
|
||||
|
||||
function clearImagePreview() {
|
||||
if (imagePreviewUrl.value) {
|
||||
URL.revokeObjectURL(imagePreviewUrl.value)
|
||||
@@ -100,7 +82,7 @@ async function onImageSelected(event: Event) {
|
||||
toast: true,
|
||||
position: 'top-end',
|
||||
icon: 'success',
|
||||
title: res.message || 'Gambar profil berjaya dikemas kini.',
|
||||
title: 'Gambar profil berjaya dikemas kini.',
|
||||
showConfirmButton: false,
|
||||
timer: 3000,
|
||||
})
|
||||
@@ -116,50 +98,10 @@ async function onImageSelected(event: Event) {
|
||||
}
|
||||
}
|
||||
|
||||
async function onSaveProfile() {
|
||||
saving.value = true
|
||||
|
||||
try {
|
||||
const res = await updateProfile({
|
||||
name: form.name.trim(),
|
||||
ic_number: form.ic_number.trim(),
|
||||
position: form.position.trim(),
|
||||
phone_number: form.phone_number.trim(),
|
||||
})
|
||||
|
||||
if (!res.success) {
|
||||
throw new Error(res.message ?? 'Gagal mengemas kini profil.')
|
||||
}
|
||||
|
||||
authStore.setUserProfile(res.data)
|
||||
syncFormFromUser()
|
||||
|
||||
await Swal.fire({
|
||||
toast: true,
|
||||
position: 'top-end',
|
||||
icon: 'success',
|
||||
title: res.message || 'Profil berjaya dikemas kini.',
|
||||
showConfirmButton: false,
|
||||
timer: 3000,
|
||||
})
|
||||
} catch (error) {
|
||||
await Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Ralat',
|
||||
text: getApiErrorMessage(error, 'Gagal mengemas kini profil.'),
|
||||
})
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => authStore.user, syncFormFromUser, { immediate: true })
|
||||
|
||||
onMounted(async () => {
|
||||
if (!authStore.user) {
|
||||
await authStore.fetchSession()
|
||||
}
|
||||
syncFormFromUser()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -191,7 +133,6 @@ onMounted(async () => {
|
||||
<div class="w-24 truncate text-lg font-medium sm:w-40 sm:whitespace-normal">
|
||||
{{ authStore.userName || '-' }}
|
||||
</div>
|
||||
<div class="opacity-70">{{ authStore.userActiveRole }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -214,87 +155,90 @@ onMounted(async () => {
|
||||
</div>
|
||||
<div
|
||||
class="mt-6 flex flex-1 items-center justify-center border-t border-foreground/15 px-5 pt-5 lg:mt-0 lg:border-0 lg:pt-0">
|
||||
<div class="grid grid-cols-3 gap-5">
|
||||
<div class="text-center">
|
||||
<div class="text-xl font-medium">{{ displayValue(authStore.activeRole?.name) }}</div>
|
||||
<div class="opacity-70">Peranan</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-xl font-medium">{{ statusLabel }}</div>
|
||||
<div class="opacity-70">Status</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-xl font-medium capitalize">{{ displayValue(authStore.activeRole?.context) }}</div>
|
||||
<div class="opacity-70">Konteks</div>
|
||||
<div
|
||||
class="relative aspect-[1.75/1] w-full max-w-[17rem] overflow-hidden rounded-2xl bg-gradient-to-br from-primary via-primary/95 to-primary/75 p-4 text-primary-foreground shadow-lg ring-1 ring-white/20 sm:max-w-xs sm:p-5"
|
||||
role="img"
|
||||
aria-label="Kad digital anggota">
|
||||
<div class="pointer-events-none absolute inset-0 bg-noise opacity-30" />
|
||||
<div class="pointer-events-none absolute -right-10 -top-10 size-36 rounded-full bg-white/10" />
|
||||
<div class="pointer-events-none absolute -bottom-12 -left-8 size-40 rounded-full bg-white/5" />
|
||||
<div
|
||||
class="pointer-events-none absolute right-4 top-1/2 size-10 -translate-y-1/2 rounded-md border border-white/20 bg-white/10" />
|
||||
|
||||
<div class="relative flex h-full flex-col justify-between">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<img :src="logoUrl" alt="" class="h-7 w-auto brightness-0 invert sm:h-8" />
|
||||
<div class="text-right text-[10px] font-semibold uppercase tracking-[0.2em] opacity-80">
|
||||
Kad Digital
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-[10px] font-medium uppercase tracking-widest opacity-70">No. Anggota</div>
|
||||
<div class="mt-1 font-mono text-2xl font-semibold tracking-[0.15em] sm:text-3xl">
|
||||
{{ displayValue(authStore.userMemberNumber) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-end justify-between gap-3 border-t border-white/15 pt-3">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate text-sm font-medium">{{ authStore.userName || '-' }}</div>
|
||||
<div class="mt-0.5 text-[10px] uppercase tracking-wide opacity-60">Nama</div>
|
||||
</div>
|
||||
<div class="shrink-0 text-right">
|
||||
<div class="text-sm font-semibold">{{ displayValue(authStore.userMemberType) }}</div>
|
||||
<div class="mt-0.5 text-[10px] uppercase tracking-wide opacity-60">Jenis Anggota</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tabs title -->
|
||||
<div class="px-5 py-4">
|
||||
<TabsList class="w-full mb-0">
|
||||
<TabsList class="w-full mb-0 flex justify-between">
|
||||
<TabsTrigger class="w-1/4 inline-flex items-center justify-center" value="1">
|
||||
<Lucide class="mr-2 size-4" icon="User" /> Profil
|
||||
</TabsTrigger>
|
||||
<TabsTrigger class="w-1/4 inline-flex items-center justify-center" value="5">
|
||||
<Lucide class="mr-2 size-4" icon="Briefcase" /> Pekerjaan
|
||||
</TabsTrigger>
|
||||
<TabsTrigger class="w-1/4 inline-flex items-center justify-center" value="3">
|
||||
<Lucide class="mr-2 size-4" icon="Banknote" /> Maklumat Bank
|
||||
<Lucide class="mr-2 size-4" icon="Banknote" /> Bank
|
||||
</TabsTrigger>
|
||||
<TabsTrigger class="w-1/4 inline-flex items-center justify-center" value="6">
|
||||
<Lucide class="mr-2 size-4" icon="Users" /> Pewaris
|
||||
</TabsTrigger>
|
||||
<TabsTrigger class="w-1/4 inline-flex items-center justify-center" value="2">
|
||||
<Lucide class="mr-2 size-4" icon="Lock" /> Kata Laluan
|
||||
</TabsTrigger>
|
||||
<TabsTrigger class="w-1/4 inline-flex items-center justify-center" value="4">
|
||||
<Lucide class="mr-2 size-4" icon="Server" /> Perkhidmatan
|
||||
<Lucide class="mr-2 size-4" icon="MoreHorizontal" /> Lain-lain
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
</Box>
|
||||
<!-- Profile Info -->
|
||||
<TabsContent value="1" class="mt-8">
|
||||
<Box raised="single" class="p-6">
|
||||
<form class="space-y-6" @submit.prevent="onSaveProfile">
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-slate-900">Maklumat Peribadi</h3>
|
||||
<p class="mt-1 text-sm text-slate-500">
|
||||
Kemas kini maklumat peribadi anda.
|
||||
</p>
|
||||
</div>
|
||||
<Button type="submit" variant="primary" :disabled="saving">
|
||||
{{ saving ? 'Menyimpan...' : 'Simpan' }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<FieldGroup>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<Field>
|
||||
<FieldLabel for="profile-name">Nama</FieldLabel>
|
||||
<Input id="profile-name" v-model="form.name" type="text" placeholder="Nama penuh" required />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="profile-email">E-mel</FieldLabel>
|
||||
<Input id="profile-email" :model-value="authStore.user?.email ?? ''" type="email" disabled />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="profile-ic">No. Kad Pengenalan</FieldLabel>
|
||||
<Input id="profile-ic" v-model="form.ic_number" type="text" placeholder="No. kad pengenalan" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="profile-phone">No. Telefon</FieldLabel>
|
||||
<Input id="profile-phone" v-model="form.phone_number" type="text" placeholder="No. telefon" />
|
||||
</Field>
|
||||
<Field class="md:col-span-2">
|
||||
<FieldLabel for="profile-position">Jawatan</FieldLabel>
|
||||
<Input id="profile-position" v-model="form.position" type="text" placeholder="Jawatan" />
|
||||
</Field>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</Box>
|
||||
<ProfileTab embedded />
|
||||
</TabsContent>
|
||||
<!-- Pekerjaan -->
|
||||
<TabsContent value="5" class="mt-8">
|
||||
<EmploymentTab embedded />
|
||||
</TabsContent>
|
||||
<!-- Bank -->
|
||||
<TabsContent value="3" class="mt-8">
|
||||
<BankDetailTab embedded />
|
||||
</TabsContent>
|
||||
<!-- Password Change -->
|
||||
<TabsContent value="2" class="mt-8">
|
||||
<ChangePassword embedded />
|
||||
<ChangePasswordTab embedded />
|
||||
</TabsContent>
|
||||
<!-- Maklumat Bank -->
|
||||
<!-- Pewaris -->
|
||||
<TabsContent value="6" class="mt-8">
|
||||
<HeirTab embedded />
|
||||
</TabsContent>
|
||||
<!-- Perkhidmatan -->
|
||||
<TabsContent value="4" class="mt-8">
|
||||
<div class="grid grid-cols-12 gap-x-6 gap-y-8">
|
||||
<!-- BEGIN: Latest Uploads -->
|
||||
@@ -627,7 +571,7 @@ onMounted(async () => {
|
||||
class="flex w-full items-center justify-center border-b border-foreground/15 pb-5 sm:w-auto sm:justify-start sm:border-b-0 sm:pb-0">
|
||||
<Badge look="outline" class="mr-3 px-3 py-2">{{
|
||||
faker['dates'][0]
|
||||
}}</Badge>
|
||||
}}</Badge>
|
||||
<div class="opacity-70">Date of Release</div>
|
||||
</div>
|
||||
<div class="mt-5 flex sm:ml-auto sm:mt-0">
|
||||
|
||||
Reference in New Issue
Block a user