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:
@@ -100,6 +100,8 @@ const props = withDefaults(defineProps<{
|
||||
currentSort?: SortConfig[];
|
||||
exportable?: boolean;
|
||||
exportFileName?: string;
|
||||
/** Title shown at the top of exported PDF documents. */
|
||||
exportPdfTitle?: string;
|
||||
/** When true, table body scrolls inside a max-height box instead of growing the page. */
|
||||
scrollable?: boolean;
|
||||
/** Max height of the scrollable table area (CSS value). */
|
||||
@@ -622,12 +624,22 @@ const exportToPDF = () => {
|
||||
else if (colCount > 8) fontSize = 7;
|
||||
const cellPadding = colCount > 8 ? 4 : 8;
|
||||
|
||||
let tableStartY = 20;
|
||||
if (props.exportPdfTitle?.trim()) {
|
||||
doc.setFontSize(14);
|
||||
doc.text(props.exportPdfTitle.trim(), doc.internal.pageSize.getWidth() / 2, 28, {
|
||||
align: 'center',
|
||||
});
|
||||
tableStartY = 48;
|
||||
}
|
||||
|
||||
const tableOptions: Parameters<typeof autoTable>[1] = {
|
||||
head: [headers],
|
||||
body: data,
|
||||
startY: tableStartY,
|
||||
styles: { fontSize, cellPadding },
|
||||
headStyles: { fillColor: [66, 139, 202] },
|
||||
margin: { top: 20 },
|
||||
margin: { top: tableStartY },
|
||||
};
|
||||
|
||||
// For wide tables: let table use natural column widths and split across pages
|
||||
|
||||
@@ -59,7 +59,7 @@ const availableModules = computed(() => [
|
||||
visible: hasPermission('lihat aktiviti'),
|
||||
},
|
||||
{
|
||||
title: 'Senarai Pengguna',
|
||||
title: 'Senarai Daftar Anggota',
|
||||
description: 'Pantau dan urus akaun pengguna sistem.',
|
||||
route: 'list-users',
|
||||
icon: 'Users' as Icon,
|
||||
|
||||
@@ -22,6 +22,7 @@ import { AlertRoot, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { Lucide } from '@/components/ui/lucide'
|
||||
import { getApiErrorMessage, getApiValidationErrors } from '@/core/utils/getApiErrorMessage'
|
||||
import { submitMembershipApplication, lookupMemberByIcNumber } from '../services/membership-application.service'
|
||||
import { sanitizeIcNumberInput } from '../utils/membership-application-form.utils'
|
||||
import type {
|
||||
MembershipApplicationFormState,
|
||||
MembershipApplicationHeirForm,
|
||||
@@ -37,7 +38,7 @@ const MIN_FEE_MONTHLY_CONTRIBUTION = 30
|
||||
|
||||
const steps = [
|
||||
{ id: 1, label: 'Maklumat Peribadi' },
|
||||
{ id: 2, label: 'Hubungan & Alamat' },
|
||||
{ id: 2, label: 'No. Telefon & Alamat' },
|
||||
{ id: 3, label: 'Maklumat Pekerjaan' },
|
||||
{ id: 4, label: 'Maklumat Penama' },
|
||||
{ id: 5, label: 'Dokumen & Hantar' },
|
||||
@@ -221,9 +222,22 @@ function clearReference(role: 'proposer' | 'supporter') {
|
||||
}
|
||||
|
||||
function handleReferenceIcInput(role: 'proposer' | 'supporter') {
|
||||
form.references[role].ic_number = sanitizeIcNumberInput(form.references[role].ic_number)
|
||||
clearReference(role)
|
||||
}
|
||||
|
||||
function handleApplicantIcInput() {
|
||||
form.applicant.ic_number = sanitizeIcNumberInput(form.applicant.ic_number)
|
||||
delete fieldErrors['applicant.ic_number']
|
||||
}
|
||||
|
||||
function handleHeirIcInput(index: number) {
|
||||
const heir = form.heirs[index]
|
||||
if (!heir) return
|
||||
heir.ic_number = sanitizeIcNumberInput(heir.ic_number)
|
||||
delete fieldErrors[`heirs.${index}.ic_number`]
|
||||
}
|
||||
|
||||
async function lookupReference(role: 'proposer' | 'supporter') {
|
||||
const reference = form.references[role]
|
||||
const icNumber = reference.ic_number.trim()
|
||||
@@ -618,7 +632,8 @@ function stepLabelClass(stepId: number) {
|
||||
</Field>
|
||||
<Field class="col-span-12 sm:col-span-6">
|
||||
<FieldLabel for="ic_number">No. Kad Pengenalan</FieldLabel>
|
||||
<Input id="ic_number" v-model="form.applicant.ic_number" type="text" />
|
||||
<Input id="ic_number" v-model="form.applicant.ic_number" type="text" inputmode="numeric" maxlength="15"
|
||||
placeholder="Contoh: 900101011234" @input="handleApplicantIcInput" />
|
||||
<FieldError v-if="fieldErrors['applicant.ic_number']">{{ fieldErrors['applicant.ic_number'] }}
|
||||
</FieldError>
|
||||
</Field>
|
||||
@@ -800,7 +815,8 @@ function stepLabelClass(stepId: number) {
|
||||
</Field>
|
||||
<Field class="col-span-12 sm:col-span-6">
|
||||
<FieldLabel :for="`heir-ic-${index}`">No. Kad Pengenalan</FieldLabel>
|
||||
<Input :id="`heir-ic-${index}`" v-model="heir.ic_number" type="text" />
|
||||
<Input :id="`heir-ic-${index}`" v-model="heir.ic_number" type="text" inputmode="numeric"
|
||||
maxlength="15" placeholder="Contoh: 900101011234" @input="handleHeirIcInput(index)" />
|
||||
<FieldError v-if="fieldErrors[`heirs.${index}.ic_number`]">
|
||||
{{ fieldErrors[`heirs.${index}.ic_number`] }}
|
||||
</FieldError>
|
||||
@@ -857,8 +873,9 @@ function stepLabelClass(stepId: number) {
|
||||
<Field class="col-span-12 sm:col-span-6">
|
||||
<FieldLabel for="proposer_ic_number">No. KP Pencadang</FieldLabel>
|
||||
<Input id="proposer_ic_number" v-model="form.references.proposer.ic_number" type="text"
|
||||
placeholder="Contoh: 900101011234" :disabled="referenceLookupLoading.proposer"
|
||||
@input="handleReferenceIcInput('proposer')" @blur="lookupReference('proposer')" />
|
||||
inputmode="numeric" maxlength="12" placeholder="Contoh: 900101011234"
|
||||
:disabled="referenceLookupLoading.proposer" @input="handleReferenceIcInput('proposer')"
|
||||
@blur="lookupReference('proposer')" />
|
||||
<p v-if="form.references.proposer.name" class="mt-1 text-sm text-success">
|
||||
{{ form.references.proposer.name }}
|
||||
</p>
|
||||
@@ -870,8 +887,9 @@ function stepLabelClass(stepId: number) {
|
||||
<Field class="col-span-12 sm:col-span-6">
|
||||
<FieldLabel for="supporter_ic_number">No. KP Penyokong</FieldLabel>
|
||||
<Input id="supporter_ic_number" v-model="form.references.supporter.ic_number" type="text"
|
||||
placeholder="Contoh: 850505055678" :disabled="referenceLookupLoading.supporter"
|
||||
@input="handleReferenceIcInput('supporter')" @blur="lookupReference('supporter')" />
|
||||
inputmode="numeric" maxlength="12" placeholder="Contoh: 850505055678"
|
||||
:disabled="referenceLookupLoading.supporter" @input="handleReferenceIcInput('supporter')"
|
||||
@blur="lookupReference('supporter')" />
|
||||
<p v-if="form.references.supporter.name" class="mt-1 text-sm text-success">
|
||||
{{ form.references.supporter.name }}
|
||||
</p>
|
||||
@@ -907,7 +925,7 @@ function stepLabelClass(stepId: number) {
|
||||
<Input id="employer_letter" type="file" accept=".pdf,.jpg,.jpeg,.png"
|
||||
@change="handleFileChange('employer_letter', $event)" />
|
||||
<FieldError v-if="fieldErrors['documents.employer_letter']">{{ fieldErrors['documents.employer_letter']
|
||||
}}</FieldError>
|
||||
}}</FieldError>
|
||||
</Field>
|
||||
|
||||
<div class="col-span-12 mt-2 rounded-lg border border-foreground/10 bg-foreground/5 p-4">
|
||||
|
||||
@@ -65,6 +65,8 @@ const DOCUMENT_TYPE_LABELS: Record<string, string> = {
|
||||
[RESULT_LETTER_DOCUMENT_TYPE]: 'Surat Keputusan',
|
||||
}
|
||||
|
||||
const BOARD_MEETING_REFERENCE_PREFIX = 'Mesyuarat Lembaga Bil.'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { hasPermission } = usePermissions()
|
||||
@@ -430,8 +432,10 @@ async function confirmPendingAction() {
|
||||
})
|
||||
boardRemarks.value = ''
|
||||
} else {
|
||||
const boardMeetingReferenceValue = `${BOARD_MEETING_REFERENCE_PREFIX} ${boardMeetingReference.value.trim()}`
|
||||
|
||||
response = await completeMembershipApplication(applicationId.value, {
|
||||
board_meeting_reference: boardMeetingReference.value.trim(),
|
||||
board_meeting_reference: boardMeetingReferenceValue,
|
||||
board_meeting_date: boardMeetingDate.value,
|
||||
})
|
||||
}
|
||||
@@ -1013,9 +1017,15 @@ onUnmounted(() => {
|
||||
<div class="mt-2 opacity-70">{{ confirmDialogDescription }}</div>
|
||||
<Field v-if="pendingAction?.type === 'complete'" class="mt-5 text-left">
|
||||
<FieldLabel for="detail-board-meeting-reference">Rujukan Mesyuarat Lembaga</FieldLabel>
|
||||
<Input id="detail-board-meeting-reference" v-model="boardMeetingReference" type="text"
|
||||
placeholder="Contoh: Mesyuarat Lembaga Bil. 3/2026" :disabled="completeSubmitting"
|
||||
@input="boardMeetingReferenceError = null" />
|
||||
<div class="flex">
|
||||
<span
|
||||
class="inline-flex items-center rounded-l-lg border border-r-0 border-foreground/10 bg-foreground/5 px-3 text-sm opacity-80">
|
||||
{{ BOARD_MEETING_REFERENCE_PREFIX }}
|
||||
</span>
|
||||
<Input id="detail-board-meeting-reference" v-model="boardMeetingReference" type="text"
|
||||
class="rounded-l-none" placeholder="Contoh: 3/2026" :disabled="completeSubmitting"
|
||||
@input="boardMeetingReferenceError = null" />
|
||||
</div>
|
||||
<FieldError v-if="boardMeetingReferenceError">{{ boardMeetingReferenceError }}</FieldError>
|
||||
</Field>
|
||||
<Field v-if="pendingAction?.type === 'complete'" class="mt-4 text-left">
|
||||
|
||||
@@ -61,6 +61,10 @@ export type ApplicantDocumentUploadType = (typeof APPLICANT_DOCUMENT_UPLOAD_TYPE
|
||||
export type AdminDocumentUploadType = (typeof ADMIN_DOCUMENT_UPLOAD_TYPES)[number]
|
||||
export type DocumentUploadType = ApplicantDocumentUploadType | AdminDocumentUploadType
|
||||
|
||||
export function sanitizeIcNumberInput(value: string): string {
|
||||
return value.replace(/\D/g, '')
|
||||
}
|
||||
|
||||
export function createSelectCollection(options: SelectOption[]) {
|
||||
return select.collection({
|
||||
items: options,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import debounce from 'lodash/debounce'
|
||||
import type { SortConfig } from '@/components/ui/usage/DataTable.vue'
|
||||
import { useApiPagination } from '@/composables/useApiPagination'
|
||||
@@ -12,12 +12,29 @@ export function useUserList() {
|
||||
const error = ref<string | null>(null)
|
||||
const search = ref('')
|
||||
const statusFilter = ref('')
|
||||
const joinDateFrom = ref('')
|
||||
const joinDateTo = ref('')
|
||||
const leaveDateFrom = ref('')
|
||||
const leaveDateTo = ref('')
|
||||
const sortBy = ref<SortConfig[]>([{ key: 'name', order: 'asc' }])
|
||||
const page = ref(1)
|
||||
const itemsPerPage = ref(10)
|
||||
|
||||
const { pagination, applyPagination } = useApiPagination({ per_page: 10 })
|
||||
|
||||
const hasJoinDateFilters = computed(() => Boolean(joinDateFrom.value || joinDateTo.value))
|
||||
const hasLeaveDateFilters = computed(() => Boolean(leaveDateFrom.value || leaveDateTo.value))
|
||||
|
||||
function clearJoinDateFilters() {
|
||||
joinDateFrom.value = ''
|
||||
joinDateTo.value = ''
|
||||
}
|
||||
|
||||
function clearLeaveDateFilters() {
|
||||
leaveDateFrom.value = ''
|
||||
leaveDateTo.value = ''
|
||||
}
|
||||
|
||||
async function fetchUsers(requestPage = page.value) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
@@ -31,11 +48,17 @@ export function useUserList() {
|
||||
sort_order: activeSort?.order ?? 'asc',
|
||||
search: search.value.trim() || undefined,
|
||||
status: statusFilter.value.trim() || undefined,
|
||||
join_date_from: joinDateFrom.value || undefined,
|
||||
join_date_to: joinDateTo.value || undefined,
|
||||
leave_date_from: leaveDateFrom.value || undefined,
|
||||
leave_date_to: leaveDateTo.value || undefined,
|
||||
})
|
||||
|
||||
users.value = data.data
|
||||
applyPagination(data.pagination)
|
||||
page.value = data.pagination.current_page
|
||||
} catch (err) {
|
||||
error.value = getApiErrorMessage(err, 'Gagal memuatkan senarai pengguna.')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -58,6 +81,10 @@ export function useUserList() {
|
||||
fetchUsers(1)
|
||||
})
|
||||
|
||||
watch([joinDateFrom, joinDateTo, leaveDateFrom, leaveDateTo], () => {
|
||||
fetchUsers(1)
|
||||
})
|
||||
|
||||
watch(page, (nextPage, previousPage) => {
|
||||
if (nextPage !== previousPage) {
|
||||
fetchUsers(nextPage)
|
||||
@@ -80,6 +107,14 @@ export function useUserList() {
|
||||
error,
|
||||
search,
|
||||
statusFilter,
|
||||
joinDateFrom,
|
||||
joinDateTo,
|
||||
leaveDateFrom,
|
||||
leaveDateTo,
|
||||
hasJoinDateFilters,
|
||||
hasLeaveDateFilters,
|
||||
clearJoinDateFilters,
|
||||
clearLeaveDateFilters,
|
||||
sortBy,
|
||||
page,
|
||||
itemsPerPage,
|
||||
|
||||
@@ -4,7 +4,7 @@ export const userMenu: Menu[] = [
|
||||
{
|
||||
icon: 'Users',
|
||||
route_name: 'list-users',
|
||||
title: 'Senarai Pengguna',
|
||||
title: 'Senarai Daftar Anggota',
|
||||
permission: 'lihat pengguna',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -26,6 +26,8 @@ export interface UserListItem {
|
||||
deleted_at?: string | null
|
||||
roles: UserRole[]
|
||||
member_number?: number | null
|
||||
join_date?: string | null
|
||||
leave_date?: string | null
|
||||
}
|
||||
|
||||
export interface UserDetail {
|
||||
@@ -42,6 +44,7 @@ export interface UserDetail {
|
||||
member_number: number | null
|
||||
member_type: string | null
|
||||
join_date: string | null
|
||||
leave_date: string | null
|
||||
birth_date: string | null
|
||||
birth_place: string | null
|
||||
roles: UserRole[]
|
||||
@@ -62,6 +65,7 @@ export interface UpdateUserPayload {
|
||||
member_number?: number | null
|
||||
member_type?: string | null
|
||||
join_date?: string | null
|
||||
leave_date?: string | null
|
||||
birth_date?: string | null
|
||||
birth_place?: string | null
|
||||
}
|
||||
@@ -89,6 +93,10 @@ export interface ListUsersParams {
|
||||
sort_order: string
|
||||
search?: string
|
||||
status?: string
|
||||
join_date_from?: string
|
||||
join_date_to?: string
|
||||
leave_date_from?: string
|
||||
leave_date_to?: string
|
||||
}
|
||||
|
||||
export interface ListDeletedUsersParams {
|
||||
|
||||
Reference in New Issue
Block a user