feat(unit-position): implement crud operations and update ui components

- Add new handler, service, and schema files for unit-position
- Update list configuration and entry form components
- Modify page title and integrate employee relation
- Implement CRUD operations with proper validation
This commit is contained in:
Khafid Prayoga
2025-10-30 12:14:32 +07:00
parent 59f1def565
commit 1dc42be406
8 changed files with 153 additions and 58 deletions
+29 -15
View File
@@ -7,18 +7,18 @@ import Label from '~/components/pub/my-ui/doc-entry/label.vue'
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
// Types
import type { DivisionPositionFormData } from '~/schemas/division-position.schema'
import type { UnitPositionFormData } from '~/schemas/unit-position.schema'
// Helpers
import type z from 'zod'
import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
import { genBase } from '~/models/_base'
import { genDivisionPosition } from '~/models/division-position'
import { genUnitPosition } from '~/models/unit-position'
interface Props {
schema: z.ZodSchema<any>
divisionId: number
units: any[]
employees: any[]
values: any
isLoading?: boolean
@@ -26,21 +26,21 @@ interface Props {
}
const props = defineProps<Props>()
const isLoading = props.isLoading !== undefined ? props.isLoading : false
const isReadonly = props.isReadonly !== undefined ? props.isReadonly : false
const emit = defineEmits<{
submit: [values: DivisionPositionFormData, resetForm: () => void]
submit: [values: UnitPositionFormData, resetForm: () => void]
cancel: [resetForm: () => void]
}>()
const { defineField, errors, meta } = useForm({
validationSchema: toTypedSchema(props.schema),
initialValues: genDivisionPosition() as Partial<DivisionPositionFormData>,
initialValues: genUnitPosition() as Partial<UnitPositionFormData>,
})
const [code, codeAttrs] = defineField('code')
const [name, nameAttrs] = defineField('name')
const [unit, unitAttrs] = defineField('unit_id')
const [employee, employeeAttrs] = defineField('employee_id')
const [headStatus, headStatusAttrs] = defineField('headStatus')
@@ -62,6 +62,7 @@ const headStatusStr = computed<string>({
if (props.values) {
if (props.values.code !== undefined) code.value = props.values.code
if (props.values.name !== undefined) name.value = props.values.name
if (props.values.unit_id !== undefined) unit.value = props.values.unit_id ? Number(props.values.unit_id) : null
if (props.values.employee_id !== undefined)
employee.value = props.values.employee_id ? Number(props.values.employee_id) : null
if (props.values.headStatus !== undefined) headStatus.value = !!props.values.headStatus
@@ -70,20 +71,18 @@ if (props.values) {
const resetForm = () => {
code.value = ''
name.value = ''
unit.value = null
employee.value = null
headStatus.value = false
}
// Form submission handler
function onSubmitForm() {
const formData: DivisionPositionFormData = {
const formData: UnitPositionFormData = {
...genBase(),
name: name.value || '',
code: code.value || '',
// readonly based on detail division
division_id: props.divisionId,
unit_id: unit.value || null,
employee_id: employee.value || null,
headStatus: headStatus.value !== undefined ? headStatus.value : undefined,
}
@@ -98,7 +97,7 @@ function onCancelForm() {
<template>
<form
id="form-division-position"
id="form-unit-position"
@submit.prevent
>
<Block
@@ -107,7 +106,7 @@ function onCancelForm() {
:colCount="1"
>
<Cell>
<Label height="compact">Kode Jabatan</Label>
<Label height="compact">Kode</Label>
<Field :errMessage="errors.code">
<Input
id="code"
@@ -118,7 +117,7 @@ function onCancelForm() {
</Field>
</Cell>
<Cell>
<Label height="compact">Nama Jabatan</Label>
<Label height="compact">Nama Posisi</Label>
<Field :errMessage="errors.name">
<Input
id="name"
@@ -129,7 +128,22 @@ function onCancelForm() {
</Field>
</Cell>
<Cell>
<Label height="compact">Pengisi Jabatan</Label>
<Label height="compact">Unit</Label>
<Field :errMessage="errors.unit_id">
<Combobox
id="unit"
v-model="unit"
v-bind="unitAttrs"
:items="units"
:is-disabled="isLoading || isReadonly"
placeholder="Pilih Unit"
search-placeholder="Cari Unit"
empty-message="Item tidak ditemukan"
/>
</Field>
</Cell>
<Cell>
<Label height="compact">Karyawan</Label>
<Field :errMessage="errors.employee_id">
<Combobox
id="employee"
+4 -8
View File
@@ -1,6 +1,6 @@
import type { Config, RecComponent } from '~/components/pub/my-ui/data-table'
import { defineAsyncComponent } from 'vue'
import type { DivisionPosition } from '~/models/division-position'
import type { UnitPosition } from '~/models/unit-position'
type SmallDetailDto = any
@@ -13,14 +13,14 @@ export const config: Config = {
[
{ label: 'Kode Posisi' },
{ label: 'Nama Posisi' },
{ label: 'Nama Divisi ' },
{ label: 'Nama Unit ' },
{ label: 'Karyawan' },
{ label: 'Status Kepala' },
{ label: '' },
],
],
keys: ['code', 'name', 'division', 'employee', 'head', 'action'],
keys: ['code', 'name', 'unit.name', 'employee', 'head', 'action'],
delKeyNames: [
{ key: 'code', label: 'Kode' },
@@ -28,12 +28,8 @@ export const config: Config = {
],
parses: {
division: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
return recX.division?.name || '-'
},
employee: (rec: unknown): unknown => {
const recX = rec as DivisionPosition
const recX = rec as UnitPosition
const fullName = [recX.employee?.person.frontTitle, recX.employee?.person.name, recX.employee?.person.endTitle]
.filter(Boolean)
.join(' ')
+43 -34
View File
@@ -3,16 +3,17 @@
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
import AppUnitList from '~/components/app/unit/list.vue'
import AppUnitEntryForm from '~/components/app/unit/entry-form.vue'
import AppUnitPositionList from '~/components/app/unit-position/list.vue'
import AppUnitPositionEntryForm from '~/components/app/unit-position/entry-form.vue'
// Helpers
import { usePaginatedList } from '~/composables/usePaginatedList'
import { toast } from '~/components/pub/ui/toast'
import { config } from '~/components/app/unit-position/list.cfg'
// Types
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
import { UnitSchema, type UnitFormData } from '~/schemas/unit.schema'
import { type UnitPositionFormData, UnitPositionSchema } from '~/schemas/unit-position.schema'
// Handlers
import {
@@ -28,13 +29,15 @@ import {
handleActionEdit,
handleActionRemove,
handleCancelForm,
} from '~/handlers/unit.handler'
} from '~/handlers/unit-position.handler'
// Services
import { getList, getDetail } from '~/services/unit.service'
import { getValueLabelList as getInstallationList } from '~/services/installation.service'
import { getList, getDetail } from '~/services/unit-position.service'
import { getValueLabelList as getValueLabelUnitList } from '~/services/unit.service'
import { getValueLabelList as getEmployeeLabelList } from '~/services/employee.service'
const installations = ref<{ value: string; label: string }[]>([])
const units = ref<{ value: string; label: string }[]>([])
const employees = ref<{ value: string | number; label: string }[]>([])
const title = ref('')
const {
@@ -44,7 +47,7 @@ const {
searchInput,
handlePageChange,
handleSearch,
fetchData: getUnitList,
fetchData: getListUnit,
} = usePaginatedList({
fetchFn: async (params: any) => {
const result = await getList({
@@ -52,11 +55,11 @@ const {
sort: 'createdAt:asc',
'page-number': params['page-number'] || 0,
'page-size': params['page-size'] || 10,
includes: 'installation',
includes: 'unit,Employee.Person',
})
return { success: result.success || false, body: result.body || {} }
},
entityName: 'unit',
entityName: 'unit-position',
})
const headerPrep: HeaderPrep = {
@@ -104,12 +107,12 @@ watch([recId, recAction], () => {
switch (recAction.value) {
case ActionEvents.showDetail:
getCurrentUnitDetail(recId.value)
title.value = 'Detail Unit'
title.value = 'Detail Unit Posisi'
isReadonly.value = true
break
case ActionEvents.showEdit:
getCurrentUnitDetail(recId.value)
title.value = 'Edit Unit'
title.value = 'Edit Unit Posisi'
isReadonly.value = false
break
case ActionEvents.showConfirmDelete:
@@ -119,8 +122,18 @@ watch([recId, recAction], () => {
})
onMounted(async () => {
installations.value = await getInstallationList()
await getUnitList()
try {
units.value = await getValueLabelUnitList({ sort: 'createdAt:asc', 'page-size': 100 })
employees.value = await getEmployeeLabelList({ sort: 'createdAt:asc', 'page-size': 100, includes: 'person' })
} catch (err) {
console.log(err)
// show toast
toast({
title: 'Terjadi Kesalahan',
description: 'Terjadi kesalahan saat memuat data',
variant: 'destructive',
})
}
})
</script>
@@ -140,7 +153,7 @@ onMounted(async () => {
<Dialog
v-model:open="isFormEntryDialogOpen"
:title="!!recItem ? title : 'Tambah Unit'"
:title="!!recItem ? title : 'Tambah Unit Posisi'"
size="lg"
prevent-outside
@update:open="
@@ -151,45 +164,41 @@ onMounted(async () => {
"
>
<AppUnitPositionEntryForm
:schema="UnitSchema"
:installations="installations"
:schema="UnitPositionSchema"
:units="units"
:employees="employees"
:values="recItem"
:is-loading="isProcessing"
:is-readonly="isReadonly"
@submit="
(values: UnitFormData | Record<string, any>, resetForm: () => void) => {
(values: UnitPositionFormData | Record<string, any>, resetForm: () => void) => {
if (recId > 0) {
handleActionEdit(recId, values, getUnitList, resetForm, toast)
handleActionEdit(recId, values, getListUnit, resetForm, toast)
return
}
handleActionSave(values, getUnitList, resetForm, toast)
handleActionSave(values, getListUnit, resetForm, toast)
}
"
@cancel="handleCancelForm"
/>
</Dialog>
<!-- Record Confirmation Modal -->
<RecordConfirmation
v-model:open="isRecordConfirmationOpen"
action="delete"
:record="recItem"
@confirm="() => handleActionRemove(recId, getUnitList, toast)"
@confirm="() => handleActionRemove(recId, getListUnit, toast)"
@cancel=""
>
<template #default="{ record }">
<div class="text-sm">
<p>
<strong>ID:</strong>
{{ record?.id }}
</p>
<p v-if="record?.name">
<strong>Nama:</strong>
{{ record.name }}
</p>
<p v-if="record?.code">
<strong>Kode:</strong>
{{ record.code }}
<div class="space-y-1 text-sm">
<p
v-for="field in config.delKeyNames"
:key="field.key"
:v-if="record?.[field.key]"
>
<span class="font-semibold">{{ field.label }}:</span>
{{ record[field.key] }}
</p>
</div>
</template>
+24
View File
@@ -0,0 +1,24 @@
// Handlers
import { genCrudHandler } from '~/handlers/_handler'
// Services
import { create, update, remove } from '~/services/unit-position.service'
export const {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} = genCrudHandler({
create,
update,
remove,
})
+3
View File
@@ -1,4 +1,5 @@
import { type Base, genBase } from './_base'
import type { Employee } from './employee'
export interface UnitPosition extends Base {
unit_id: number
@@ -6,6 +7,8 @@ export interface UnitPosition extends Base {
name: string
headStatus?: boolean
employee_id?: number
employee?: Employee | null
}
export function genUnitPosition(): UnitPosition {
@@ -6,7 +6,7 @@ import Error from '~/components/pub/my-ui/error/error.vue'
definePageMeta({
// middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Daftar Unit',
title: 'Daftar Unit Posisi',
contentFrame: 'cf-container-lg',
})
+24
View File
@@ -0,0 +1,24 @@
import { z } from 'zod'
import type { UnitPosition } from '~/models/unit-position'
const UnitPositionSchema = z.object({
code: z.string({ required_error: 'Kode harus diisi' }).min(1, 'Kode minimum 1 karakter'),
name: z.string({ required_error: 'Nama harus diisi' }).min(1, 'Nama minimum 1 karakter'),
headStatus: z.boolean().optional().nullable(),
unit_id: z
.union([
z.string({ required_error: 'Unit Induk harus diisi' }),
z.number({ required_error: 'Unit Induk harus diisi' }),
])
.optional()
.nullable(),
employee_id: z
.union([z.string({ required_error: 'Karyawan harus diisi' }), z.number({ required_error: 'Karyawan harus diisi' })])
.optional()
.nullable(),
})
type UnitPositionFormData = z.infer<typeof UnitPositionSchema> & Partial<UnitPosition>
export { UnitPositionSchema }
export type { UnitPositionFormData }
+25
View File
@@ -0,0 +1,25 @@
// Base
import * as base from './_crud-base'
const path = '/api/v1/unit-position'
const name = 'unit-position'
export function create(data: any) {
return base.create(path, data, name)
}
export function getList(params: any = null) {
return base.getList(path, params, name)
}
export function getDetail(id: number | string) {
return base.getDetail(path, id, name)
}
export function update(id: number | string, data: any) {
return base.update(path, id, data, name)
}
export function remove(id: number | string) {
return base.remove(path, id, name)
}