DONE: add bos signature on letter, remove unused files or templates files, update logo to mykopkb, remove theme switcher use gradient colour, add external system module on frontend

This commit is contained in:
ISMAIL MASSERAN
2026-07-06 12:50:14 +08:00
parent cc2492c3fc
commit 501545959f
93 changed files with 2187 additions and 18091 deletions
@@ -0,0 +1,74 @@
import { onMounted, ref, watch } from 'vue'
import debounce from 'lodash/debounce'
import { useApiPagination } from '@/composables/useApiPagination'
import { getApiErrorMessage } from '@/core/utils/getApiErrorMessage'
import { listActivityLogs } from '../services/activity-log.service'
import type { ActivityLogItem } from '../types/activity-log.types'
export function useActivityLogList() {
const activityLogs = ref<ActivityLogItem[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
const search = ref('')
const page = ref(1)
const itemsPerPage = ref(10)
const { pagination, applyPagination } = useApiPagination({ per_page: 10 })
async function fetchActivityLogs(requestPage = page.value) {
loading.value = true
error.value = null
try {
const data = await listActivityLogs({
page: requestPage,
per_page: itemsPerPage.value,
search: search.value.trim() || undefined,
})
activityLogs.value = data.data
applyPagination(data.pagination)
page.value = data.pagination.current_page
} catch (err) {
error.value = getApiErrorMessage(err, 'Gagal memuatkan log aktiviti.')
activityLogs.value = []
} finally {
loading.value = false
}
}
const debouncedSearch = debounce(() => {
fetchActivityLogs(1)
}, 400)
watch(search, () => {
debouncedSearch()
})
watch(page, (nextPage, previousPage) => {
if (nextPage !== previousPage) {
fetchActivityLogs(nextPage)
}
})
watch(itemsPerPage, (nextValue, previousValue) => {
if (nextValue !== previousValue) {
fetchActivityLogs(1)
}
})
onMounted(() => {
fetchActivityLogs(1)
})
return {
activityLogs,
loading,
error,
search,
page,
itemsPerPage,
pagination,
fetchActivityLogs,
}
}
+2
View File
@@ -0,0 +1,2 @@
export { activityLogLayoutRoutes } from './routes'
export { activityLogMenu } from './menu'
+10
View File
@@ -0,0 +1,10 @@
import type { Menu } from '@/core/types/menu'
export const activityLogMenu: Menu[] = [
{
icon: 'ScrollText',
route_name: 'list-activity-logs',
title: 'Log Aktiviti',
permission: 'lihat log aktiviti',
},
]
@@ -0,0 +1,105 @@
<script lang="ts" setup>
import dayjs from 'dayjs'
import { Search } from '@lucide/vue'
import { AlertRoot, AlertTitle, AlertDescription } from '@/components/ui/alert'
import { Input } from '@/components/ui/input'
import DataTable from '@/components/ui/usage/DataTable.vue'
import type { TableHeader } from '@/components/ui/usage/DataTable.vue'
import { useActivityLogList } from '../composables/useActivityLogList'
import { formatModelType } from '../utils/activity-log.utils'
const {
activityLogs,
loading,
error,
search,
page,
itemsPerPage,
pagination,
} = useActivityLogList()
function formatDateTime(value: string | null | undefined): string {
if (!value) return '-'
return dayjs(value).format('DD MMM YYYY, HH:mm')
}
function formatCauserName(item: { causer?: { name?: string } | null }): string {
return item.causer?.name ?? '-'
}
const headers: TableHeader[] = [
{ title: 'Bil.', key: '#', sortable: false },
{
title: 'Tarikh',
key: 'created_at',
sortable: false,
exportValue: (item) => formatDateTime(item.created_at),
},
{
title: 'Pengguna',
key: 'causer.name',
sortable: false,
exportValue: (item) => formatCauserName(item),
},
{
title: 'Emel',
key: 'causer.email',
sortable: false,
exportValue: (item) => item.causer?.email ?? '-',
},
{ title: 'Keterangan', key: 'description', sortable: false },
{
title: 'Subjek',
key: 'subject_type',
sortable: false,
exportValue: (item) => formatModelType(item.subject_type),
},
{ title: 'Peristiwa', key: 'event', sortable: false },
]
</script>
<template>
<div class="w-full space-y-6">
<div>
<h2 class="text-lg font-medium">Log Aktiviti</h2>
<p class="mt-1 text-sm opacity-70">Semak rekod aktiviti pengguna dalam sistem.</p>
</div>
<AlertRoot v-if="error" variant="danger">
<AlertTitle>Error</AlertTitle>
<AlertDescription>{{ error }}</AlertDescription>
</AlertRoot>
<DataTable :headers="headers" :items="activityLogs" :loading="loading" :pagination="pagination" show-pagination
exportable export-file-name="activity-logs" v-model:page="page" v-model:items-per-page="itemsPerPage">
<template #toolbar>
<div class="relative w-full max-w-md flex-1">
<Search class="pointer-events-none absolute top-1/2 left-3 z-10 size-4 -translate-y-1/2 text-foreground/50"
aria-hidden="true" />
<Input v-model="search" type="search" placeholder="Cari keterangan, subjek, peristiwa, pengguna..."
class="w-full pl-9" aria-label="Cari log aktiviti" />
</div>
</template>
<template #item.created_at="{ item }">
{{ formatDateTime(item.created_at) }}
</template>
<template #item.causer.name="{ item }">
{{ formatCauserName(item) }}
</template>
<template #item.causer.email="{ item }">
<span class="lowercase">{{ item.causer?.email ?? '-' }}</span>
</template>
<template #item.subject_type="{ item }">
{{ formatModelType(item.subject_type) }}
</template>
<template #item.event="{ item }">
{{ item.event ?? '-' }}
</template>
</DataTable>
</div>
</template>
+14
View File
@@ -0,0 +1,14 @@
import type { RouteRecordRaw } from 'vue-router'
export const activityLogLayoutRoutes: RouteRecordRaw[] = [
{
path: 'activity-logs',
name: 'list-activity-logs',
component: () => import('./pages/ActivityLogList.vue'),
meta: {
title: 'Log Aktiviti',
module: 'activity-log',
permission: 'lihat log aktiviti',
},
},
]
@@ -0,0 +1,17 @@
import { api } from '@/core/services/api'
import type { PaginatedApiResponse } from '@/core/types/api'
import type { ActivityLogItem, ListActivityLogsParams } from '../types/activity-log.types'
export async function listActivityLogs(
params: ListActivityLogsParams,
): Promise<PaginatedApiResponse<ActivityLogItem>> {
const { data } = await api.get<PaginatedApiResponse<ActivityLogItem>>('/v1/activitylogs', {
params,
})
if (!data.success) {
throw new Error(data.message ?? 'Failed to load activity logs')
}
return data
}
@@ -0,0 +1,26 @@
export type ActivityLogCauser = {
id: string
name: string
email: string
}
export type ActivityLogItem = {
id: number
log_name: string | null
description: string
subject_type: string | null
subject_id: string | null
event: string | null
causer_type: string | null
causer_id: string | null
properties: Record<string, unknown> | null
created_at: string
updated_at: string
causer: ActivityLogCauser | null
}
export type ListActivityLogsParams = {
page?: number
per_page?: number
search?: string
}
@@ -0,0 +1,6 @@
export function formatModelType(value: string | null | undefined): string {
if (!value) return '-'
const segments = value.split('\\')
return segments[segments.length - 1] || value
}
@@ -11,14 +11,14 @@ const sections = [
id: 'pengenalan',
title: '1. Pengenalan',
placeholder: `
Terma dan Syarat ini mengawal penggunaan platform, laman web, dan perkhidmatan yang disediakan oleh Kementerian Pertahanan Malaysia (MINDEF). Dengan mengakses atau menggunakan perkhidmatan kami, anda dianggap telah membaca, memahami, dan bersetuju untuk terikat dengan terma ini. Jika anda tidak bersetuju dengan mana-mana bahagian terma ini, sila hentikan penggunaan perkhidmatan kami.
Terma dan Syarat ini mengawal penggunaan platform, laman web, dan perkhidmatan yang disediakan oleh Koperasi Permodalan Kelantan Berhad (KOPKB). Dengan mengakses atau menggunakan perkhidmatan kami, anda dianggap telah membaca, memahami, dan bersetuju untuk terikat dengan terma ini. Jika anda tidak bersetuju dengan mana-mana bahagian terma ini, sila hentikan penggunaan perkhidmatan kami.
`,
},
{
id: 'akaun',
title: '2. Akaun & Kelayakan',
placeholder: `
Anda mesti berumur sekurang-kurangnya 18 tahun dan merupakan tentera. Anda bertanggungjawab memastikan maklumat akaun sentiasa tepat dan terkini. Anda juga bertanggungjawab menjaga kerahsiaan kata laluan serta segala aktiviti yang berlaku di bawah akaun anda.
Anda mesti berumur sekurang-kurangnya 18 tahun dan merupakan ahli KOPKB. Anda bertanggungjawab memastikan maklumat akaun sentiasa tepat dan terkini. Anda juga bertanggungjawab menjaga kerahsiaan kata laluan serta segala aktiviti yang berlaku di bawah akaun anda.
`,
},
{
@@ -39,7 +39,7 @@ Sesetengah perkhidmatan mungkin tertakluk kepada bayaran yang dinyatakan semasa
id: 'kandungan',
title: '5. Kandungan & Hak Milik',
placeholder: `
Semua kandungan, reka bentuk, logo, teks, grafik, dan bahan lain yang terdapat pada platform ini adalah hak milik Kementerian Pertahanan Malaysia (MINDEF) atau pemberi lesennya. Anda diberikan lesen terhad untuk menggunakan kandungan tersebut bagi tujuan penggunaan peribadi dan bukan komersial sahaja. Sebarang penyalinan, pengubahsuaian, atau pengedaran tanpa kebenaran bertulis adalah dilarang.
Semua kandungan, reka bentuk, logo, teks, grafik, dan bahan lain yang terdapat pada platform ini adalah hak milik Koperasi Permodalan Kelantan Berhad (KOPKB) atau pemberi lesennya. Anda diberikan lesen terhad untuk menggunakan kandungan tersebut bagi tujuan penggunaan peribadi dan bukan komersial sahaja. Sebarang penyalinan, pengubahsuaian, atau pengedaran tanpa kebenaran bertulis adalah dilarang.
`,
},
{
@@ -69,8 +69,8 @@ Kami boleh mengemas kini atau meminda Terma dan Syarat ini dari semasa ke semasa
placeholder: `
Sekiranya anda mempunyai sebarang pertanyaan berkaitan Terma dan Syarat ini, sila hubungi:
Nama: KAPT MOHAMMAD EFANDY BIN JAFFARI
E-mel: effandy.jaffari@army.mil.my
Nama: Koperasi Permodalan Kelantan Berhad (KOPKB)
E-mel: admin@koppkb.com
`,
},
] as const;
@@ -0,0 +1,41 @@
import { computed, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { useExternalSystemList } from './useExternalSystemList'
import {
getExternalSystemStatus,
isExternalSystemAccessible,
} from '../utils/external-system.utils'
export function useExternalSystemDetail() {
const route = useRoute()
const { getSystemById } = useExternalSystemList()
const loading = ref(false)
const error = ref<string | null>(null)
const systemId = computed(() => String(route.params.id ?? ''))
const system = computed(() => getSystemById(systemId.value) ?? null)
const status = computed(() => (system.value ? getExternalSystemStatus(system.value) : null))
const isAccessible = computed(() =>
system.value ? isExternalSystemAccessible(system.value) : false,
)
watch(
systemId,
() => {
error.value = system.value ? null : 'Sistem luaran tidak dijumpai.'
},
{ immediate: true },
)
return {
system,
loading,
error,
status,
isAccessible,
}
}
@@ -0,0 +1,48 @@
import { computed, ref } from 'vue'
import { dummyExternalSystems } from '../data/dummy-external-systems'
import {
externalSystemStatusLabel,
getExternalSystemStatus,
} from '../utils/external-system.utils'
import type { ExternalSystem } from '../types/external-system.types'
export function useExternalSystemList() {
const search = ref('')
const loading = ref(false)
const systems = computed(() => {
const query = search.value.trim().toLowerCase()
if (!query) {
return dummyExternalSystems
}
return dummyExternalSystems.filter((system) => {
const haystack = [
system.name,
system.code,
system.description,
externalSystemStatusLabel(getExternalSystemStatus(system)),
]
.join(' ')
.toLowerCase()
return haystack.includes(query)
})
})
const availableCount = computed(
() => systems.value.filter((system) => getExternalSystemStatus(system) === 'available').length,
)
function getSystemById(id: string): ExternalSystem | undefined {
return dummyExternalSystems.find((system) => system.id === id)
}
return {
systems,
search,
loading,
availableCount,
getSystemById,
}
}
@@ -0,0 +1,38 @@
import type { ExternalSystem } from '../types/external-system.types'
export const dummyExternalSystems: ExternalSystem[] = [
{
id: 'ext-001',
code: 'KOPKB-PORTAL',
name: 'Portal Ahli KOPKB',
description:
'Sistem utama keahlian Koperasi Permodalan Kelantan Berhad untuk semakan dividen, penyata dan maklumat ahli.',
url: 'https://anggota.koppkb.com',
icon: 'Users',
is_active: true,
starts_at: '2026-01-01T00:00:00+08:00',
ends_at: null,
opens_in_new_tab: true,
contact_email: 'sokongan@koppkb.com',
notes: 'Log masuk menggunakan e-mel berdaftar ahli KOPKB.',
created_at: '2026-01-15T09:00:00+08:00',
updated_at: '2026-06-01T14:30:00+08:00',
},
{
id: 'ext-002',
code: 'AGM-VOTE',
name: 'Sistem Pengundian AGM',
description:
'Platform pengundian dalam talian untuk Mesyuarat Agung Tahunan. Hanya tersedia semasa tempoh pengundian.',
url: 'https://e-vote.erahn.com.my/login',
icon: 'Vote',
is_active: true,
starts_at: '2026-05-01T08:00:00+08:00',
ends_at: null,
opens_in_new_tab: true,
contact_email: 'agm@koppkb.com',
notes: 'Sila lengkapkan profil sebelum mengundi.',
created_at: '2026-05-20T10:00:00+08:00',
updated_at: '2026-06-28T11:15:00+08:00',
},
]
+2
View File
@@ -0,0 +1,2 @@
export { externalSystemLayoutRoutes } from './routes'
export { externalSystemMenu } from './menu'
+9
View File
@@ -0,0 +1,9 @@
import type { Menu } from '@/core/types/menu'
export const externalSystemMenu: Menu[] = [
{
icon: 'ExternalLink',
route_name: 'list-external-systems',
title: 'Sistem Luaran',
},
]
@@ -0,0 +1,115 @@
<script lang="ts" setup>
import { 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 { Lucide } from '@/components/ui/lucide'
import { useExternalSystemDetail } from '../composables/useExternalSystemDetail'
import {
externalSystemStatusLabel,
externalSystemStatusVariant,
formatExternalSystemDateTime,
openExternalSystem,
} from '../utils/external-system.utils'
const router = useRouter()
const { system, error, status, isAccessible } = useExternalSystemDetail()
function goBack() {
router.push({ name: 'list-external-systems' })
}
function handleOpen() {
if (!system.value) return
openExternalSystem(system.value)
}
</script>
<template>
<div>
<div class="mb-5 flex flex-wrap items-center gap-3">
<Button variant="ghost" look="outline" @click="goBack">
<Lucide icon="ArrowLeft" class="size-4" />
Kembali
</Button>
</div>
<AlertRoot v-if="error" class="mb-6" variant="danger">
<AlertTitle>Ralat</AlertTitle>
<AlertDescription>{{ error }}</AlertDescription>
</AlertRoot>
<template v-else-if="system && status">
<Box class="p-5 sm:p-6">
<div class="flex flex-wrap items-start justify-between gap-4">
<div class="flex min-w-0 items-start gap-4">
<div
class="flex size-12 shrink-0 items-center justify-center rounded-2xl bg-primary/10 text-primary"
>
<Lucide :icon="system.icon" class="size-6" />
</div>
<div class="min-w-0">
<div class="text-sm opacity-70">Sistem Luaran</div>
<h2 class="text-xl font-semibold">{{ system.name }}</h2>
<div class="mt-1 text-sm font-medium text-primary/80">{{ system.code }}</div>
</div>
</div>
<Badge look="outline" :variant="externalSystemStatusVariant(status)">
{{ externalSystemStatusLabel(status) }}
</Badge>
</div>
<p class="mt-5 max-w-3xl text-sm leading-relaxed opacity-80">
{{ system.description }}
</p>
<div class="mt-6 grid gap-3 text-sm sm:grid-cols-2 lg:grid-cols-4">
<div>
<span class="opacity-70">URL:</span>
<div class="mt-0.5 break-all font-medium">{{ system.url }}</div>
</div>
<div>
<span class="opacity-70">Tarikh Mula:</span>
<div class="mt-0.5 font-medium">{{ formatExternalSystemDateTime(system.starts_at) }}</div>
</div>
<div>
<span class="opacity-70">Tarikh Tamat:</span>
<div class="mt-0.5 font-medium">{{ formatExternalSystemDateTime(system.ends_at) }}</div>
</div>
<div>
<span class="opacity-70">E-mel Sokongan:</span>
<div class="mt-0.5 font-medium">{{ system.contact_email ?? '-' }}</div>
</div>
</div>
<div v-if="system.notes" class="mt-6 border-t border-foreground/10 pt-5">
<div class="text-sm font-medium opacity-70">Nota</div>
<p class="mt-2 text-sm leading-relaxed opacity-80">{{ system.notes }}</p>
</div>
<div class="mt-6 flex flex-wrap gap-3 border-t border-foreground/10 pt-5">
<Button
look="outline"
variant="primary"
:disabled="!isAccessible"
@click="handleOpen"
>
Buka Sistem
<Lucide icon="ExternalLink" class="size-4" />
</Button>
</div>
<AlertRoot v-if="!isAccessible" class="mt-4" look="outline" variant="pending">
<Lucide class="mr-2 size-4 shrink-0" icon="Clock" />
<AlertTitle>Sistem Tidak Tersedia</AlertTitle>
<AlertDescription>
Pautan ini hanya boleh dibuka semasa tempoh acara yang ditetapkan dan sistem berstatus
aktif.
</AlertDescription>
</AlertRoot>
</Box>
</template>
</div>
</template>
@@ -0,0 +1,104 @@
<script lang="ts" setup>
import { 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 { Input } from '@/components/ui/input'
import { Lucide } from '@/components/ui/lucide'
import { useExternalSystemList } from '../composables/useExternalSystemList'
import {
externalSystemStatusLabel,
externalSystemStatusVariant,
formatExternalSystemDateTime,
getExternalSystemStatus,
openExternalSystem,
} from '../utils/external-system.utils'
import type { ExternalSystem } from '../types/external-system.types'
const router = useRouter()
const { systems, search, loading, availableCount } = useExternalSystemList()
function goToDetail(system: ExternalSystem) {
router.push({ name: 'view-external-system', params: { id: system.id } })
}
function handleOpen(system: ExternalSystem) {
openExternalSystem(system)
}
</script>
<template>
<div>
<div class="flex flex-wrap items-start justify-between gap-4">
<div>
<h2 class="text-lg font-medium">Sistem Luaran</h2>
</div>
<Badge look="outline" variant="primary">
{{ availableCount }} tersedia
</Badge>
</div>
<div class="mt-5 grid grid-cols-12 gap-x-6 gap-y-8">
<div class="col-span-12 mt-2 flex flex-wrap items-center sm:flex-nowrap">
<div class="w-full sm:w-auto">
<div class="relative w-56">
<Input v-model="search" class="w-56 pr-10" type="search" placeholder="Cari sistem..." :disabled="loading" />
<Lucide class="absolute inset-y-0 right-0 my-auto mr-3 h-4 w-4" icon="Search" />
</div>
</div>
</div>
<template v-if="systems.length">
<Box v-for="system in systems" :key="system.id"
class="col-span-12 flex h-full flex-col p-5 md:col-span-6 xl:col-span-4">
<div class="mb-4 flex items-start justify-between gap-3">
<div class="flex size-10 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary">
<Lucide :icon="system.icon" class="size-5" />
</div>
<Badge look="outline" :variant="externalSystemStatusVariant(getExternalSystemStatus(system))">
{{ externalSystemStatusLabel(getExternalSystemStatus(system)) }}
</Badge>
</div>
<div class="text-base font-medium">{{ system.name }}</div>
<div class="mt-1 text-xs font-medium uppercase tracking-wide text-primary/80">
{{ system.code }}
</div>
<p class="mt-2 flex-1 text-sm leading-relaxed opacity-70">
{{ system.description }}
</p>
<div class="mt-4 space-y-1 text-xs opacity-70">
<div>
<span class="font-medium">Mula:</span>
{{ formatExternalSystemDateTime(system.starts_at) }}
</div>
<div>
<span class="font-medium">Tamat:</span>
{{ formatExternalSystemDateTime(system.ends_at) }}
</div>
</div>
<div class="mt-5 flex flex-col gap-2 sm:flex-row">
<Button class="w-full sm:flex-1" look="outline" variant="primary"
:disabled="getExternalSystemStatus(system) !== 'available'" @click="handleOpen(system)">
Buka Sistem
<Lucide icon="ExternalLink" class="size-4" />
</Button>
<Button class="w-full sm:flex-1" variant="ghost" @click="goToDetail(system)">
Butiran
<Lucide icon="ArrowRight" class="size-4" />
</Button>
</div>
</Box>
</template>
<Box v-else class="col-span-12 p-8 text-center">
<Lucide icon="SearchX" class="mx-auto size-8 opacity-40" />
<div class="mt-3 text-base font-medium">Tiada sistem dijumpai</div>
<p class="mt-1 text-sm opacity-70">Cuba istilah carian yang berbeza.</p>
</Box>
</div>
</div>
</template>
+22
View File
@@ -0,0 +1,22 @@
import type { RouteRecordRaw } from 'vue-router'
export const externalSystemLayoutRoutes: RouteRecordRaw[] = [
{
path: 'external-systems',
name: 'list-external-systems',
component: () => import('./pages/ExternalSystemList.vue'),
meta: {
title: 'Senarai Sistem Luaran',
module: 'external-system',
},
},
{
path: 'external-systems/:id',
name: 'view-external-system',
component: () => import('./pages/ExternalSystemDetail.vue'),
meta: {
title: 'Butiran Sistem Luaran',
module: 'external-system',
},
},
]
@@ -0,0 +1,20 @@
import type { Icon } from '@/components/ui/lucide'
export type ExternalSystemStatus = 'available' | 'upcoming' | 'ended' | 'inactive'
export type ExternalSystem = {
id: string
code: string
name: string
description: string
url: string
icon: Icon
is_active: boolean
starts_at: string | null
ends_at: string | null
opens_in_new_tab: boolean
contact_email: string | null
notes: string | null
created_at: string
updated_at: string
}
@@ -0,0 +1,74 @@
import dayjs from 'dayjs'
import type { BadgeVariants } from '@/components/ui/styles/badge.styles'
import type { ExternalSystem, ExternalSystemStatus } from '../types/external-system.types'
export function getExternalSystemStatus(
system: ExternalSystem,
now = dayjs(),
): ExternalSystemStatus {
if (!system.is_active) {
return 'inactive'
}
const startsAt = system.starts_at ? dayjs(system.starts_at) : null
const endsAt = system.ends_at ? dayjs(system.ends_at) : null
if (startsAt?.isAfter(now)) {
return 'upcoming'
}
if (endsAt?.isBefore(now)) {
return 'ended'
}
return 'available'
}
export function isExternalSystemAccessible(system: ExternalSystem, now = dayjs()): boolean {
return getExternalSystemStatus(system, now) === 'available'
}
export function externalSystemStatusLabel(status: ExternalSystemStatus): string {
switch (status) {
case 'available':
return 'Tersedia'
case 'upcoming':
return 'Akan Datang'
case 'ended':
return 'Tamat'
case 'inactive':
return 'Tidak Aktif'
}
}
export function externalSystemStatusVariant(
status: ExternalSystemStatus,
): NonNullable<BadgeVariants['variant']> {
switch (status) {
case 'available':
return 'success'
case 'upcoming':
return 'pending'
case 'ended':
return 'ghost'
case 'inactive':
return 'danger'
}
}
export function formatExternalSystemDateTime(value: string | null): string {
if (!value) return '-'
return dayjs(value).format('DD MMM YYYY, HH:mm')
}
export function openExternalSystem(system: ExternalSystem) {
if (!isExternalSystemAccessible(system)) {
return
}
window.open(
system.url,
system.opens_in_new_tab ? '_blank' : '_self',
'noopener,noreferrer',
)
}
@@ -223,7 +223,7 @@ onMounted(async () => {
<div class="truncate text-base font-medium">
{{ displayValue(companyName) }}
</div>
<div class="text-xs opacity-70">Syarikat</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">
@@ -113,7 +113,7 @@ onMounted(() => {
<div class="flex items-center gap-3 rounded-lg border border-foreground/10 p-4">
<Lucide class="size-5 text-primary" icon="Briefcase" />
<div class="min-w-0">
<div class="text-xs uppercase tracking-wide text-slate-500">Syarikat</div>
<div class="text-xs uppercase tracking-wide text-slate-500">Unit</div>
<div class="truncate text-base font-medium text-slate-900">
{{ displayValue(member.company_name) }}
</div>