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

- Add new handler, service, and schema files for subspecialist position
- Update list configuration and entry form components
- Modify list view to display subspecialist position data
- Include subspecialist relation in position model
This commit is contained in:
Khafid Prayoga
2025-10-31 14:57:45 +07:00
parent d3090d04ef
commit ba0ac753b2
8 changed files with 162 additions and 55 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 { SubSpecialistPositionFormData } from '~/schemas/subspecialist-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 { genSubSpecialistPosition } from '~/models/subspecialist-position'
interface Props {
schema: z.ZodSchema<any>
divisionId: number
subSpecialists: 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: SubSpecialistPositionFormData, resetForm: () => void]
cancel: [resetForm: () => void]
}>()
const { defineField, errors, meta } = useForm({
validationSchema: toTypedSchema(props.schema),
initialValues: genDivisionPosition() as Partial<DivisionPositionFormData>,
initialValues: genSubSpecialistPosition() as Partial<SubSpecialistPositionFormData>,
})
const [code, codeAttrs] = defineField('code')
const [name, nameAttrs] = defineField('name')
const [subSpecialist, subSpecialistAttrs] = defineField('subspecialist_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.subspecialist_id !== undefined)
subSpecialist.value = props.values.subspecialist_id ? Number(props.values.subspecialist_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 = ''
subSpecialist.value = null
employee.value = null
headStatus.value = false
}
// Form submission handler
function onSubmitForm() {
const formData: DivisionPositionFormData = {
const formData: SubSpecialistPositionFormData = {
...genBase(),
name: name.value || '',
code: code.value || '',
// readonly based on detail division
division_id: props.divisionId,
subspecialist_id: subSpecialist.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">Sub Spesialis</Label>
<Field :errMessage="errors.subspecialist_id">
<Combobox
id="specialist"
v-model="subSpecialist"
v-bind="subSpecialistAttrs"
:items="subSpecialists"
:is-disabled="isLoading || isReadonly"
placeholder="Pilih Sub Spesialis"
search-placeholder="Cari Sub Spesialis"
empty-message="Item tidak ditemukan"
/>
</Field>
</Cell>
<Cell>
<Label height="compact">Karyawan</Label>
<Field :errMessage="errors.employee_id">
<Combobox
id="employee"
@@ -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 { SubSpecialistPosition } from '~/models/subspecialist-position'
type SmallDetailDto = any
@@ -13,14 +13,14 @@ export const config: Config = {
[
{ label: 'Kode Posisi' },
{ label: 'Nama Posisi' },
{ label: 'Nama Divisi ' },
{ label: 'Nama Sub Spesialis ' },
{ label: 'Karyawan' },
{ label: 'Status Kepala' },
{ label: '' },
],
],
keys: ['code', 'name', 'division', 'employee', 'head', 'action'],
keys: ['code', 'name', 'subspecialist', 'employee', 'head', 'action'],
delKeyNames: [
{ key: 'code', label: 'Kode' },
@@ -28,12 +28,12 @@ export const config: Config = {
],
parses: {
division: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
return recX.division?.name || '-'
subspecialist: (rec: unknown): unknown => {
const recX = rec as SubSpecialistPosition
return recX.subspecialist?.name || '-'
},
employee: (rec: unknown): unknown => {
const recX = rec as DivisionPosition
const recX = rec as SubSpecialistPosition
const fullName = [recX.employee?.person.frontTitle, recX.employee?.person.name, recX.employee?.person.endTitle]
.filter(Boolean)
.join(' ')
+4 -1
View File
@@ -31,6 +31,9 @@ function handlePageChange(page: number) {
:rows="data"
:skeleton-size="paginationMeta?.pageSize"
/>
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
<PaginationView
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
</div>
</template>
@@ -3,16 +3,20 @@
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 AppSubSpecialistList from '~/components/app/subspecialist/list.vue'
import AppSubSpecialistEntryForm from '~/components/app/subspecialist/entry-form.vue'
import AppSubSpecialistPositionList from '~/components/app/subspecialist-position/list.vue'
import AppSubSpecialistPositionEntryForm from '~/components/app/subspecialist-position/entry-form.vue'
// Helpers
import { usePaginatedList } from '~/composables/usePaginatedList'
import { toast } from '~/components/pub/ui/toast'
import { config } from '~/components/app/subspecialist-position/list.cfg'
// Types
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
import { SubspecialistSchema, type SubspecialistFormData } from '~/schemas/subspecialist.schema'
import {
type SubSpecialistPositionFormData,
SubSpecialistPositionSchema,
} from '~/schemas/subspecialist-position.schema'
// Handlers
import {
@@ -28,13 +32,15 @@ import {
handleActionEdit,
handleActionRemove,
handleCancelForm,
} from '~/handlers/subspecialist.handler'
} from '~/handlers/subspecialist-position.handler'
// Services
import { getList, getDetail } from '~/services/subspecialist.service'
import { getValueLabelList as getSpecialistsList } from '~/services/specialist.service'
import { getList, getDetail } from '~/services/subspecialist-position.service'
import { getValueLabelList as getValueLabelSubSpecialistList } from '~/services/subspecialist.service'
import { getValueLabelList as getEmployeeLabelList } from '~/services/employee.service'
const specialists = ref<{ value: string | number; label: string }[]>([])
const subSpecialists = ref<{ value: string | number; label: string }[]>([])
const employees = ref<{ value: string | number; label: string }[]>([])
const title = ref('')
const {
@@ -52,11 +58,11 @@ const {
sort: 'createdAt:asc',
'page-number': params['page-number'] || 0,
'page-size': params['page-size'] || 10,
includes: 'specialist',
includes: 'subspecialist,Employee.Person',
})
return { success: result.success || false, body: result.body || {} }
},
entityName: 'subspecialist',
entityName: 'subspecialist-position',
})
const headerPrep: HeaderPrep = {
@@ -92,6 +98,7 @@ provide('table_data_loader', isLoading)
const getCurrentSubSpecialistDetail = async (id: number | string) => {
const result = await getDetail(id)
console.log(result)
if (result.success) {
const currentValue = result.body?.data || {}
recItem.value = currentValue
@@ -104,12 +111,12 @@ watch([recId, recAction], () => {
switch (recAction.value) {
case ActionEvents.showDetail:
getCurrentSubSpecialistDetail(recId.value)
title.value = 'Detail Sub Spesialis'
title.value = 'Detail Sub Spesialis Posisi'
isReadonly.value = true
break
case ActionEvents.showEdit:
getCurrentSubSpecialistDetail(recId.value)
title.value = 'Edit Sub Spesialis'
title.value = 'Edit Sub Spesialis Posisi'
isReadonly.value = false
break
case ActionEvents.showConfirmDelete:
@@ -119,8 +126,18 @@ watch([recId, recAction], () => {
})
onMounted(async () => {
specialists.value = await getSpecialistsList()
await getSubSpecialistList()
try {
subSpecialists.value = await getValueLabelSubSpecialistList({ 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>
@@ -132,7 +149,7 @@ onMounted(async () => {
@search="handleSearch"
/>
<AppSubspecialistPositionList
<AppSubSpecialistPositionList
:data="data"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
@@ -150,19 +167,20 @@ onMounted(async () => {
}
"
>
<AppSubspecialistPositionEntryForm
:schema="SubspecialistSchema"
:specialists="specialists"
<AppSubSpecialistPositionEntryForm
:schema="SubSpecialistPositionSchema"
:sub-specialists="subSpecialists"
:employees="employees"
:values="recItem"
:is-loading="isProcessing"
:is-readonly="isReadonly"
@submit="
(values: SubspecialistFormData | Record<string, any>, resetForm: () => void) => {
(values: SubSpecialistPositionFormData | Record<string, any>, resetForm: () => void) => {
if (recId > 0) {
handleActionEdit(recId, values, getSubSpecialistList, resetForm, toast)
handleActionEdit(recId, values, getSubSpecialistList, onResetState, toast)
return
}
handleActionSave(values, getSubSpecialistList, resetForm, toast)
handleActionSave(values, getSubSpecialistList, onResetState, toast)
}
"
@cancel="handleCancelForm"
@@ -178,18 +196,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/subspecialist-position.service'
export const {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} = genCrudHandler({
create,
update,
remove,
})
+2
View File
@@ -1,5 +1,6 @@
import { type Base, genBase } from './_base'
import type { Employee } from './employee'
import type { Subspecialist } from './subspecialist'
export interface SubSpecialistPosition extends Base {
subspecialist_id: number
@@ -8,6 +9,7 @@ export interface SubSpecialistPosition extends Base {
headStatus?: boolean
employee_id?: number
subspecialist?: Subspecialist | null
employee?: Employee | null
}
@@ -0,0 +1,24 @@
import { z } from 'zod'
import type { SubSpecialistPosition } from '~/models/subspecialist-position'
const SubSpecialistPositionSchema = 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(),
subspecialist_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 SubSpecialistPositionFormData = z.infer<typeof SubSpecialistPositionSchema> & Partial<SubSpecialistPosition>
export { SubSpecialistPositionSchema }
export type { SubSpecialistPositionFormData }
@@ -0,0 +1,25 @@
// Base
import * as base from './_crud-base'
const path = '/api/v1/subspecialist-position'
const name = 'subspecialist-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)
}