DONE: use full name for date in letter, include password in email after membership is done, block ic_number to exclude - (#9)
Build Docker Image / build-backend (push) Successful in 44s
Build Docker Image / build-frontend (push) Successful in 18s

Co-authored-by: ISMAIL MASSERAN <topaz@ISMAILs-Macbook.local>
Reviewed-on: #9
This commit was merged in pull request #9.
This commit is contained in:
2026-07-08 12:15:41 +08:00
parent 18c0d268b1
commit ebd3caecc6
23 changed files with 352 additions and 41 deletions
+84 -8
View File
@@ -36,9 +36,9 @@ type StatusFilterChip = {
const STATUS_FILTER_CHIPS: StatusFilterChip[] = [
{ label: 'Semua', value: '', variant: 'ghost' },
{ label: 'Active', value: 'active', variant: 'success' },
{ label: 'Inactive', value: 'inactive', variant: 'danger' },
{ label: 'Pending', value: 'pending', variant: 'pending' },
{ label: 'Aktif', value: 'active', variant: 'success' },
{ label: 'Tidak Aktif', value: 'inactive', variant: 'danger' },
{ label: 'Menunggu', value: 'pending', variant: 'pending' },
]
const router = useRouter()
@@ -97,6 +97,11 @@ function formatDeletedAt(value: string | null | undefined): string {
return dayjs(value).format('DD MMM YYYY, HH:mm')
}
function formatUserDate(value: string | null | undefined): string {
if (!value) return '-'
return dayjs(value).format('DD MMM YYYY')
}
function openDeleteConfirmation(user: UserListItem) {
userToDelete.value = user
deleteError.value = null
@@ -217,9 +222,20 @@ async function confirmRestore() {
const headers: TableHeader[] = [
{ title: 'Bil.', key: '#', sortable: false },
{ title: 'Name', key: 'name', sortable: true },
{ title: 'Emel', key: 'email', sortable: true },
{ title: 'Jawatan', key: 'position', sortable: true },
{ title: 'No. Anggota', key: 'member_number', sortable: true, align: 'center' },
{
title: 'Tarikh Menjadi Anggota',
key: 'join_date',
sortable: true,
exportValue: (item) => formatUserDate(item.join_date),
},
{
title: 'Tarikh Berhenti',
key: 'leave_date',
sortable: true,
exportValue: (item) => formatUserDate(item.leave_date),
},
{
title: 'Peranan',
key: 'roles',
@@ -253,6 +269,14 @@ const {
error,
search,
statusFilter,
joinDateFrom,
joinDateTo,
leaveDateFrom,
leaveDateTo,
hasJoinDateFilters,
hasLeaveDateFilters,
clearJoinDateFilters,
clearLeaveDateFilters,
sortBy,
page,
itemsPerPage,
@@ -306,8 +330,8 @@ onMounted(() => {
<template>
<div class="w-full space-y-6">
<div>
<h2 class="text-lg font-medium">Senarai Pengguna</h2>
<p class="mt-1 text-sm opacity-70">Urus dan semak pengguna koperasi.</p>
<h2 class="text-lg font-medium">Senarai Daftar Anggota</h2>
<p class="mt-1 text-sm opacity-70">Urus dan semak anggota koperasi.</p>
</div>
<AlertRoot v-if="error" class="mt-6" variant="danger">
<AlertTitle>Error</AlertTitle>
@@ -315,8 +339,8 @@ onMounted(() => {
</AlertRoot>
<DataTable :headers="headers" :items="users" :loading="loading" :pagination="pagination" :current-sort="sortBy"
show-pagination exportable export-file-name="users" v-model:page="page" v-model:items-per-page="itemsPerPage"
@update:sort-by="handleSortUpdate">
show-pagination exportable export-file-name="users" export-pdf-title="Senarai Daftar Anggota" v-model:page="page"
v-model:items-per-page="itemsPerPage" @update:sort-by="handleSortUpdate">
<template #toolbar>
<div class="flex w-full flex-col gap-3">
<div class="flex w-full flex-wrap items-center gap-3">
@@ -341,6 +365,50 @@ onMounted(() => {
{{ chip.label }}
</Badge>
</div>
<div class="flex flex-col gap-3">
<div class="rounded-lg border border-foreground/10 p-3">
<div class="flex flex-wrap items-end gap-3">
<div class="flex min-w-[16rem] flex-1 flex-col gap-1.5">
<span class="text-sm font-medium">Tarikh Menjadi Anggota</span>
<div class="flex items-center gap-2">
<Input v-model="joinDateFrom" type="date" aria-label="Tarikh menjadi anggota dari" />
<span class="text-sm opacity-50"></span>
<Input v-model="joinDateTo" type="date" aria-label="Tarikh menjadi anggota hingga" />
</div>
</div>
<Button
v-if="hasJoinDateFilters"
type="button"
variant="ghost"
look="outline"
@click="clearJoinDateFilters"
>
Reset
</Button>
</div>
</div>
<div class="rounded-lg border border-foreground/10 p-3">
<div class="flex flex-wrap items-end gap-3">
<div class="flex min-w-[16rem] flex-1 flex-col gap-1.5">
<span class="text-sm font-medium">Tarikh Berhenti</span>
<div class="flex items-center gap-2">
<Input v-model="leaveDateFrom" type="date" aria-label="Tarikh berhenti dari" />
<span class="text-sm opacity-50"></span>
<Input v-model="leaveDateTo" type="date" aria-label="Tarikh berhenti hingga" />
</div>
</div>
<Button
v-if="hasLeaveDateFilters"
type="button"
variant="ghost"
look="outline"
@click="clearLeaveDateFilters"
>
Reset
</Button>
</div>
</div>
</div>
</div>
</template>
@@ -369,6 +437,14 @@ onMounted(() => {
<span class="text-center">{{ item.member_number }}</span>
</template>
<template #item.join_date="{ item }">
{{ formatUserDate(item.join_date) }}
</template>
<template #item.leave_date="{ item }">
{{ formatUserDate(item.leave_date) }}
</template>
<!-- centre align status -->
<template #item.status="{ item }">
<Badge :variant="statusBadgeVariant(item.status)" class="capitalize text-center">