DONE: use full name for date in letter, include password in email after membership is done, block ic_number to exclude - (#9)
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:
@@ -1,6 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import dayjs from 'dayjs'
|
||||
import * as select from '@zag-js/select'
|
||||
import { AlertRoot, AlertTitle, AlertDescription } from '@/components/ui/alert'
|
||||
import { Box } from '@/components/ui/box'
|
||||
@@ -115,6 +116,7 @@ const form = reactive({
|
||||
phone_number: '',
|
||||
member_number: '',
|
||||
join_date: '',
|
||||
leave_date: '',
|
||||
birth_date: '',
|
||||
birth_place: '',
|
||||
})
|
||||
@@ -132,6 +134,7 @@ function syncFormFromUser(user: Awaited<ReturnType<typeof getUser>>['data']) {
|
||||
form.phone_number = user.phone_number ?? ''
|
||||
form.member_number = user.member_number != null ? String(user.member_number) : ''
|
||||
form.join_date = toDateInputValue(user.join_date)
|
||||
form.leave_date = toDateInputValue(user.leave_date)
|
||||
form.birth_date = toDateInputValue(user.birth_date)
|
||||
form.birth_place = user.birth_place ?? ''
|
||||
statusValue.value = apiValueToLabel(STATUS_OPTIONS, user.status ?? 'active')
|
||||
@@ -159,6 +162,17 @@ async function fetchUser() {
|
||||
}
|
||||
}
|
||||
|
||||
const selectedStatus = computed(
|
||||
() => labelToApiValue(STATUS_OPTIONS, statusValue.value[0]) ?? 'active',
|
||||
)
|
||||
const isInactiveStatus = computed(() => selectedStatus.value === 'inactive')
|
||||
|
||||
watch(selectedStatus, (newStatus, oldStatus) => {
|
||||
if (newStatus === 'inactive' && oldStatus !== 'inactive' && !form.leave_date) {
|
||||
form.leave_date = dayjs().format('YYYY-MM-DD')
|
||||
}
|
||||
})
|
||||
|
||||
async function handleSubmit() {
|
||||
saving.value = true
|
||||
error.value = null
|
||||
@@ -176,6 +190,7 @@ async function handleSubmit() {
|
||||
member_number: form.member_number ? Number(form.member_number) : null,
|
||||
member_type: labelToApiValue(MEMBER_TYPE_OPTIONS, memberTypeValue.value[0]),
|
||||
join_date: form.join_date || null,
|
||||
leave_date: isInactiveStatus.value ? form.leave_date || null : null,
|
||||
birth_date: form.birth_date || null,
|
||||
birth_place: form.birth_place.trim() || null,
|
||||
})
|
||||
@@ -351,6 +366,17 @@ onMounted(() => {
|
||||
<Input id="user-join-date" v-model="form.join_date" class="w-full" type="date" :disabled="formDisabled" />
|
||||
</Field>
|
||||
|
||||
<Field v-if="isInactiveStatus">
|
||||
<FieldLabel for="user-leave-date">Tarikh Berhenti Menjadi Anggota</FieldLabel>
|
||||
<Input
|
||||
id="user-leave-date"
|
||||
v-model="form.leave_date"
|
||||
class="w-full"
|
||||
type="date"
|
||||
:disabled="formDisabled"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="user-birth-date">Tarikh Lahir</FieldLabel>
|
||||
<Input id="user-birth-date" v-model="form.birth_date" class="w-full" type="date" :disabled="formDisabled" />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -107,6 +107,10 @@ function formatAddressLine(address: Address) {
|
||||
<FieldLabel for="view-user-join-date">Tarikh Sertai</FieldLabel>
|
||||
<Input id="view-user-join-date" :model-value="formatDate(user.join_date)" type="text" disabled />
|
||||
</Field>
|
||||
<Field v-if="user.status === 'inactive' || user.leave_date">
|
||||
<FieldLabel for="view-user-leave-date">Tarikh Berhenti Menjadi Anggota</FieldLabel>
|
||||
<Input id="view-user-leave-date" :model-value="formatDate(user.leave_date)" type="text" disabled />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="view-user-birth-date">Tarikh Lahir</FieldLabel>
|
||||
<Input id="view-user-birth-date" :model-value="formatDate(user.birth_date)" type="text" disabled />
|
||||
|
||||
Reference in New Issue
Block a user