feat(specialist-position): implement crud operations for specialist positions

add handler, service, schema and update components for specialist position management
update list configuration and form to handle specialist relations
This commit is contained in:
Khafid Prayoga
2025-10-31 10:55:45 +07:00
parent bf441ff714
commit c340de3114
6 changed files with 143 additions and 53 deletions
@@ -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 { SpecialistPositionFormData } from '~/schemas/specialist-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 { genSpecialistPosition } from '~/models/specialist-position'
interface Props {
schema: z.ZodSchema<any>
divisionId: number
specialists: 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: SpecialistPositionFormData, resetForm: () => void]
cancel: [resetForm: () => void]
}>()
const { defineField, errors, meta } = useForm({
validationSchema: toTypedSchema(props.schema),
initialValues: genDivisionPosition() as Partial<DivisionPositionFormData>,
initialValues: genSpecialistPosition() as Partial<SpecialistPositionFormData>,
})
const [code, codeAttrs] = defineField('code')
const [name, nameAttrs] = defineField('name')
const [specialist, specialistAttrs] = defineField('specialist_id')
const [employee, employeeAttrs] = defineField('employee_id')
const [headStatus, headStatusAttrs] = defineField('headStatus')
@@ -62,6 +62,8 @@ 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.specialist_id !== undefined)
specialist.value = props.values.specialist_id ? Number(props.values.specialist_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 +72,18 @@ if (props.values) {
const resetForm = () => {
code.value = ''
name.value = ''
specialist.value = null
employee.value = null
headStatus.value = false
}
// Form submission handler
function onSubmitForm() {
const formData: DivisionPositionFormData = {
const formData: SpecialistPositionFormData = {
...genBase(),
name: name.value || '',
code: code.value || '',
// readonly based on detail division
division_id: props.divisionId,
specialist_id: specialist.value || null,
employee_id: employee.value || null,
headStatus: headStatus.value !== undefined ? headStatus.value : undefined,
}
@@ -98,7 +98,7 @@ function onCancelForm() {
<template>
<form
id="form-division-position"
id="form-specialist-position"
@submit.prevent
>
<Block
@@ -107,7 +107,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 +118,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 +129,22 @@ function onCancelForm() {
</Field>
</Cell>
<Cell>
<Label height="compact">Pengisi Jabatan</Label>
<Label height="compact">Spesialis</Label>
<Field :errMessage="errors.specialist_id">
<Combobox
id="specialist"
v-model="specialist"
v-bind="specialistAttrs"
:items="specialists"
:is-disabled="isLoading || isReadonly"
placeholder="Pilih Spesialis"
search-placeholder="Cari Spesialis"
empty-message="Item tidak ditemukan"
/>
</Field>
</Cell>
<Cell>
<Label height="compact">Karyawan</Label>
<Field :errMessage="errors.employee_id">
<Combobox
id="employee"
@@ -13,14 +13,14 @@ export const config: Config = {
[
{ label: 'Kode Posisi' },
{ label: 'Nama Posisi' },
{ label: 'Nama Divisi ' },
{ label: 'Nama Spesialis ' },
{ label: 'Karyawan' },
{ label: 'Status Kepala' },
{ label: '' },
],
],
keys: ['code', 'name', 'division', 'employee', 'head', 'action'],
keys: ['code', 'name', 'specialist.name', 'employee', 'head', 'action'],
delKeyNames: [
{ key: 'code', label: 'Kode' },
@@ -28,10 +28,6 @@ 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 fullName = [recX.employee?.person.frontTitle, recX.employee?.person.name, recX.employee?.person.endTitle]
@@ -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 AppSpecialistList from '~/components/app/specialist/list.vue'
import AppSpecialistEntryForm from '~/components/app/specialist/entry-form.vue'
import AppSpecialistPositionList from '~/components/app/specialist-position/list.vue'
import AppSpecialistPositionEntryForm from '~/components/app/specialist-position/entry-form.vue'
// Helpers
import { usePaginatedList } from '~/composables/usePaginatedList'
import { toast } from '~/components/pub/ui/toast'
import { config } from '~/components/app/specialist-position/list.cfg'
// Types
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
import { SpecialistSchema, type SpecialistFormData } from '~/schemas/specialist.schema'
import { type SpecialistPositionFormData, SpecialistPositionSchema } from '~/schemas/specialist-position.schema'
// Handlers
import {
@@ -28,13 +29,15 @@ import {
handleActionEdit,
handleActionRemove,
handleCancelForm,
} from '~/handlers/specialist.handler'
} from '~/handlers/specialist-position.handler'
// Services
import { getList, getDetail } from '~/services/specialist.service'
import { getValueLabelList as getUnitList } from '~/services/unit.service'
import { getList, getDetail } from '~/services/specialist-position.service'
import { getValueLabelList as getValueLabelSpecialistList } from '~/services/specialist.service'
import { getValueLabelList as getEmployeeLabelList } from '~/services/employee.service'
const units = ref<{ value: string | number; label: string }[]>([])
const specialists = ref<{ value: string | number; label: string }[]>([])
const employees = ref<{ value: string | number; label: string }[]>([])
const title = ref('')
const {
@@ -52,11 +55,11 @@ const {
sort: 'createdAt:asc',
'page-number': params['page-number'] || 0,
'page-size': params['page-size'] || 10,
includes: 'unit',
includes: 'specialist,Employee.Person',
})
return { success: result.success || false, body: result.body || {} }
},
entityName: 'specialist',
entityName: 'specialist-position',
})
const headerPrep: HeaderPrep = {
@@ -98,18 +101,17 @@ const getCurrentSpecialistDetail = async (id: number | string) => {
isFormEntryDialogOpen.value = true
}
}
// Watch for row actions when recId or recAction changes
watch([recId, recAction], () => {
switch (recAction.value) {
case ActionEvents.showDetail:
getCurrentSpecialistDetail(recId.value)
title.value = 'Detail Spesialis'
title.value = 'Detail Spesialis Posisi'
isReadonly.value = true
break
case ActionEvents.showEdit:
getCurrentSpecialistDetail(recId.value)
title.value = 'Edit Spesialis'
title.value = 'Edit Spesialis Posisi'
isReadonly.value = false
break
case ActionEvents.showConfirmDelete:
@@ -119,8 +121,18 @@ watch([recId, recAction], () => {
})
onMounted(async () => {
units.value = await getUnitList()
await getSpecialistList()
try {
specialists.value = await getValueLabelSpecialistList({ 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>
@@ -131,13 +143,11 @@ onMounted(async () => {
:ref-search-nav="headerPrep.refSearchNav"
@search="handleSearch"
/>
<AppSpecialistPositionList
:data="data"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
<Dialog
v-model:open="isFormEntryDialogOpen"
:title="!!recItem ? title : 'Tambah Spesialis'"
@@ -151,13 +161,14 @@ onMounted(async () => {
"
>
<AppSpecialistPositionEntryForm
:schema="SpecialistSchema"
:units="units"
:schema="SpecialistPositionSchema"
:specialists="specialists"
:employees="employees"
:values="recItem"
:is-loading="isProcessing"
:is-readonly="isReadonly"
@submit="
(values: SpecialistFormData | Record<string, any>, resetForm: () => void) => {
(values: SpecialistPositionFormData | Record<string, any>, resetForm: () => void) => {
if (recId > 0) {
handleActionEdit(recId, values, getSpecialistList, resetForm, toast)
return
@@ -168,7 +179,6 @@ onMounted(async () => {
@cancel="handleCancelForm"
/>
</Dialog>
<!-- Record Confirmation Modal -->
<RecordConfirmation
v-model:open="isRecordConfirmationOpen"
@@ -178,18 +188,14 @@ onMounted(async () => {
@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>
@@ -0,0 +1,24 @@
// Handlers
import { genCrudHandler } from '~/handlers/_handler'
// Services
import { create, update, remove } from '~/services/specialist-position.service'
export const {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} = genCrudHandler({
create,
update,
remove,
})
+24
View File
@@ -0,0 +1,24 @@
import { z } from 'zod'
import type { SpecialistPosition } from '~/models/specialist-position'
const SpecialistPositionSchema = 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(),
specialist_id: z
.union([
z.string({ required_error: 'Spesialis harus diisi' }),
z.number({ required_error: 'Spesialis 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 SpecialistPositionFormData = z.infer<typeof SpecialistPositionSchema> & Partial<SpecialistPosition>
export { SpecialistPositionSchema }
export type { SpecialistPositionFormData }
@@ -0,0 +1,25 @@
// Base
import * as base from './_crud-base'
const path = '/api/v1/specialist-position'
const name = 'specialist-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)
}