DONE: merge heir tab into profile tab, santize userIcNum and name, rename to malay, add stat card on user list

This commit is contained in:
ISMAIL MASSERAN
2026-07-09 12:48:26 +08:00
parent ebd3caecc6
commit af7f47bf71
30 changed files with 562 additions and 578 deletions
+75 -42
View File
@@ -2,6 +2,7 @@
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
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 { TabsRoot, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
@@ -13,7 +14,6 @@ import type { UserDetail } from '../types/user.types'
import UserProfileTab from './UserProfileTab.vue'
import UserEmploymentTab from './UserEmploymentTab.vue'
import UserBankDetailTab from './UserBankDetailTab.vue'
import UserHeirTab from './UserHeirTab.vue'
const router = useRouter()
const route = useRoute()
@@ -29,15 +29,41 @@ const displayValue = (value: string | number | null | undefined) => {
return String(value).trim() || '-'
}
const STATUS_LABELS: Record<string, string> = {
active: 'Aktif',
pending: 'Menunggu',
inactive: 'Tidak Aktif',
}
const statusLabel = computed(() => {
const status = user.value?.status
if (!status) return '-'
return status.charAt(0).toUpperCase() + status.slice(1)
return STATUS_LABELS[status] ?? status.charAt(0).toUpperCase() + status.slice(1)
})
const roleNames = computed(() =>
user.value?.roles?.map((role) => role.name).join(', ') || '-',
)
const statusBadgeVariant = computed(() => {
const status = user.value?.status
if (status === 'active') return 'success' as const
if (status === 'pending') return 'pending' as const
return 'secondary' as const
})
const companyName = computed(() => {
const employments = user.value?.employments ?? []
const currentEmployment = employments.find((employment) => employment.is_current)
return currentEmployment?.company_name ?? employments[0]?.company_name ?? null
})
function formatDateLabel(value: string | null | undefined): string {
if (!value) return '-'
const date = new Date(value)
if (Number.isNaN(date.getTime())) return value
return new Intl.DateTimeFormat('ms-MY', {
day: 'numeric',
month: 'short',
year: 'numeric',
}).format(date)
}
const avatarFallback = computed(() => {
const name = user.value?.name?.trim()
@@ -82,53 +108,66 @@ onMounted(() => {
<TabsRoot v-else-if="user" defaultValue="1">
<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="flex flex-col border-b border-foreground/15 lg:flex-row">
<!-- Identity -->
<div class="flex flex-1 items-center justify-center p-5 lg:justify-start">
<AvatarRoot class="size-20 border-5 bg-background rounded-full sm:size-24 lg:size-32">
<AvatarFallback>{{ avatarFallback }}</AvatarFallback>
<AvatarImage v-if="user.image_url" :src="user.image_url" :alt="user.name" />
</AvatarRoot>
<div class="ml-5">
<div class="w-24 truncate text-lg font-medium sm:w-40 sm:whitespace-normal">
<div class="ml-5 min-w-0">
<div class="truncate text-lg font-medium sm:whitespace-normal">
{{ displayValue(user.name) }}
</div>
<div class="opacity-70">{{ roleNames }}</div>
<div v-if="user.member_type"
class="mt-1 truncate text-sm capitalize opacity-70 sm:whitespace-normal">
{{ displayValue(user.member_type) }}
</div>
<div class="mt-3 flex flex-wrap items-center gap-2">
<Badge :variant="statusBadgeVariant">{{ statusLabel }}</Badge>
<Badge v-if="user.member_number" look="outline" variant="secondary">
No. {{ user.member_number }}
</Badge>
</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">
<!-- Contact & membership -->
<div class="flex-1 border-t border-foreground/15 p-5 lg:border-t-0 lg:border-l">
<div class="text-center font-medium lg:text-left">Maklumat Hubungan</div>
<div class="mt-4 flex flex-col items-center lg:items-start">
<div class="flex items-center truncate sm:whitespace-normal">
<Lucide class="mr-2 size-4" icon="Mail" />
<Lucide class="mr-2 size-4 shrink-0" icon="Mail" />
{{ displayValue(user.email) }}
</div>
<div class="mt-3 flex items-center truncate sm:whitespace-normal">
<Lucide class="mr-2 size-4" icon="Phone" />
<Lucide class="mr-2 size-4 shrink-0" icon="Phone" />
{{ displayValue(user.phone_number) }}
</div>
<div class="mt-3 flex items-center truncate sm:whitespace-normal">
<Lucide class="mr-2 size-4" icon="IdCard" />
<Lucide class="mr-2 size-4 shrink-0" icon="IdCard" />
{{ displayValue(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="grid grid-cols-3 gap-5">
<div class="text-center">
<div class="truncate text-xl font-medium">{{ user.roles?.length ?? 0 }}</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="truncate text-xl font-medium capitalize">
{{ displayValue(user.member_type) }}
<div class="mt-6 grid grid-cols-2 gap-4 sm:grid-cols-3">
<div class="text-center lg:text-left">
<div class="truncate text-base font-medium">
{{ displayValue(user.position) }}
</div>
<div class="opacity-70">Jenis Anggota</div>
<div class="text-xs opacity-70">Jawatan</div>
</div>
<div class="text-center lg:text-left">
<div class="truncate text-base font-medium">
{{ displayValue(companyName) }}
</div>
<div class="text-xs opacity-70">Unit</div>
</div>
<div class="col-span-2 text-center sm:col-span-1 lg:text-left">
<div class="truncate text-base font-medium">
{{ formatDateLabel(user.join_date) }}
</div>
<div class="text-xs opacity-70">Tarikh Sertai</div>
</div>
</div>
</div>
@@ -136,18 +175,15 @@ onMounted(() => {
<div class="px-5 py-4">
<TabsList class="mb-0 w-full flex justify-between">
<TabsTrigger class="inline-flex w-1/4 items-center justify-center" value="1">
<TabsTrigger class="inline-flex w-1/3 items-center justify-center" value="1">
<Lucide class="mr-2 size-4" icon="User" /> Profil
</TabsTrigger>
<TabsTrigger class="inline-flex w-1/4 items-center justify-center" value="5">
<TabsTrigger class="inline-flex w-1/3 items-center justify-center" value="5">
<Lucide class="mr-2 size-4" icon="Briefcase" /> Pekerjaan
</TabsTrigger>
<TabsTrigger class="inline-flex w-1/4 items-center justify-center" value="3">
<TabsTrigger class="inline-flex w-1/3 items-center justify-center" value="3">
<Lucide class="mr-2 size-4" icon="Banknote" /> Bank
</TabsTrigger>
<TabsTrigger class="inline-flex w-1/4 items-center justify-center" value="6">
<Lucide class="mr-2 size-4" icon="Users" /> Penama
</TabsTrigger>
</TabsList>
</div>
</Box>
@@ -161,9 +197,6 @@ onMounted(() => {
<TabsContent value="3" class="mt-8">
<UserBankDetailTab :user="user" embedded />
</TabsContent>
<TabsContent value="6" class="mt-8">
<UserHeirTab :user="user" embedded />
</TabsContent>
</TabsRoot>
</div>
</template>