455 lines
14 KiB
Vue
455 lines
14 KiB
Vue
<script lang="ts" setup>
|
|
import { computed, reactive, ref } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import * as select from '@zag-js/select'
|
|
import { AlertRoot, AlertTitle, AlertDescription } from '@/components/ui/alert'
|
|
import { Box } from '@/components/ui/box'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Field, FieldLabel } from '@/components/ui/field'
|
|
import { Input } from '@/components/ui/input'
|
|
import {
|
|
SelectRoot,
|
|
SelectControl,
|
|
SelectTrigger,
|
|
SelectValueText,
|
|
SelectContent,
|
|
SelectItemGroup,
|
|
SelectItemGroupLabel,
|
|
SelectItem,
|
|
SelectItemText,
|
|
} from '@/components/ui/select'
|
|
import { getApiErrorMessage } from '@/core/utils/getApiErrorMessage'
|
|
import { sanitizeIcNumberInput, sanitizeNameInput } from '@/utils/form-input.utils'
|
|
import { createUser } from '../services/user.service'
|
|
|
|
type SelectOption = { label: string; value: string }
|
|
|
|
const STATUS_OPTIONS: SelectOption[] = [
|
|
{ label: 'Active', value: 'active' },
|
|
{ label: 'Inactive', value: 'inactive' },
|
|
{ label: 'Pending', value: 'pending' },
|
|
]
|
|
|
|
const GENDER_OPTIONS: SelectOption[] = [
|
|
{ label: 'Lelaki', value: 'Lelaki' },
|
|
{ label: 'Perempuan', value: 'Perempuan' },
|
|
]
|
|
|
|
const MARRIAGE_STATUS_OPTIONS: SelectOption[] = [
|
|
{ label: 'Belum Berkahwin', value: 'Belum Berkahwin' },
|
|
{ label: 'Berkahwin', value: 'Berkahwin' },
|
|
{ label: 'Bercerai', value: 'Bercerai' },
|
|
{ label: 'Balu', value: 'Balu' },
|
|
{ label: 'Duda', value: 'Duda' },
|
|
]
|
|
|
|
const MEMBER_TYPE_OPTIONS: SelectOption[] = [
|
|
{ label: 'Anggota', value: 'Anggota' },
|
|
{ label: 'Pesara', value: 'Pesara' },
|
|
]
|
|
|
|
function createSelectCollection(options: SelectOption[]) {
|
|
return select.collection({
|
|
items: options,
|
|
itemToValue: (item) => item.label,
|
|
})
|
|
}
|
|
|
|
function labelToApiValue(options: SelectOption[], label: string | undefined): string | null {
|
|
if (!label) return null
|
|
return options.find((option) => option.label === label)?.value ?? null
|
|
}
|
|
|
|
const statusCollection = createSelectCollection(STATUS_OPTIONS)
|
|
const genderCollection = createSelectCollection(GENDER_OPTIONS)
|
|
const marriageStatusCollection = createSelectCollection(MARRIAGE_STATUS_OPTIONS)
|
|
const memberTypeCollection = createSelectCollection(MEMBER_TYPE_OPTIONS)
|
|
|
|
const statusValue = ref<string[]>(['Pending'])
|
|
const genderValue = ref<string[]>([])
|
|
const marriageStatusValue = ref<string[]>([])
|
|
const memberTypeValue = ref<string[]>([])
|
|
|
|
const statusInitial = ref<string[]>(['Pending'])
|
|
|
|
function setStatusValue(details: { value: string[] }) {
|
|
statusValue.value = details.value
|
|
}
|
|
|
|
function setGenderValue(details: { value: string[] }) {
|
|
genderValue.value = details.value
|
|
}
|
|
|
|
function setMarriageStatusValue(details: { value: string[] }) {
|
|
marriageStatusValue.value = details.value
|
|
}
|
|
|
|
function setMemberTypeValue(details: { value: string[] }) {
|
|
memberTypeValue.value = details.value
|
|
}
|
|
|
|
const router = useRouter()
|
|
|
|
const saving = ref(false)
|
|
const error = ref<string | null>(null)
|
|
const successMessage = ref<string | null>(null)
|
|
|
|
const form = reactive({
|
|
name: '',
|
|
email: '',
|
|
ic_number: '',
|
|
position: '',
|
|
phone_number: '',
|
|
member_number: '',
|
|
join_date: '',
|
|
birth_date: '',
|
|
birth_place: '',
|
|
})
|
|
|
|
function handleNameInput() {
|
|
form.name = sanitizeNameInput(form.name)
|
|
}
|
|
|
|
function handleIcNumberInput() {
|
|
form.ic_number = sanitizeIcNumberInput(form.ic_number)
|
|
}
|
|
|
|
function requireSelectValue(label: string | undefined, fieldName: string): string {
|
|
if (!label) {
|
|
throw new Error(`${fieldName} diperlukan.`)
|
|
}
|
|
return label
|
|
}
|
|
|
|
async function handleSubmit() {
|
|
saving.value = true
|
|
error.value = null
|
|
successMessage.value = null
|
|
|
|
try {
|
|
const status = labelToApiValue(STATUS_OPTIONS, statusValue.value[0])
|
|
const gender = labelToApiValue(GENDER_OPTIONS, genderValue.value[0])
|
|
const marriageStatus = labelToApiValue(MARRIAGE_STATUS_OPTIONS, marriageStatusValue.value[0])
|
|
const memberType = labelToApiValue(MEMBER_TYPE_OPTIONS, memberTypeValue.value[0])
|
|
|
|
if (!status) throw new Error('Status pengguna diperlukan.')
|
|
requireSelectValue(genderValue.value[0], 'Jantina')
|
|
requireSelectValue(marriageStatusValue.value[0], 'Status perkahwinan')
|
|
requireSelectValue(memberTypeValue.value[0], 'Jenis anggota')
|
|
|
|
if (!form.member_number) throw new Error('Nombor anggota diperlukan.')
|
|
if (!form.join_date) throw new Error('Tarikh sertai diperlukan.')
|
|
if (!form.birth_date) throw new Error('Tarikh lahir diperlukan.')
|
|
if (!form.birth_place.trim()) throw new Error('Tempat lahir diperlukan.')
|
|
|
|
await createUser({
|
|
name: form.name.trim(),
|
|
email: form.email.trim(),
|
|
ic_number: form.ic_number.trim(),
|
|
position: form.position.trim(),
|
|
phone_number: form.phone_number.trim() || null,
|
|
status,
|
|
gender: gender!,
|
|
marriage_status: marriageStatus!,
|
|
member_number: Number(form.member_number),
|
|
member_type: memberType!,
|
|
join_date: form.join_date,
|
|
birth_date: form.birth_date,
|
|
birth_place: form.birth_place.trim(),
|
|
})
|
|
|
|
successMessage.value = 'Pengguna berjaya didaftarkan.'
|
|
setTimeout(() => {
|
|
router.push({ name: 'list-users' })
|
|
}, 300)
|
|
} catch (err) {
|
|
error.value = getApiErrorMessage(err, 'Gagal mendaftarkan pengguna.')
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
const formDisabled = computed(() => saving.value)
|
|
</script>
|
|
|
|
<template>
|
|
<div class="w-full space-y-6">
|
|
<div class="flex flex-wrap items-center gap-3">
|
|
<h2 class="mr-auto text-lg font-medium">Daftar Pengguna</h2>
|
|
<Button
|
|
look="outline"
|
|
variant="secondary"
|
|
type="button"
|
|
:disabled="saving"
|
|
@click="router.push({ name: 'list-users' })"
|
|
>
|
|
Kembali
|
|
</Button>
|
|
</div>
|
|
|
|
<AlertRoot v-if="error" variant="danger">
|
|
<AlertTitle>Ralat</AlertTitle>
|
|
<AlertDescription>{{ error }}</AlertDescription>
|
|
</AlertRoot>
|
|
|
|
<AlertRoot v-if="successMessage" variant="success">
|
|
<AlertTitle>Berjaya</AlertTitle>
|
|
<AlertDescription>{{ successMessage }}</AlertDescription>
|
|
</AlertRoot>
|
|
|
|
<Box>
|
|
<form class="space-y-4" @submit.prevent="handleSubmit">
|
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
<Field>
|
|
<FieldLabel for="user-name">Nama</FieldLabel>
|
|
<Input
|
|
id="user-name"
|
|
v-model="form.name"
|
|
class="w-full"
|
|
type="text"
|
|
placeholder="Nama penuh"
|
|
:disabled="formDisabled"
|
|
required
|
|
@input="handleNameInput"
|
|
/>
|
|
</Field>
|
|
|
|
<Field>
|
|
<FieldLabel for="user-email">Emel</FieldLabel>
|
|
<Input
|
|
id="user-email"
|
|
v-model="form.email"
|
|
class="w-full"
|
|
type="email"
|
|
placeholder="emel@example.com"
|
|
:disabled="formDisabled"
|
|
required
|
|
/>
|
|
</Field>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
<Field>
|
|
<FieldLabel for="user-ic">Nombor Kad Pengenalan</FieldLabel>
|
|
<Input
|
|
id="user-ic"
|
|
v-model="form.ic_number"
|
|
class="w-full"
|
|
type="text"
|
|
inputmode="numeric"
|
|
maxlength="15"
|
|
placeholder="Contoh: 900101011234"
|
|
:disabled="formDisabled"
|
|
required
|
|
@input="handleIcNumberInput"
|
|
/>
|
|
</Field>
|
|
|
|
<Field>
|
|
<FieldLabel for="user-phone">Nombor Telefon</FieldLabel>
|
|
<Input
|
|
id="user-phone"
|
|
v-model="form.phone_number"
|
|
class="w-full"
|
|
type="text"
|
|
placeholder="Nombor telefon"
|
|
:disabled="formDisabled"
|
|
/>
|
|
</Field>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
<Field>
|
|
<FieldLabel for="user-position">Jawatan</FieldLabel>
|
|
<Input
|
|
id="user-position"
|
|
v-model="form.position"
|
|
class="w-full"
|
|
type="text"
|
|
placeholder="Jawatan"
|
|
:disabled="formDisabled"
|
|
required
|
|
/>
|
|
</Field>
|
|
|
|
<Field>
|
|
<FieldLabel>Status Pengguna</FieldLabel>
|
|
<SelectRoot
|
|
class="w-full"
|
|
:collection="statusCollection"
|
|
:default-value="statusInitial"
|
|
:disabled="formDisabled"
|
|
@value-change="setStatusValue"
|
|
>
|
|
<SelectControl>
|
|
<SelectTrigger>
|
|
<SelectValueText placeholder="Pilih status" />
|
|
</SelectTrigger>
|
|
</SelectControl>
|
|
<SelectContent>
|
|
<SelectItemGroup>
|
|
<SelectItemGroupLabel>Status</SelectItemGroupLabel>
|
|
<SelectItem
|
|
v-for="item in statusCollection.items"
|
|
:key="item.label"
|
|
:item="item"
|
|
>
|
|
<SelectItemText>{{ item.label }}</SelectItemText>
|
|
</SelectItem>
|
|
</SelectItemGroup>
|
|
</SelectContent>
|
|
</SelectRoot>
|
|
</Field>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
<Field>
|
|
<FieldLabel>Jantina</FieldLabel>
|
|
<SelectRoot
|
|
class="w-full"
|
|
:collection="genderCollection"
|
|
:disabled="formDisabled"
|
|
@value-change="setGenderValue"
|
|
>
|
|
<SelectControl>
|
|
<SelectTrigger>
|
|
<SelectValueText placeholder="Pilih jantina" />
|
|
</SelectTrigger>
|
|
</SelectControl>
|
|
<SelectContent>
|
|
<SelectItemGroup>
|
|
<SelectItemGroupLabel>Jantina</SelectItemGroupLabel>
|
|
<SelectItem
|
|
v-for="item in genderCollection.items"
|
|
:key="item.label"
|
|
:item="item"
|
|
>
|
|
<SelectItemText>{{ item.label }}</SelectItemText>
|
|
</SelectItem>
|
|
</SelectItemGroup>
|
|
</SelectContent>
|
|
</SelectRoot>
|
|
</Field>
|
|
|
|
<Field>
|
|
<FieldLabel>Status Perkahwinan</FieldLabel>
|
|
<SelectRoot
|
|
class="w-full"
|
|
:collection="marriageStatusCollection"
|
|
:disabled="formDisabled"
|
|
@value-change="setMarriageStatusValue"
|
|
>
|
|
<SelectControl>
|
|
<SelectTrigger>
|
|
<SelectValueText placeholder="Pilih status perkahwinan" />
|
|
</SelectTrigger>
|
|
</SelectControl>
|
|
<SelectContent>
|
|
<SelectItemGroup>
|
|
<SelectItemGroupLabel>Status Perkahwinan</SelectItemGroupLabel>
|
|
<SelectItem
|
|
v-for="item in marriageStatusCollection.items"
|
|
:key="item.label"
|
|
:item="item"
|
|
>
|
|
<SelectItemText>{{ item.label }}</SelectItemText>
|
|
</SelectItem>
|
|
</SelectItemGroup>
|
|
</SelectContent>
|
|
</SelectRoot>
|
|
</Field>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
<Field>
|
|
<FieldLabel for="user-member-number">Nombor Anggota</FieldLabel>
|
|
<Input
|
|
id="user-member-number"
|
|
v-model="form.member_number"
|
|
class="w-full"
|
|
type="number"
|
|
min="0"
|
|
placeholder="Nombor anggota"
|
|
:disabled="formDisabled"
|
|
required
|
|
/>
|
|
</Field>
|
|
|
|
<Field>
|
|
<FieldLabel>Jenis Anggota</FieldLabel>
|
|
<SelectRoot
|
|
class="w-full"
|
|
:collection="memberTypeCollection"
|
|
:disabled="formDisabled"
|
|
@value-change="setMemberTypeValue"
|
|
>
|
|
<SelectControl>
|
|
<SelectTrigger>
|
|
<SelectValueText placeholder="Pilih jenis anggota" />
|
|
</SelectTrigger>
|
|
</SelectControl>
|
|
<SelectContent>
|
|
<SelectItemGroup>
|
|
<SelectItemGroupLabel>Jenis Anggota</SelectItemGroupLabel>
|
|
<SelectItem
|
|
v-for="item in memberTypeCollection.items"
|
|
:key="item.label"
|
|
:item="item"
|
|
>
|
|
<SelectItemText>{{ item.label }}</SelectItemText>
|
|
</SelectItem>
|
|
</SelectItemGroup>
|
|
</SelectContent>
|
|
</SelectRoot>
|
|
</Field>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
<Field>
|
|
<FieldLabel for="user-join-date">Tarikh Sertai</FieldLabel>
|
|
<Input
|
|
id="user-join-date"
|
|
v-model="form.join_date"
|
|
class="w-full"
|
|
type="date"
|
|
:disabled="formDisabled"
|
|
required
|
|
/>
|
|
</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"
|
|
required
|
|
/>
|
|
</Field>
|
|
</div>
|
|
|
|
<Field>
|
|
<FieldLabel for="user-birth-place">Tempat Lahir</FieldLabel>
|
|
<Input
|
|
id="user-birth-place"
|
|
v-model="form.birth_place"
|
|
class="w-full"
|
|
type="text"
|
|
placeholder="Tempat lahir"
|
|
:disabled="formDisabled"
|
|
required
|
|
/>
|
|
</Field>
|
|
|
|
<div class="flex items-center justify-end gap-2 pt-2">
|
|
<Button type="submit" variant="primary" look="outline" :disabled="formDisabled">
|
|
{{ saving ? 'Mendaftar...' : 'Daftar' }}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</Box>
|
|
</div>
|
|
</template>
|