203 lines
7.6 KiB
Vue
203 lines
7.6 KiB
Vue
<script lang="ts" setup>
|
|
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'
|
|
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
|
import { Lucide } from '@/components/ui/lucide'
|
|
import { getApiErrorMessage } from '@/core/utils/getApiErrorMessage'
|
|
import { getUser } from '../services/user.service'
|
|
import type { UserDetail } from '../types/user.types'
|
|
import UserProfileTab from './UserProfileTab.vue'
|
|
import UserEmploymentTab from './UserEmploymentTab.vue'
|
|
import UserBankDetailTab from './UserBankDetailTab.vue'
|
|
|
|
const router = useRouter()
|
|
const route = useRoute()
|
|
|
|
const userId = computed(() => String(route.params.id ?? ''))
|
|
|
|
const loading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
const user = ref<UserDetail | null>(null)
|
|
|
|
const displayValue = (value: string | number | null | undefined) => {
|
|
if (value === null || value === undefined || value === '') return '-'
|
|
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_LABELS[status] ?? status.charAt(0).toUpperCase() + status.slice(1)
|
|
})
|
|
|
|
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()
|
|
if (!name) return '--'
|
|
return name.slice(0, 2).toUpperCase()
|
|
})
|
|
|
|
async function fetchUser() {
|
|
loading.value = true
|
|
error.value = null
|
|
|
|
try {
|
|
const response = await getUser(userId.value)
|
|
user.value = response.data
|
|
} catch (err) {
|
|
error.value = getApiErrorMessage(err, 'Gagal memuatkan profil pengguna.')
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
fetchUser()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<div class="flex flex-wrap items-center gap-3">
|
|
<h2 class="mr-auto text-lg font-medium">Profil {{ user?.name }} - {{ user?.member_number }}</h2>
|
|
<Button look="outline" variant="secondary" type="button" @click="router.push({ name: 'list-users' })">
|
|
Kembali
|
|
</Button>
|
|
</div>
|
|
|
|
<AlertRoot v-if="error" class="mt-5" variant="danger">
|
|
<AlertTitle>Ralat</AlertTitle>
|
|
<AlertDescription>{{ error }}</AlertDescription>
|
|
</AlertRoot>
|
|
|
|
<div v-if="loading" class="mt-5 opacity-70">Memuatkan profil...</div>
|
|
|
|
<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 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 min-w-0">
|
|
<div class="truncate text-lg font-medium sm:whitespace-normal">
|
|
{{ displayValue(user.name) }}
|
|
</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>
|
|
|
|
<!-- 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 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 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 shrink-0" icon="IdCard" />
|
|
{{ displayValue(user.ic_number) }}
|
|
</div>
|
|
</div>
|
|
|
|
<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="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>
|
|
</div>
|
|
|
|
<div class="px-5 py-4">
|
|
<TabsList class="mb-0 w-full flex justify-between">
|
|
<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/3 items-center justify-center" value="5">
|
|
<Lucide class="mr-2 size-4" icon="Briefcase" /> Pekerjaan
|
|
</TabsTrigger>
|
|
<TabsTrigger class="inline-flex w-1/3 items-center justify-center" value="3">
|
|
<Lucide class="mr-2 size-4" icon="Banknote" /> Bank
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
</div>
|
|
</Box>
|
|
|
|
<TabsContent value="1" class="mt-8">
|
|
<UserProfileTab :user="user" embedded />
|
|
</TabsContent>
|
|
<TabsContent value="5" class="mt-8">
|
|
<UserEmploymentTab :user="user" embedded />
|
|
</TabsContent>
|
|
<TabsContent value="3" class="mt-8">
|
|
<UserBankDetailTab :user="user" embedded />
|
|
</TabsContent>
|
|
</TabsRoot>
|
|
</div>
|
|
</template>
|