Files
My-KOPKB/fe/src/modules/profile/pages/ProfileOverview2.vue
T
ismailmasseran 421775d3ff
Build Docker Image / build-backend (push) Successful in 46s
Build Docker Image / build-frontend (push) Failing after 11s
Dev/v1.1 (#2)
Co-authored-by: ISMAIL MASSERAN <topaz@ISMAILs-Macbook.local>
Reviewed-on: #2
2026-07-02 10:09:08 +08:00

240 lines
10 KiB
Vue

<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'
import Swal from 'sweetalert2'
import fakers from '@/utils/faker'
import { Button } from '@/components/ui/button'
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'
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { SwitchRoot, SwitchControl } from '@/components/ui/switch'
import { ProgressRoot, ProgressTrack, ProgressRange } from '@/components/ui/progress-linear'
import {
CarouselRoot,
CarouselPrevTrigger,
CarouselNextTrigger,
CarouselItemGroup,
CarouselItem,
} from '@/components/ui/carousel'
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 { uploadProfileImage } from '@/modules/profile/services/profile.service'
import { useAuthStore } from '@/stores/auth'
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 uploadingImage = ref(false)
const imageInputRef = ref<HTMLInputElement | null>(null)
const imagePreviewUrl = ref<string | null>(null)
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 clearImagePreview() {
if (imagePreviewUrl.value) {
URL.revokeObjectURL(imagePreviewUrl.value)
imagePreviewUrl.value = null
}
if (imageInputRef.value) {
imageInputRef.value.value = ''
}
}
function openImagePicker() {
if (uploadingImage.value) return
imageInputRef.value?.click()
}
async function onImageSelected(event: Event) {
const input = event.target as HTMLInputElement
const file = input.files?.[0]
if (!file) return
clearImagePreview()
imagePreviewUrl.value = URL.createObjectURL(file)
uploadingImage.value = true
try {
const res = await uploadProfileImage(file)
if (!res.success) {
throw new Error(res.message ?? 'Gagal mengemas kini gambar profil.')
}
authStore.setUserProfile(res.data)
clearImagePreview()
await Swal.fire({
toast: true,
position: 'top-end',
icon: 'success',
title: 'Gambar profil berjaya dikemas kini.',
showConfirmButton: false,
timer: 3000,
})
} catch (error) {
clearImagePreview()
await Swal.fire({
icon: 'error',
title: 'Ralat',
text: getApiErrorMessage(error, 'Gagal mengemas kini gambar profil.'),
})
} finally {
uploadingImage.value = false
}
}
onMounted(async () => {
if (!authStore.user) {
await authStore.fetchSession()
}
})
</script>
<template>
<div>
<div class="flex items-center">
<h2 class="mr-auto text-lg font-medium">Profil</h2>
</div>
<TabsRoot defaultValue="1">
<!-- BEGIN: Profile Info -->
<Box raised="single" class="mt-5 p-0">
<div class="flex flex-col border-b border-foreground/15 p-5 lg:flex-row">
<div class="flex flex-1 items-center justify-center px-5 lg:justify-start">
<div class="relative" :class="{ 'opacity-60': uploadingImage }">
<AvatarRoot class="size-20 border-5 bg-background rounded-full sm:size-24 lg:size-32">
<AvatarFallback>{{ authStore.userName }}</AvatarFallback>
<AvatarImage v-if="avatarSrc" :src="avatarSrc" :alt="authStore.userName" />
</AvatarRoot>
<button type="button"
class="bg-(--color)/70 border-3 border-background absolute bottom-0 right-0 mb-1 mr-1 flex cursor-pointer items-center justify-center rounded-full p-2 text-white [--color:var(--color-primary)] disabled:cursor-not-allowed disabled:opacity-60"
:disabled="uploadingImage" aria-label="Tukar gambar profil" @click="openImagePicker">
<Lucide class="size-4" :icon="uploadingImage ? 'LoaderCircle' : 'Camera'"
:class="{ 'animate-spin': uploadingImage }" />
</button>
<input ref="imageInputRef" type="file" accept="image/jpeg,image/png,image/jpg,image/gif" class="hidden"
@change="onImageSelected" />
</div>
<div class="ml-5">
<div class="w-24 truncate text-lg font-medium sm:w-40 sm:whitespace-normal">
{{ authStore.userName || '-' }}
</div>
</div>
</div>
<div
class="mt-6 flex-1 border-t border-l border-r border-foreground/15 px-5 pt-5 lg:mt-0 lg:border-t-0 lg:pt-0">
<div class="text-center font-medium lg:mt-3 lg:text-left">Maklumat Hubungan</div>
<div class="mt-4 flex flex-col items-center justify-center lg:items-start">
<div class="flex items-center truncate sm:whitespace-normal">
<Lucide class="mr-2 size-4" icon="Mail" />
{{ displayValue(authStore.user?.email) }}
</div>
<div class="mt-3 flex items-center truncate sm:whitespace-normal">
<Lucide class="mr-2 size-4" icon="Phone" />
{{ displayValue(authStore.user?.phone_number) }}
</div>
<div class="mt-3 flex items-center truncate sm:whitespace-normal">
<Lucide class="mr-2 size-4" icon="IdCard" />
{{ displayValue(authStore.user?.ic_number) }}
</div>
</div>
</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="relative aspect-1.75/1 w-full max-w-68 overflow-hidden rounded-2xl bg-linear-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 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" /> 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>
</TabsList>
</div>
</Box>
<!-- Profile Info -->
<TabsContent value="1" class="mt-8">
<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">
<ChangePasswordTab embedded />
</TabsContent>
<!-- Pewaris -->
<TabsContent value="6" class="mt-8">
<HeirTab embedded />
</TabsContent>
</TabsRoot>
</div>
</template>