feat(unit): implement unit management flow with list and form components

- Add schema validation and default query params for unit management
- Create list view with pagination, search, and delete functionality
- Implement form entry with validation for creating/editing units
- Replace placeholder content with functional unit list component
- Remove unused unit add page and update page titles
This commit is contained in:
Khafid Prayoga
2025-09-04 13:17:06 +07:00
parent 3dbc1b8fd1
commit 39d96e7b24
8 changed files with 509 additions and 195 deletions
+89 -30
View File
@@ -1,14 +1,19 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Block from '~/components/pub/custom-ui/form/block.vue'
import { toTypedSchema } from '@vee-validate/zod'
import Combobox from '~/components/pub/custom-ui/form/combobox.vue'
import FieldGroup from '~/components/pub/custom-ui/form/field-group.vue'
import Field from '~/components/pub/custom-ui/form/field.vue'
import Label from '~/components/pub/custom-ui/form/label.vue'
interface UnitFormData {
name: string
code: string
parentId: string
}
const props = defineProps<{
modelValue: any
installation: {
unit: {
msg: {
placeholder: string
search: string
@@ -20,47 +25,101 @@ const props = defineProps<{
code: string
}[]
}
schema: any
initialValues?: Partial<UnitFormData>
errors?: FormErrors
}>()
const emit = defineEmits(['update:modelValue', 'event'])
const data = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
})
const emit = defineEmits<{
'submit': [values: UnitFormData, resetForm: () => void]
'cancel': [resetForm: () => void]
}>()
const formSchema = toTypedSchema(props.schema)
// Form submission handler
function onSubmitForm(values: any, { resetForm }: { resetForm: () => void }) {
const formData: UnitFormData = {
name: values.name || '',
code: values.code || '',
parentId: values.parentId || '',
}
emit('submit', formData, resetForm)
}
// Form cancel handler
function onCancelForm({ resetForm }: { resetForm: () => void }) {
emit('cancel', resetForm)
}
</script>
<template>
<form id="entry-form">
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
<div class="flex flex-col justify-between">
<Block>
<FieldGroup :column="2">
<Label>Nama</Label>
<Form
v-slot="{ handleSubmit, resetForm }" as="" keep-values :validation-schema="formSchema"
:initial-values="initialValues"
>
<form id="entry-form" @submit="handleSubmit($event, (values) => onSubmitForm(values, { resetForm }))">
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
<div class="flex flex-col justify-between">
<FieldGroup>
<Label label-for="name">Nama</Label>
<Field id="name" :errors="errors">
<Input v-model="data.name" />
<FormField v-slot="{ componentField }" name="name">
<FormItem>
<FormControl>
<Input
id="name" type="text" placeholder="Masukkan nama unit" autocomplete="off"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<FieldGroup :column="2">
<Label>Kode</Label>
<FieldGroup>
<Label label-for="code">Kode</Label>
<Field id="code" :errors="errors">
<Input v-model="data.code" />
<FormField v-slot="{ componentField }" name="code">
<FormItem>
<FormControl>
<Input id="code" type="text" placeholder="Masukkan kode unit" autocomplete="off" v-bind="componentField" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<FieldGroup :column="2">
<Label>Instalasi</Label>
<FieldGroup>
<Label label-for="parentId">Instalasi</Label>
<Field id="parentId" :errors="errors">
<Combobox
v-model="data.parentId"
:items="props.installation.items"
:placeholder="props.installation.msg.placeholder"
:search-placeholder="props.installation.msg.search"
:empty-message="props.installation.msg.empty"
/>
<FormField v-slot="{ componentField }" name="parentId">
<FormItem>
<FormControl>
<Combobox
id="parentId" v-bind="componentField" :items="unit.items"
:placeholder="unit.msg.placeholder" :search-placeholder="unit.msg.search"
:empty-message="unit.msg.empty"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
</Block>
</div>
</div>
</div>
</form>
<div class="flex justify-end gap-2 mt-4">
<Button type="button" variant="outline" @click="onCancelForm({ resetForm })">
Batal
</Button>
<Button type="submit">
Simpan
</Button>
</div>
</form>
</Form>
</template>
+86
View File
@@ -0,0 +1,86 @@
import type {
Col,
KeyLabel,
RecComponent,
RecStrFuncComponent,
RecStrFuncUnknown,
Th,
} from '~/components/pub/custom-ui/data/types'
import { defineAsyncComponent } from 'vue'
type SmallDetailDto = any
const action = defineAsyncComponent(() => import('~/components/pub/custom-ui/data/dropdown-action-ud.vue'))
export const cols: Col[] = [
{ width: 100 },
{ },
{ },
{ },
{ width: 50 },
]
export const header: Th[][] = [
[
{ label: 'Id' },
{ label: 'Nama' },
{ label: 'Kode' },
{ label: 'Instalasi' },
{ label: '' },
],
]
export const keys = [
'id',
'firstName',
'cellphone',
'birth_place',
'action',
]
export const delKeyNames: KeyLabel[] = [
{ key: 'code', label: 'Kode' },
{ key: 'name', label: 'Nama' },
]
export const funcParsed: RecStrFuncUnknown = {
name: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
return `${recX.frontTitle} ${recX.name} ${recX.endTitle}`.trim()
},
identity_number: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
if (recX.identity_number?.substring(0, 5) === 'BLANK') {
return '(TANPA NIK)'
}
return recX.identity_number
},
inPatient_itemPrice: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
return Number(recX.inPatient_itemPrice.price).toLocaleString('id-ID')
},
outPatient_itemPrice: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
return Number(recX.outPatient_itemPrice.price).toLocaleString('id-ID')
},
}
export const funcComponent: RecStrFuncComponent = {
action(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: action,
props: {
size: 'sm',
},
}
return res
},
}
export const funcHtml: RecStrFuncUnknown = {
patient_address(_rec) {
return '-'
},
}
+24 -43
View File
@@ -1,54 +1,35 @@
<script setup lang="ts">
// #region Props & Emits
const props = defineProps<{
title: string
}>()
import type { PaginationMeta } from '~/components/pub/custom-ui/pagination/pagination.type'
import { cols, funcComponent, funcHtml, funcParsed, header, keys } from './list-cfg'
interface Props {
data: any[]
paginationMeta?: PaginationMeta
}
defineProps<Props>()
const emit = defineEmits<{
(e: 'update', value: string): void
pageChange: [page: number]
}>()
// #endregion
// #region State & Computed
// =============================
const count = ref(0)
const double = computed(() => count.value * 2)
// #endregion
// #region Lifecycle Hooks
onMounted(() => {
// init code
fetchData()
})
// #endregion
// #region Functions
async function fetchData() {
console.log('fetched')
function handlePageChange(page: number) {
emit('pageChange', page)
}
// #endregion region
// #region Utilities & event handlers
function increment() {
count.value++
}
// #endregion
// #region Watchers
watch(count, (newVal, oldVal) => {
console.log('count changed:', oldVal, '->', newVal)
})
// #endregion
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<p>Double: {{ double }}</p>
<button @click="increment">+1</button>
<div class="space-y-4">
<PubBaseDataTable
:rows="data" :cols="cols" :header="header" :keys="keys" :func-parsed="funcParsed"
:func-html="funcHtml" :func-component="funcComponent" :skeleton-size="paginationMeta?.pageSize"
/>
<template v-if="paginationMeta">
<div v-if="paginationMeta.totalPage > 1">
<PubCustomUiPagination :pagination-meta="paginationMeta" @page-change="handlePageChange" />
</div>
</template>
</div>
</template>
<style scoped>
/* component style */
</style>