Merge pull request #99 from dikstub-rssa/feat/fe-integrasi-2-90

Integrasi Part 2
This commit is contained in:
Munawwirul Jamal
2025-10-01 03:15:58 +07:00
committed by GitHub
80 changed files with 3725 additions and 2083 deletions
+102
View File
@@ -0,0 +1,102 @@
<script setup lang="ts">
// Components
import Block from '~/components/pub/custom-ui/doc-entry/block.vue'
import Cell from '~/components/pub/custom-ui/doc-entry/cell.vue'
import Field from '~/components/pub/custom-ui/doc-entry/field.vue'
import Label from '~/components/pub/custom-ui/doc-entry/label.vue'
// Types
import type { DivisionFormData } from '~/schemas/division.schema.ts'
// Helpers
import type z from 'zod'
import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
interface Props {
schema: z.ZodSchema<any>
values: any
isLoading?: boolean
isReadonly?: boolean
}
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: DivisionFormData, resetForm: () => void]
cancel: [resetForm: () => void]
}>()
const { defineField, errors, meta } = useForm({
validationSchema: toTypedSchema(props.schema),
initialValues: {
code: '',
name: '',
division_id: '',
} as Partial<DivisionFormData>,
})
const [code, codeAttrs] = defineField('code')
const [name, nameAttrs] = defineField('name')
const [division, divisionAttrs] = defineField('division_id')
// Fill fields from props.values if provided
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.division_id !== undefined) division.value = props.values.division_id
}
const resetForm = () => {
code.value = ''
name.value = ''
division.value = ''
}
// Form submission handler
function onSubmitForm(values: any) {
const formData: DivisionFormData = {
name: name.value || '',
code: code.value || '',
division_id: division.value || '',
}
emit('submit', formData, resetForm)
}
// Form cancel handler
function onCancelForm() {
emit('cancel', resetForm)
}
</script>
<template>
<form id="form-division" @submit.prevent>
<Block labelSize="thin" class="!mb-2.5 !pt-0 xl:!mb-3" :colCount="1">
<Cell>
<Label height="compact">Kode</Label>
<Field :errMessage="errors.code">
<Input id="code" v-model="code" v-bind="codeAttrs" :disabled="isLoading || isReadonly" />
</Field>
</Cell>
<Cell>
<Label height="compact">Nama</Label>
<Field :errMessage="errors.name">
<Input id="name" v-model="name" v-bind="nameAttrs" :disabled="isLoading || isReadonly" />
</Field>
</Cell>
</Block>
<div class="my-2 flex justify-end gap-2 py-2">
<Button type="button" variant="secondary" class="w-[120px]" @click="onCancelForm"> Kembali </Button>
<Button
v-if="!isReadonly"
type="button"
class="w-[120px]"
:disabled="isLoading || !meta.valid"
@click="onSubmitForm"
>
Simpan
</Button>
</div>
</form>
</template>
@@ -48,8 +48,4 @@ export const funcComponent: RecStrFuncComponent = {
}, },
} }
export const funcHtml: RecStrFuncUnknown = { export const funcHtml: RecStrFuncUnknown = {}
patient_address(_rec) {
return '-'
},
}
+1 -1
View File
@@ -6,7 +6,7 @@ import Field from '~/components/pub/custom-ui/doc-entry/field.vue'
import Label from '~/components/pub/custom-ui/doc-entry/label.vue' import Label from '~/components/pub/custom-ui/doc-entry/label.vue'
// Types // Types
import type { MaterialFormData } from '~/schemas/material.schema' import type { MaterialFormData } from '~/schemas/material.schema.ts'
// Helpers // Helpers
import type z from 'zod' import type z from 'zod'
@@ -8,7 +8,7 @@ import Label from '~/components/pub/custom-ui/doc-entry/label.vue'
import Button from '~/components/pub/ui/button/Button.vue' import Button from '~/components/pub/ui/button/Button.vue'
// Types // Types
import type { MedicineBaseFormData } from '~/schemas/medicine.schema' import type { BaseFormData } from '~/schemas/base.schema.ts'
// Helpers // Helpers
import type z from 'zod' import type z from 'zod'
@@ -26,7 +26,7 @@ const props = defineProps<Props>()
const isLoading = props.isLoading !== undefined ? props.isLoading : false const isLoading = props.isLoading !== undefined ? props.isLoading : false
const isReadonly = props.isReadonly !== undefined ? props.isReadonly : false const isReadonly = props.isReadonly !== undefined ? props.isReadonly : false
const emit = defineEmits<{ const emit = defineEmits<{
submit: [values: MedicineBaseFormData, resetForm: () => void] submit: [values: BaseFormData, resetForm: () => void]
cancel: [resetForm: () => void] cancel: [resetForm: () => void]
}>() }>()
+1 -10
View File
@@ -10,11 +10,6 @@ import { defineAsyncComponent } from 'vue'
const action = defineAsyncComponent(() => import('~/components/pub/custom-ui/data/dropdown-action-dud.vue')) const action = defineAsyncComponent(() => import('~/components/pub/custom-ui/data/dropdown-action-dud.vue'))
const _doctorStatus = {
0: 'Tidak Aktif',
1: 'Aktif',
}
export const cols: Col[] = [{}, {}, { width: 50 }] export const cols: Col[] = [{}, {}, { width: 50 }]
export const header: Th[][] = [[{ label: 'Kode' }, { label: 'Nama' }, { label: 'Aksi' }]] export const header: Th[][] = [[{ label: 'Kode' }, { label: 'Nama' }, { label: 'Aksi' }]]
@@ -39,8 +34,4 @@ export const funcComponent: RecStrFuncComponent = {
}, },
} }
export const funcHtml: RecStrFuncUnknown = { export const funcHtml: RecStrFuncUnknown = {}
patient_address(_rec) {
return '-'
},
}
@@ -8,7 +8,7 @@ import Label from '~/components/pub/custom-ui/doc-entry/label.vue'
import Button from '~/components/pub/ui/button/Button.vue' import Button from '~/components/pub/ui/button/Button.vue'
// Types // Types
import type { MedicineBaseFormData } from '~/schemas/medicine.schema' import type { BaseFormData } from '~/schemas/base.schema.ts'
// Helpers // Helpers
import type z from 'zod' import type z from 'zod'
@@ -26,7 +26,7 @@ const props = defineProps<Props>()
const isLoading = props.isLoading !== undefined ? props.isLoading : false const isLoading = props.isLoading !== undefined ? props.isLoading : false
const isReadonly = props.isReadonly !== undefined ? props.isReadonly : false const isReadonly = props.isReadonly !== undefined ? props.isReadonly : false
const emit = defineEmits<{ const emit = defineEmits<{
submit: [values: MedicineBaseFormData, resetForm: () => void] submit: [values: BaseFormData, resetForm: () => void]
cancel: [resetForm: () => void] cancel: [resetForm: () => void]
}>() }>()
+1 -10
View File
@@ -10,11 +10,6 @@ import { defineAsyncComponent } from 'vue'
const action = defineAsyncComponent(() => import('~/components/pub/custom-ui/data/dropdown-action-dud.vue')) const action = defineAsyncComponent(() => import('~/components/pub/custom-ui/data/dropdown-action-dud.vue'))
const _doctorStatus = {
0: 'Tidak Aktif',
1: 'Aktif',
}
export const cols: Col[] = [{}, {}, { width: 50 }] export const cols: Col[] = [{}, {}, { width: 50 }]
export const header: Th[][] = [[{ label: 'Kode' }, { label: 'Nama' }, { label: 'Aksi' }]] export const header: Th[][] = [[{ label: 'Kode' }, { label: 'Nama' }, { label: 'Aksi' }]]
@@ -39,8 +34,4 @@ export const funcComponent: RecStrFuncComponent = {
}, },
} }
export const funcHtml: RecStrFuncUnknown = { export const funcHtml: RecStrFuncUnknown = {}
patient_address(_rec) {
return '-'
},
}
+172 -69
View File
@@ -1,80 +1,183 @@
<script setup lang="ts"> <script setup lang="ts">
import Block from '~/components/pub/custom-ui/form/block.vue' // Components
import FieldGroup from '~/components/pub/custom-ui/form/field-group.vue' import Block from '~/components/pub/custom-ui/doc-entry/block.vue'
import Field from '~/components/pub/custom-ui/form/field.vue' import Cell from '~/components/pub/custom-ui/doc-entry/cell.vue'
import Label from '~/components/pub/custom-ui/form/label.vue' import Field from '~/components/pub/custom-ui/doc-entry/field.vue'
import Label from '~/components/pub/custom-ui/doc-entry/label.vue'
import Button from '~/components/pub/ui/button/Button.vue'
const props = defineProps<{ modelValue: any }>() // Helpers
const emit = defineEmits(['update:modelValue', 'event']) import type z from 'zod'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
const data = computed({ interface Props {
get: () => props.modelValue, schema?: z.ZodSchema<any>
set: (val) => emit('update:modelValue', val), values?: any
isLoading?: boolean
isReadonly?: boolean
medicineGroups?: { value: string, label: string }[]
medicineMethods?: { value: string, label: string }[]
uoms?: { value: string, label: string }[]
infras?: { value: number|null, label: string }[]
}
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: any, resetForm: () => void]
cancel: [resetForm: () => void]
}>()
const { defineField, errors, meta } = useForm({
validationSchema: props.schema ? toTypedSchema(props.schema) : undefined,
initialValues: {
code: '',
name: '',
medicineGroup_code: '',
medicineMethod_code: '',
uom_code: '',
infra_id: null,
stock: 0,
},
}) })
const items = [ const [code, codeAttrs] = defineField('code')
{ value: '1', label: 'item 1' }, const [name, nameAttrs] = defineField('name')
{ value: '2', label: 'item 2' }, const [medicineGroup_code, medicineGroupAttrs] = defineField('medicineGroup_code')
{ value: '3', label: 'item 3' }, const [medicineMethod_code, medicineMethodAttrs] = defineField('medicineMethod_code')
{ value: '4', label: 'item 4' }, const [uom_code, uomAttrs] = defineField('uom_code')
] const [infra_id, infraAttrs] = defineField('infra_id')
const [stock, stockAttrs] = defineField('stock')
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.medicineGroup_code !== undefined) medicineGroup_code.value = props.values.medicineGroup_code
if (props.values.medicineMethod_code !== undefined) medicineMethod_code.value = props.values.medicineMethod_code
if (props.values.uom_code !== undefined) uom_code.value = props.values.uom_code
if (props.values.infra_id !== undefined) infra_id.value = props.values.infra_id
if (props.values.stock !== undefined) stock.value = props.values.stock
}
const resetForm = () => {
code.value = ''
name.value = ''
medicineGroup_code.value = ''
medicineMethod_code.value = ''
uom_code.value = ''
infra_id.value = null
stock.value = 0
}
function onSubmitForm() {
const formData = {
code: code.value || '',
name: name.value || '',
medicineGroup_code: medicineGroup_code.value || '',
medicineMethod_code: medicineMethod_code.value || '',
uom_code: uom_code.value || '',
infra_id: infra_id.value === '' ? null : infra_id.value,
stock: stock.value || 0,
}
emit('submit', formData, resetForm)
}
function onCancelForm() {
emit('cancel', resetForm)
}
</script> </script>
<template> <template>
<form id="entry-form"> <form id="form-medicine" @submit.prevent>
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl"> <Block labelSize="thin" class="!mb-2.5 !pt-0 xl:!mb-3" :colCount="1">
<div class="flex flex-col justify-between"> <Cell>
<Block> <Label height="">Kode</Label>
<FieldGroup :column="2"> <Field :errMessage="errors.code">
<Label>Nama</Label> <Input id="code" v-model="code" v-bind="codeAttrs" :disabled="isLoading || isReadonly" class="input input-bordered w-full" />
<Field> </Field>
<Input type="text" name="identity_number" /> </Cell>
</Field> <Cell>
</FieldGroup> <Label height="compact">Nama</Label>
<FieldGroup :column="2"> <Field :errMessage="errors.name">
<Label>Kode</Label> <Input id="name" v-model="name" v-bind="nameAttrs" :disabled="isLoading || isReadonly" class="input input-bordered w-full" />
<Field name="sip_number"> </Field>
<Input type="text" name="sip_no" /> </Cell>
</Field> <Cell>
</FieldGroup> <Label height="compact">Kelompok Obat</Label>
<FieldGroup :column="2"> <Field :errMessage="errors.medicineGroup_code">
<Label>Cara Pemberian</Label> <Select
<Field name="phone"> id="medicineGroup_code"
<Select :items="items" /> v-model="medicineGroup_code"
</Field> icon-name="i-lucide-chevron-down"
</FieldGroup> placeholder="Pilih kelompok obat"
<FieldGroup :column="2"> v-bind="medicineGroupAttrs"
<Label>Bentuk Sediaan</Label> :items="props.medicineGroups || []"
<Field> :disabled="isLoading || isReadonly"
<Select :items="items" /> />
</Field> </Field>
</FieldGroup> </Cell>
<FieldGroup :column="2"> <Cell>
<Label>Dosis</Label> <Label height="compact">Metode Pemberian</Label>
<Field> <Field :errMessage="errors.medicineMethod_code">
<Input type="number" name="outPatient_rate" /> <Select
</Field> id="medicineMethod_code"
</FieldGroup> v-model="medicineMethod_code"
<FieldGroup :column="2"> icon-name="i-lucide-chevron-down"
<Label>Infra</Label> placeholder="Pilih metode pemberian"
<Field> v-bind="medicineMethodAttrs"
<Input /> :items="props.medicineMethods || []"
</Field> :disabled="isLoading || isReadonly"
</FieldGroup> />
<FieldGroup :column="2"> </Field>
<Label>Stock</Label> </Cell>
<Field> <Cell>
<Input type="number" /> <Label height="compact">Satuan</Label>
</Field> <Field :errMessage="errors.uom_code">
</FieldGroup> <Select
<FieldGroup :column="2"> id="uom_code"
<Label>Status</Label> v-model="uom_code"
<Field> icon-name="i-lucide-chevron-down"
<Input /> placeholder="Pilih satuan"
</Field> v-bind="uomAttrs"
</FieldGroup> :items="props.uoms || []"
</Block> :disabled="isLoading || isReadonly"
</div> />
</Field>
</Cell>
<!-- <Cell>
<Label height="compact">Infra</Label>
<Field :errMessage="errors.infra_id">
<Select
id="infra_id"
v-model="infra_id"
icon-name="i-lucide-chevron-down"
placeholder="Tidak ada"
v-bind="infraAttrs"
:items="props.infras || []"
:disabled="isLoading || isReadonly"
/>
</Field>
</Cell> -->
<Cell>
<Label height="compact">Stok</Label>
<Field :errMessage="errors.stock">
<Input id="stock" v-model="stock" type="number" v-bind="stockAttrs" :disabled="isLoading || isReadonly" class="input input-bordered w-full" />
</Field>
</Cell>
</Block>
<div class="my-2 flex justify-end gap-2 py-2">
<Button type="button" variant="secondary" class="w-[120px]" @click="onCancelForm"> Kembali </Button>
<Button
v-if="!isReadonly"
type="button"
class="w-[120px]"
:disabled="isLoading || !meta.valid"
@click="onSubmitForm"
>
Simpan
</Button>
</div> </div>
</form> </form>
</template> </template>
+6 -14
View File
@@ -18,16 +18,15 @@ export const header: Th[][] = [
[ [
{ label: 'Kode' }, { label: 'Kode' },
{ label: 'Name' }, { label: 'Name' },
{ label: 'Kategori' },
{ label: 'Golongan' }, { label: 'Golongan' },
{ label: 'Metode Pemberian' }, { label: 'Metode Pemberian' },
{ label: 'Bentuk' }, { label: "Satuan" },
{ label: 'Stok' }, { label: 'Stok' },
{ label: 'Aksi' }, { label: 'Aksi' },
], ],
] ]
export const keys = ['code', 'name', 'category', 'group', 'method', 'unit', 'total', 'action'] export const keys = ['code', 'name', 'group', 'method', 'unit', 'stock', 'action']
export const delKeyNames: KeyLabel[] = [ export const delKeyNames: KeyLabel[] = [
{ key: 'code', label: 'Kode' }, { key: 'code', label: 'Kode' },
@@ -35,17 +34,14 @@ export const delKeyNames: KeyLabel[] = [
] ]
export const funcParsed: RecStrFuncUnknown = { export const funcParsed: RecStrFuncUnknown = {
cateogry: (rec: unknown): unknown => {
return (rec as SmallDetailDto).medicineCategory?.name || '-'
},
group: (rec: unknown): unknown => { group: (rec: unknown): unknown => {
return (rec as SmallDetailDto).medicineGroup?.name || '-' return (rec as SmallDetailDto).medicineGroup_code || '-'
}, },
method: (rec: unknown): unknown => { method: (rec: unknown): unknown => {
return (rec as SmallDetailDto).medicineMethod?.name || '-' return (rec as SmallDetailDto).medicineMethod_code || '-'
}, },
unit: (rec: unknown): unknown => { unit: (rec: unknown): unknown => {
return (rec as SmallDetailDto).medicineUnit?.name || '-' return (rec as SmallDetailDto).uom_code || '-'
}, },
} }
@@ -60,8 +56,4 @@ export const funcComponent: RecStrFuncComponent = {
}, },
} }
export const funcHtml: RecStrFuncUnknown = { export const funcHtml: RecStrFuncUnknown = {}
// (_rec) {
// return '-'
// },
}
+26 -10
View File
@@ -1,19 +1,35 @@
<script setup lang="ts"> <script setup lang="ts">
import type { PaginationMeta } from '~/components/pub/custom-ui/pagination/pagination.type'
import PaginationView from '~/components/pub/custom-ui/pagination/pagination-view.vue'
import { cols, funcComponent, funcHtml, funcParsed, header, keys } from './list-cfg' import { cols, funcComponent, funcHtml, funcParsed, header, keys } from './list-cfg'
defineProps<{ interface Props {
data: any[] data: any[]
paginationMeta: PaginationMeta
}
defineProps<Props>()
const emit = defineEmits<{
pageChange: [page: number]
}>() }>()
function handlePageChange(page: number) {
emit('pageChange', page)
}
</script> </script>
<template> <template>
<PubBaseDataTable <div class="space-y-4">
:rows="data" <PubBaseDataTable
:cols="cols" :rows="data"
:header="header" :cols="cols"
:keys="keys" :header="header"
:func-parsed="funcParsed" :keys="keys"
:func-html="funcHtml" :func-parsed="funcParsed"
:func-component="funcComponent" :func-html="funcHtml"
/> :func-component="funcComponent"
/>
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
</div>
</template> </template>
@@ -0,0 +1,181 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
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 SpecialistFormData {
name: string
code: string
installationId: string
unitId: string
}
const props = defineProps<{
schema: any
initialValues?: Partial<SpecialistFormData>
errors?: FormErrors
installation: {
msg: {
placeholder: string
search: string
empty: string
}
items: {
value: string
label: string
code: string
}[]
}
unit: {
msg: {
placeholder: string
search: string
empty: string
}
items: {
value: string
label: string
code: string
}[]
}
}>()
const emit = defineEmits<{
'submit': [values: SpecialistFormData, resetForm: () => void]
'cancel': [resetForm: () => void]
'installationChanged': [id: string]
}>()
const formSchema = toTypedSchema(props.schema)
// Form submission handler
function onSubmitForm(values: any, { resetForm }: { resetForm: () => void }) {
const formData: SpecialistFormData = {
name: values.name || '',
code: values.code || '',
installationId: values.installationId || '',
unitId: values.unitId || '',
}
emit('submit', formData, resetForm)
}
// Form cancel handler
function onCancelForm({ resetForm }: { resetForm: () => void }) {
emit('cancel', resetForm)
}
// Watch for installation changes
function onInstallationChanged(installationId: string, setFieldValue: any) {
setFieldValue('unitId', '', false)
emit('installationChanged', installationId || '')
}
</script>
<template>
<Form
v-slot="{ handleSubmit, resetForm, values, setFieldValue }" 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">
<FormField v-slot="{ componentField }" name="name">
<FormItem>
<FormControl>
<Input
id="name"
type="text"
placeholder="Masukkan nama spesialisasi"
autocomplete="off"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<FieldGroup>
<Label label-for="code">Kode</Label>
<Field id="code" :errors="errors">
<FormField v-slot="{ componentField }" name="code">
<FormItem>
<FormControl>
<Input
id="code"
type="text"
placeholder="Masukkan kode spesialisasi"
autocomplete="off"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<FieldGroup>
<Label label-for="installationId">Instalasi</Label>
<Field id="installationId" :errors="errors">
<FormField v-slot="{ componentField }" name="installationId">
<FormItem>
<FormControl>
<Combobox
id="installationId"
v-bind="componentField"
:items="installation.items"
:placeholder="installation.msg.placeholder"
:search-placeholder="installation.msg.search"
:empty-message="installation.msg.empty"
@update:model-value="(value) => onInstallationChanged(value, setFieldValue)"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<FieldGroup>
<Label label-for="unitId">Unit</Label>
<Field id="unitId" :errors="errors">
<FormField v-slot="{ componentField }" name="unitId">
<FormItem>
<FormControl>
<Combobox
id="unitId"
:disabled="!values.installationId"
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>
</div>
</div>
<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>
+107 -161
View File
@@ -1,182 +1,128 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FormErrors } from '~/types/error' // Components
import { toTypedSchema } from '@vee-validate/zod' import Block from '~/components/pub/custom-ui/doc-entry/block.vue'
import Combobox from '~/components/pub/custom-ui/form/combobox.vue' import Cell from '~/components/pub/custom-ui/doc-entry/cell.vue'
import FieldGroup from '~/components/pub/custom-ui/form/field-group.vue' import Field from '~/components/pub/custom-ui/doc-entry/field.vue'
import Field from '~/components/pub/custom-ui/form/field.vue' import Label from '~/components/pub/custom-ui/doc-entry/label.vue'
import Label from '~/components/pub/custom-ui/form/label.vue' // import Combobox from '~/components/pub/custom-ui/form/combobox.vue'
import { Form } from '~/components/pub/ui/form'
interface SpecialistFormData { // Types
name: string import type { SpecialistFormData } from '~/schemas/specialist.schema.ts'
code: string
installationId: string // Helpers
unitId: string import type z from 'zod'
import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
interface Props {
schema: z.ZodSchema<any>
units: any[]
values: any
isLoading?: boolean
isReadonly?: boolean
} }
const props = defineProps<{ const props = defineProps<Props>()
schema: any const isLoading = props.isLoading !== undefined ? props.isLoading : false
initialValues?: Partial<SpecialistFormData> const isReadonly = props.isReadonly !== undefined ? props.isReadonly : false
errors?: FormErrors
installation: {
msg: {
placeholder: string
search: string
empty: string
}
items: {
value: string
label: string
code: string
}[]
}
unit: {
msg: {
placeholder: string
search: string
empty: string
}
items: {
value: string
label: string
code: string
}[]
}
}>()
const emit = defineEmits<{ const emit = defineEmits<{
'submit': [values: SpecialistFormData, resetForm: () => void] submit: [values: SpecialistFormData, resetForm: () => void]
'cancel': [resetForm: () => void] cancel: [resetForm: () => void]
'installationChanged': [id: string]
}>() }>()
const formSchema = toTypedSchema(props.schema) const { defineField, errors, meta } = useForm({
validationSchema: toTypedSchema(props.schema),
initialValues: {
code: '',
name: '',
unit_id: 0,
} as Partial<SpecialistFormData>,
})
const [code, codeAttrs] = defineField('code')
const [name, nameAttrs] = defineField('name')
const [unit, unitAttrs] = defineField('unit_id')
// Fill fields from props.values if provided
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
}
const resetForm = () => {
code.value = ''
name.value = ''
unit.value = ''
}
// Form submission handler // Form submission handler
function onSubmitForm(values: any, { resetForm }: { resetForm: () => void }) { function onSubmitForm(values: any) {
const formData: SpecialistFormData = { const formData: SpecialistFormData = {
name: values.name || '', name: name.value || '',
code: values.code || '', code: code.value || '',
installationId: values.installationId || '', unit_id: unit.value || '',
unitId: values.unitId || '',
} }
emit('submit', formData, resetForm) emit('submit', formData, resetForm)
} }
// Form cancel handler // Form cancel handler
function onCancelForm({ resetForm }: { resetForm: () => void }) { function onCancelForm() {
emit('cancel', resetForm) emit('cancel', resetForm)
} }
// Watch for installation changes
function onInstallationChanged(installationId: string, setFieldValue: any) {
setFieldValue('unitId', '', false)
emit('installationChanged', installationId || '')
}
</script> </script>
<template> <template>
<Form <form id="form-specialist" @submit.prevent>
v-slot="{ handleSubmit, resetForm, values, setFieldValue }" as="" keep-values :validation-schema="formSchema" <Block labelSize="thin" class="!mb-2.5 !pt-0 xl:!mb-3" :colCount="1">
:initial-values="initialValues" <Cell>
> <Label height="compact">Kode</Label>
<form id="entry-form" @submit="handleSubmit($event, (values) => onSubmitForm(values, { resetForm }))"> <Field :errMessage="errors.code">
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl"> <Input id="code" v-model="code" v-bind="codeAttrs" :disabled="isLoading || isReadonly" />
<div class="flex flex-col justify-between"> </Field>
<FieldGroup> </Cell>
<Label label-for="name">Nama</Label> <Cell>
<Field id="name" :errors="errors"> <Label height="compact">Nama</Label>
<FormField v-slot="{ componentField }" name="name"> <Field :errMessage="errors.name">
<FormItem> <Input id="name" v-model="name" v-bind="nameAttrs" :disabled="isLoading || isReadonly" />
<FormControl> </Field>
<Input </Cell>
id="name" <Cell>
type="text" <Label height="compact">Unit</Label>
placeholder="Masukkan nama spesialisasi" <Field :errMessage="errors.unit">
autocomplete="off" <!-- <Combobox
v-bind="componentField" id="unit"
/> placeholder="Pilih Unit"
</FormControl> search-placeholder="Cari unit"
<FormMessage /> empty-message="Unit tidak ditemukan"
</FormItem> v-model="unit"
</FormField> v-bind="unitAttrs"
</Field> :items="units"
</FieldGroup> :disabled="isLoading || isReadonly"
/> -->
<FieldGroup> <Select
<Label label-for="code">Kode</Label> id="unit"
<Field id="code" :errors="errors"> icon-name="i-lucide-chevron-down"
<FormField v-slot="{ componentField }" name="code"> placeholder="Pilih unit"
<FormItem> v-model="unit"
<FormControl> v-bind="unitAttrs"
<Input :items="units"
id="code" :disabled="isLoading || isReadonly"
type="text" />
placeholder="Masukkan kode spesialisasi" </Field>
autocomplete="off" </Cell>
v-bind="componentField" </Block>
/> <div class="my-2 flex justify-end gap-2 py-2">
</FormControl> <Button type="button" variant="secondary" class="w-[120px]" @click="onCancelForm"> Kembali </Button>
<FormMessage /> <Button
</FormItem> v-if="!isReadonly"
</FormField> type="button"
</Field> class="w-[120px]"
</FieldGroup> :disabled="isLoading || !meta.valid"
@click="onSubmitForm"
<FieldGroup> >
<Label label-for="installationId">Instalasi</Label> Simpan
<Field id="installationId" :errors="errors"> </Button>
<FormField v-slot="{ componentField }" name="installationId"> </div>
<FormItem> </form>
<FormControl>
<Combobox
id="installationId"
v-bind="componentField"
:items="installation.items"
:placeholder="installation.msg.placeholder"
:search-placeholder="installation.msg.search"
:empty-message="installation.msg.empty"
@update:model-value="(value) => onInstallationChanged(value, setFieldValue)"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<FieldGroup>
<Label label-for="unitId">Unit</Label>
<Field id="unitId" :errors="errors">
<FormField v-slot="{ componentField }" name="unitId">
<FormItem>
<FormControl>
<Combobox
id="unitId"
:disabled="!values.installationId"
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>
</div>
</div>
<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> </template>
+10 -12
View File
@@ -10,15 +10,13 @@ import { defineAsyncComponent } from 'vue'
type SmallDetailDto = any type SmallDetailDto = any
const action = defineAsyncComponent(() => import('~/components/pub/custom-ui/data/dropdown-action-ud.vue')) const action = defineAsyncComponent(() => import('~/components/pub/custom-ui/data/dropdown-action-dud.vue'))
export const cols: Col[] = [{ width: 100 }, {}, {}, {}, { width: 50 }] export const cols: Col[] = [{}, {}, {}, { width: 50 }]
export const header: Th[][] = [ export const header: Th[][] = [[{ label: 'Kode' }, { label: 'Nama' }, { label: 'Unit' }, { label: '' }]]
[{ label: 'Id' }, { label: 'Name' }, { label: 'Code' }, { label: 'Unit' }, { label: '' }],
]
export const keys = ['id', 'name', 'cellphone', 'religion_code', 'action'] export const keys = ['code', 'name', 'unit', 'action']
export const delKeyNames: KeyLabel[] = [ export const delKeyNames: KeyLabel[] = [
{ key: 'code', label: 'Kode' }, { key: 'code', label: 'Kode' },
@@ -28,7 +26,11 @@ export const delKeyNames: KeyLabel[] = [
export const funcParsed: RecStrFuncUnknown = { export const funcParsed: RecStrFuncUnknown = {
name: (rec: unknown): unknown => { name: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto const recX = rec as SmallDetailDto
return `${recX.firstName} ${recX.lastName || ''}`.trim() return `${recX.name}`.trim()
},
unit: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
return recX.unit_id || '-'
}, },
} }
@@ -38,13 +40,9 @@ export const funcComponent: RecStrFuncComponent = {
idx, idx,
rec: rec as object, rec: rec as object,
component: action, component: action,
props: {
size: 'sm',
},
} }
return res return res
}, },
} }
export const funcHtml: RecStrFuncUnknown = { export const funcHtml: RecStrFuncUnknown = {}
}
+8 -3
View File
@@ -22,10 +22,15 @@ function handlePageChange(page: number) {
<template> <template>
<div class="space-y-4"> <div class="space-y-4">
<PubBaseDataTable <PubBaseDataTable
:rows="data" :cols="cols" :header="header" :keys="keys" :func-parsed="funcParsed" :rows="data"
:func-html="funcHtml" :func-component="funcComponent" :skeleton-size="paginationMeta?.pageSize" :cols="cols"
:header="header"
:keys="keys"
:func-parsed="funcParsed"
:func-html="funcHtml"
:func-component="funcComponent"
:skeleton-size="paginationMeta?.pageSize"
/> />
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" /> <PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
</div> </div>
</template> </template>
@@ -0,0 +1,213 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
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 SubSpecialistFormData {
name: string
code: string
installationId: string
unitId: string
specialistId: string
}
const props = defineProps<{
schema: any
initialValues?: Partial<SubSpecialistFormData>
errors?: FormErrors
installation: {
msg: {
placeholder: string
search: string
empty: string
}
items: {
value: string
label: string
code: string
}[]
}
unit: {
msg: {
placeholder: string
search: string
empty: string
}
items: {
value: string
label: string
code: string
}[]
}
specialist: {
msg: {
placeholder: string
search: string
empty: string
}
items: {
value: string
label: string
code: string
}[]
}
}>()
const emit = defineEmits<{
'submit': [values: SubSpecialistFormData, resetForm: () => void]
'cancel': [resetForm: () => void]
'installationChanged': [id: string]
'unitChanged': [id: string]
}>()
const formSchema = toTypedSchema(props.schema)
// Form submission handler
function onSubmitForm(values: any, { resetForm }: { resetForm: () => void }) {
const formData: SubSpecialistFormData = {
name: values.name || '',
code: values.code || '',
installationId: values.installationId || '',
unitId: values.unitId || '',
specialistId: values.specialistId || '',
}
emit('submit', formData, resetForm)
}
// Form cancel handler
function onCancelForm({ resetForm }: { resetForm: () => void }) {
emit('cancel', resetForm)
}
// Watch for installation changes
function onInstallationChanged(installationId: string, setFieldValue: any) {
setFieldValue('unitId', '', false)
setFieldValue('specialistId', '', false)
emit('installationChanged', installationId || '')
}
function onUnitChanged(unitId: string, setFieldValue: any) {
setFieldValue('specialistId', '', false)
emit('unitChanged', unitId || '')
}
</script>
<template>
<Form
v-slot="{
handleSubmit,
resetForm,
values,
setFieldValue,
}" 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">
<FormField v-slot="{ componentField }" name="name">
<FormItem>
<FormControl>
<Input
id="name" type="text" placeholder="Masukkan nama spesialisasi" autocomplete="off"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<FieldGroup>
<Label label-for="code">Kode</Label>
<Field id="code" :errors="errors">
<FormField v-slot="{ componentField }" name="code">
<FormItem>
<FormControl>
<Input
id="code" type="text" placeholder="Masukkan kode spesialisasi" autocomplete="off"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<FieldGroup>
<Label label-for="installationId">Instalasi</Label>
<Field id="installationId" :errors="errors">
<FormField v-slot="{ componentField }" name="installationId">
<FormItem>
<FormControl>
<Combobox
id="installationId" v-bind="componentField" :items="installation.items"
:placeholder="installation.msg.placeholder" :search-placeholder="installation.msg.search"
:empty-message="installation.msg.empty"
@update:model-value="(value) => onInstallationChanged(value, setFieldValue)"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<FieldGroup>
<Label label-for="unitId">Unit</Label>
<Field id="unitId" :errors="errors">
<FormField v-slot="{ componentField }" name="unitId">
<FormItem>
<FormControl>
<Combobox
id="unitId" :disabled="!values.installationId" v-bind="componentField" :items="unit.items"
:placeholder="unit.msg.placeholder" :search-placeholder="unit.msg.search"
:empty-message="unit.msg.empty"
@update:model-value="(value) => onUnitChanged(value, setFieldValue)"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<FieldGroup>
<Label label-for="specialistId">Specialist</Label>
<Field id="specialistId" :errors="errors">
<FormField v-slot="{ componentField }" name="specialistId">
<FormItem>
<FormControl>
<Combobox
id="specialistId" :disabled="!values.unitId" v-bind="componentField" :items="specialist.items"
:placeholder="specialist.msg.placeholder" :search-placeholder="specialist.msg.search"
:empty-message="specialist.msg.empty"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
</div>
</div>
<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>
+108 -194
View File
@@ -1,214 +1,128 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FormErrors } from '~/types/error' // Components
import { toTypedSchema } from '@vee-validate/zod' import Block from '~/components/pub/custom-ui/doc-entry/block.vue'
import Combobox from '~/components/pub/custom-ui/form/combobox.vue' import Cell from '~/components/pub/custom-ui/doc-entry/cell.vue'
import FieldGroup from '~/components/pub/custom-ui/form/field-group.vue' import Field from '~/components/pub/custom-ui/doc-entry/field.vue'
import Field from '~/components/pub/custom-ui/form/field.vue' import Label from '~/components/pub/custom-ui/doc-entry/label.vue'
import Label from '~/components/pub/custom-ui/form/label.vue' // import Combobox from '~/components/pub/custom-ui/form/combobox.vue'
import { Form } from '~/components/pub/ui/form'
interface SubSpecialistFormData { // Types
name: string import type { SubspecialistFormData } from '~/schemas/subspecialist.schema.ts'
code: string
installationId: string // Helpers
unitId: string import type z from 'zod'
specialistId: string import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
interface Props {
schema: z.ZodSchema<any>
specialists: any[]
values: any
isLoading?: boolean
isReadonly?: boolean
} }
const props = defineProps<{ const props = defineProps<Props>()
schema: any const isLoading = props.isLoading !== undefined ? props.isLoading : false
initialValues?: Partial<SubSpecialistFormData> const isReadonly = props.isReadonly !== undefined ? props.isReadonly : false
errors?: FormErrors
installation: {
msg: {
placeholder: string
search: string
empty: string
}
items: {
value: string
label: string
code: string
}[]
}
unit: {
msg: {
placeholder: string
search: string
empty: string
}
items: {
value: string
label: string
code: string
}[]
}
specialist: {
msg: {
placeholder: string
search: string
empty: string
}
items: {
value: string
label: string
code: string
}[]
}
}>()
const emit = defineEmits<{ const emit = defineEmits<{
'submit': [values: SubSpecialistFormData, resetForm: () => void] submit: [values: SubspecialistFormData, resetForm: () => void]
'cancel': [resetForm: () => void] cancel: [resetForm: () => void]
'installationChanged': [id: string]
'unitChanged': [id: string]
}>() }>()
const formSchema = toTypedSchema(props.schema) const { defineField, errors, meta } = useForm({
validationSchema: toTypedSchema(props.schema),
initialValues: {
code: '',
name: '',
specialist_id: 0,
} as Partial<SubspecialistFormData>,
})
const [code, codeAttrs] = defineField('code')
const [name, nameAttrs] = defineField('name')
const [specialist, specialistAttrs] = defineField('specialist_id')
// Fill fields from props.values if provided
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
}
const resetForm = () => {
code.value = ''
name.value = ''
specialist.value = ''
}
// Form submission handler // Form submission handler
function onSubmitForm(values: any, { resetForm }: { resetForm: () => void }) { function onSubmitForm(values: any) {
const formData: SubSpecialistFormData = { const formData: SubspecialistFormData = {
name: values.name || '', name: name.value || '',
code: values.code || '', code: code.value || '',
installationId: values.installationId || '', specialist_id: specialist.value || '',
unitId: values.unitId || '',
specialistId: values.specialistId || '',
} }
emit('submit', formData, resetForm) emit('submit', formData, resetForm)
} }
// Form cancel handler // Form cancel handler
function onCancelForm({ resetForm }: { resetForm: () => void }) { function onCancelForm() {
emit('cancel', resetForm) emit('cancel', resetForm)
} }
// Watch for installation changes
function onInstallationChanged(installationId: string, setFieldValue: any) {
setFieldValue('unitId', '', false)
setFieldValue('specialistId', '', false)
emit('installationChanged', installationId || '')
}
function onUnitChanged(unitId: string, setFieldValue: any) {
setFieldValue('specialistId', '', false)
emit('unitChanged', unitId || '')
}
</script> </script>
<template> <template>
<Form <form id="form-specialist" @submit.prevent>
v-slot="{ <Block labelSize="thin" class="!mb-2.5 !pt-0 xl:!mb-3" :colCount="1">
handleSubmit, <Cell>
resetForm, <Label height="compact">Kode</Label>
values, <Field :errMessage="errors.code">
setFieldValue, <Input id="code" v-model="code" v-bind="codeAttrs" :disabled="isLoading || isReadonly" />
}" as="" keep-values :validation-schema="formSchema" </Field>
:initial-values="initialValues" </Cell>
> <Cell>
<form id="entry-form" @submit="handleSubmit($event, (values) => onSubmitForm(values, { resetForm }))"> <Label height="compact">Nama</Label>
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl"> <Field :errMessage="errors.name">
<div class="flex flex-col justify-between"> <Input id="name" v-model="name" v-bind="nameAttrs" :disabled="isLoading || isReadonly" />
<FieldGroup> </Field>
<Label label-for="name">Nama</Label> </Cell>
<Field id="name" :errors="errors"> <Cell>
<FormField v-slot="{ componentField }" name="name"> <Label height="compact">Spesialis</Label>
<FormItem> <Field :errMessage="errors.specialist">
<FormControl> <!-- <Combobox
<Input id="specialis"
id="name" type="text" placeholder="Masukkan nama spesialisasi" autocomplete="off" placeholder="Pilih spesialis"
v-bind="componentField" search-placeholder="Cari spesialis"
/> empty-message="Spesialis tidak ditemukan"
</FormControl> v-model="specialist"
<FormMessage /> v-bind="specialistAttrs"
</FormItem> :items="specialists"
</FormField> :disabled="isLoading || isReadonly"
</Field> /> -->
</FieldGroup> <Select
id="specialist"
<FieldGroup> icon-name="i-lucide-chevron-down"
<Label label-for="code">Kode</Label> placeholder="Pilih spesialis"
<Field id="code" :errors="errors"> v-model="specialist"
<FormField v-slot="{ componentField }" name="code"> v-bind="specialistAttrs"
<FormItem> :items="specialists"
<FormControl> :disabled="isLoading || isReadonly"
<Input />
id="code" type="text" placeholder="Masukkan kode spesialisasi" autocomplete="off" </Field>
v-bind="componentField" </Cell>
/> </Block>
</FormControl> <div class="my-2 flex justify-end gap-2 py-2">
<FormMessage /> <Button type="button" variant="secondary" class="w-[120px]" @click="onCancelForm"> Kembali </Button>
</FormItem> <Button
</FormField> v-if="!isReadonly"
</Field> type="button"
</FieldGroup> class="w-[120px]"
:disabled="isLoading || !meta.valid"
<FieldGroup> @click="onSubmitForm"
<Label label-for="installationId">Instalasi</Label> >
<Field id="installationId" :errors="errors"> Simpan
<FormField v-slot="{ componentField }" name="installationId"> </Button>
<FormItem> </div>
<FormControl> </form>
<Combobox
id="installationId" v-bind="componentField" :items="installation.items"
:placeholder="installation.msg.placeholder" :search-placeholder="installation.msg.search"
:empty-message="installation.msg.empty"
@update:model-value="(value) => onInstallationChanged(value, setFieldValue)"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<FieldGroup>
<Label label-for="unitId">Unit</Label>
<Field id="unitId" :errors="errors">
<FormField v-slot="{ componentField }" name="unitId">
<FormItem>
<FormControl>
<Combobox
id="unitId" :disabled="!values.installationId" v-bind="componentField" :items="unit.items"
:placeholder="unit.msg.placeholder" :search-placeholder="unit.msg.search"
:empty-message="unit.msg.empty"
@update:model-value="(value) => onUnitChanged(value, setFieldValue)"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<FieldGroup>
<Label label-for="specialistId">Specialist</Label>
<Field id="specialistId" :errors="errors">
<FormField v-slot="{ componentField }" name="specialistId">
<FormItem>
<FormControl>
<Combobox
id="specialistId" :disabled="!values.unitId" v-bind="componentField" :items="specialist.items"
:placeholder="specialist.msg.placeholder" :search-placeholder="specialist.msg.search"
:empty-message="specialist.msg.empty"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
</div>
</div>
<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> </template>
+7 -15
View File
@@ -10,14 +10,13 @@ import { defineAsyncComponent } from 'vue'
type SmallDetailDto = any type SmallDetailDto = any
const action = defineAsyncComponent(() => import('~/components/pub/custom-ui/data/dropdown-action-ud.vue')) const action = defineAsyncComponent(() => import('~/components/pub/custom-ui/data/dropdown-action-dud.vue'))
export const cols: Col[] = [{ width: 100 }, {}, {}, {}, { width: 50 }] export const cols: Col[] = [{}, {}, {}, { width: 50 }]
export const header: Th[][] = [ export const header: Th[][] = [[{ label: 'Kode' }, { label: 'Nama' }, { label: 'Specialis' }, { label: '' }]]
[{ label: 'Id' }, { label: 'Nama' }, { label: 'Kode' }, { label: 'Specialist' }, { label: '' }],
] export const keys = ['code', 'name', 'specialist', 'action']
export const keys = ['id', 'name', 'cellphone', 'religion_code', 'action']
export const delKeyNames: KeyLabel[] = [ export const delKeyNames: KeyLabel[] = [
{ key: 'code', label: 'Kode' }, { key: 'code', label: 'Kode' },
@@ -27,15 +26,11 @@ export const delKeyNames: KeyLabel[] = [
export const funcParsed: RecStrFuncUnknown = { export const funcParsed: RecStrFuncUnknown = {
name: (rec: unknown): unknown => { name: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto const recX = rec as SmallDetailDto
return `${recX.firstName} ${recX.lastName || ''}`.trim() return `${recX.name}`.trim()
},
unit: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
return recX.unit?.name || '-'
}, },
specialist: (rec: unknown): unknown => { specialist: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto const recX = rec as SmallDetailDto
return recX.specialist?.name || '-' return recX.specialist_id || '-'
}, },
} }
@@ -45,9 +40,6 @@ export const funcComponent: RecStrFuncComponent = {
idx, idx,
rec: rec as object, rec: rec as object,
component: action, component: action,
props: {
size: 'sm',
},
} }
return res return res
}, },
+8 -3
View File
@@ -22,10 +22,15 @@ function handlePageChange(page: number) {
<template> <template>
<div class="space-y-4"> <div class="space-y-4">
<PubBaseDataTable <PubBaseDataTable
:rows="data" :cols="cols" :header="header" :keys="keys" :func-parsed="funcParsed" :rows="data"
:func-html="funcHtml" :func-component="funcComponent" :skeleton-size="paginationMeta?.pageSize" :cols="cols"
:header="header"
:keys="keys"
:func-parsed="funcParsed"
:func-html="funcHtml"
:func-component="funcComponent"
:skeleton-size="paginationMeta?.pageSize"
/> />
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" /> <PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
</div> </div>
</template> </template>
+1 -1
View File
@@ -6,7 +6,7 @@ import Field from '~/components/pub/custom-ui/doc-entry/field.vue'
import Label from '~/components/pub/custom-ui/doc-entry/label.vue' import Label from '~/components/pub/custom-ui/doc-entry/label.vue'
// Types // Types
import type { DeviceFormData } from '~/schemas/device.schema' import type { DeviceFormData } from '~/schemas/device.schema.ts'
// Helpers // Helpers
import type z from 'zod' import type z from 'zod'
+125
View File
@@ -0,0 +1,125 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
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<{
unit: {
msg: {
placeholder: string
search: string
empty: string
}
items: {
value: string
label: string
code: string
}[]
}
schema: any
initialValues?: Partial<UnitFormData>
errors?: FormErrors
}>()
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
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">
<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>
<Label label-for="code">Kode</Label>
<Field id="code" :errors="errors">
<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>
<Label label-for="parentId">Instalasi</Label>
<Field id="parentId" :errors="errors">
<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>
</div>
</div>
<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>
+81 -105
View File
@@ -1,126 +1,102 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FormErrors } from '~/types/error' // Components
import { toTypedSchema } from '@vee-validate/zod' import Block from '~/components/pub/custom-ui/doc-entry/block.vue'
import Combobox from '~/components/pub/custom-ui/form/combobox.vue' import Cell from '~/components/pub/custom-ui/doc-entry/cell.vue'
import FieldGroup from '~/components/pub/custom-ui/form/field-group.vue' import Field from '~/components/pub/custom-ui/doc-entry/field.vue'
import Field from '~/components/pub/custom-ui/form/field.vue' import Label from '~/components/pub/custom-ui/doc-entry/label.vue'
import Label from '~/components/pub/custom-ui/form/label.vue'
import { Form } from '~/components/pub/ui/form'
interface UnitFormData { // Types
name: string import type { UnitFormData } from '~/schemas/unit.schema.ts'
code: string
parentId: string // Helpers
import type z from 'zod'
import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
interface Props {
schema: z.ZodSchema<any>
values: any
isLoading?: boolean
isReadonly?: boolean
} }
const props = defineProps<{ const props = defineProps<Props>()
unit: { const isLoading = props.isLoading !== undefined ? props.isLoading : false
msg: { const isReadonly = props.isReadonly !== undefined ? props.isReadonly : false
placeholder: string
search: string
empty: string
}
items: {
value: string
label: string
code: string
}[]
}
schema: any
initialValues?: Partial<UnitFormData>
errors?: FormErrors
}>()
const emit = defineEmits<{ const emit = defineEmits<{
'submit': [values: UnitFormData, resetForm: () => void] submit: [values: UnitFormData, resetForm: () => void]
'cancel': [resetForm: () => void] cancel: [resetForm: () => void]
}>() }>()
const formSchema = toTypedSchema(props.schema) const { defineField, errors, meta } = useForm({
validationSchema: toTypedSchema(props.schema),
initialValues: {
code: '',
name: '',
installation: '',
} as Partial<UnitFormData>,
})
const [code, codeAttrs] = defineField('code')
const [name, nameAttrs] = defineField('name')
const [installation, installationAttrs] = defineField('installation')
// Fill fields from props.values if provided
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.installation !== undefined) installation.value = props.values.installation
}
const resetForm = () => {
code.value = ''
name.value = ''
installation.value = ''
}
// Form submission handler // Form submission handler
function onSubmitForm(values: any, { resetForm }: { resetForm: () => void }) { function onSubmitForm(values: any) {
const formData: UnitFormData = { const formData: UnitFormData = {
name: values.name || '', name: name.value || '',
code: values.code || '', code: code.value || '',
parentId: values.parentId || '', installation: installation.value || '',
} }
emit('submit', formData, resetForm) emit('submit', formData, resetForm)
} }
// Form cancel handler // Form cancel handler
function onCancelForm({ resetForm }: { resetForm: () => void }) { function onCancelForm() {
emit('cancel', resetForm) emit('cancel', resetForm)
} }
</script> </script>
<template> <template>
<Form <form id="form-unit" @submit.prevent>
v-slot="{ handleSubmit, resetForm }" as="" keep-values :validation-schema="formSchema" <Block labelSize="thin" class="!mb-2.5 !pt-0 xl:!mb-3" :colCount="1">
:initial-values="initialValues" <Cell>
> <Label height="compact">Kode</Label>
<form id="entry-form" @submit="handleSubmit($event, (values) => onSubmitForm(values, { resetForm }))"> <Field :errMessage="errors.code">
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl"> <Input id="code" v-model="code" v-bind="codeAttrs" :disabled="isLoading || isReadonly" />
<div class="flex flex-col justify-between"> </Field>
<FieldGroup> </Cell>
<Label label-for="name">Nama</Label> <Cell>
<Field id="name" :errors="errors"> <Label height="compact">Nama</Label>
<FormField v-slot="{ componentField }" name="name"> <Field :errMessage="errors.name">
<FormItem> <Input id="name" v-model="name" v-bind="nameAttrs" :disabled="isLoading || isReadonly" />
<FormControl> </Field>
<Input </Cell>
id="name" type="text" placeholder="Masukkan nama unit" autocomplete="off" </Block>
v-bind="componentField" <div class="my-2 flex justify-end gap-2 py-2">
/> <Button type="button" variant="secondary" class="w-[120px]" @click="onCancelForm"> Kembali </Button>
</FormControl> <Button
<FormMessage /> v-if="!isReadonly"
</FormItem> type="button"
</FormField> class="w-[120px]"
</Field> :disabled="isLoading || !meta.valid"
</FieldGroup> @click="onSubmitForm"
>
<FieldGroup> Simpan
<Label label-for="code">Kode</Label> </Button>
<Field id="code" :errors="errors"> </div>
<FormField v-slot="{ componentField }" name="code"> </form>
<FormItem>
<FormControl>
<Input id="code" type="text" placeholder="Masukkan kode unit" autocomplete="off" v-bind="componentField" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<FieldGroup>
<Label label-for="parentId">Instalasi</Label>
<Field id="parentId" :errors="errors">
<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>
</div>
</div>
<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> </template>
+8 -47
View File
@@ -10,33 +10,13 @@ import { defineAsyncComponent } from 'vue'
type SmallDetailDto = any type SmallDetailDto = any
const action = defineAsyncComponent(() => import('~/components/pub/custom-ui/data/dropdown-action-ud.vue')) const action = defineAsyncComponent(() => import('~/components/pub/custom-ui/data/dropdown-action-dud.vue'))
export const cols: Col[] = [ export const cols: Col[] = [{}, {}, {}, { width: 50 }]
{ width: 100 },
{ },
{ },
{ },
{ width: 50 },
]
export const header: Th[][] = [ export const header: Th[][] = [[{ label: 'Kode' }, { label: 'Nama' }, { label: 'Instalasi' }, { label: '' }]]
[
{ label: 'Id' },
{ label: 'Nama' },
{ label: 'Kode' },
{ label: 'Instalasi' },
{ label: '' },
],
]
export const keys = [ export const keys = ['code', 'name', 'installation', 'action']
'id',
'firstName',
'cellphone',
'birth_place',
'action',
]
export const delKeyNames: KeyLabel[] = [ export const delKeyNames: KeyLabel[] = [
{ key: 'code', label: 'Kode' }, { key: 'code', label: 'Kode' },
@@ -46,22 +26,10 @@ export const delKeyNames: KeyLabel[] = [
export const funcParsed: RecStrFuncUnknown = { export const funcParsed: RecStrFuncUnknown = {
name: (rec: unknown): unknown => { name: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto const recX = rec as SmallDetailDto
return `${recX.frontTitle} ${recX.name} ${recX.endTitle}`.trim() return `${recX.name}`.trim()
}, },
identity_number: (rec: unknown): unknown => { installation: (_rec: unknown): unknown => {
const recX = rec as SmallDetailDto return '-'
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')
}, },
} }
@@ -71,16 +39,9 @@ export const funcComponent: RecStrFuncComponent = {
idx, idx,
rec: rec as object, rec: rec as object,
component: action, component: action,
props: {
size: 'sm',
},
} }
return res return res
}, },
} }
export const funcHtml: RecStrFuncUnknown = { export const funcHtml: RecStrFuncUnknown = {}
patient_address(_rec) {
return '-'
},
}
+8 -3
View File
@@ -22,10 +22,15 @@ function handlePageChange(page: number) {
<template> <template>
<div class="space-y-4"> <div class="space-y-4">
<PubBaseDataTable <PubBaseDataTable
:rows="data" :cols="cols" :header="header" :keys="keys" :func-parsed="funcParsed" :rows="data"
:func-html="funcHtml" :func-component="funcComponent" :skeleton-size="paginationMeta?.pageSize" :cols="cols"
:header="header"
:keys="keys"
:func-parsed="funcParsed"
:func-html="funcHtml"
:func-component="funcComponent"
:skeleton-size="paginationMeta?.pageSize"
/> />
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" /> <PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
</div> </div>
</template> </template>
+1 -1
View File
@@ -8,7 +8,7 @@ import Label from '~/components/pub/custom-ui/doc-entry/label.vue'
import Button from '~/components/pub/ui/button/Button.vue' import Button from '~/components/pub/ui/button/Button.vue'
// Types // Types
import type { UomFormData } from '~/schemas/uom.schema' import type { UomFormData } from '~/schemas/uom.schema.ts'
// Helpers // Helpers
import type z from 'zod' import type z from 'zod'
-62
View File
@@ -1,62 +0,0 @@
<script setup lang="ts">
import { z, ZodError } from 'zod'
import Action from '~/components/pub/custom-ui/nav-footer/ba-dr-su.vue'
const errors = ref({})
const data = ref({
code: '',
name: '',
type: '',
})
const schema = z.object({
code: z.string().min(1, 'Code must be at least 1 characters long'),
name: z.string().min(1, 'Name must be at least 1 characters long'),
type: z.string(),
})
function onClick(type: string) {
if (type === 'cancel') {
navigateTo('/tools-equipment-src/device')
} else if (type === 'draft') {
// do something
} else if (type === 'submit') {
// do something
const input = data.value
console.log(input)
const errorsParsed: any = {}
try {
const result = schema.safeParse(input)
if (!result.success) {
// You can handle the error here, e.g. show a message
const errorsCaptures = result?.error?.errors || []
const errorMessage = result.error.errors[0]?.message ?? 'Validation error occurred'
errorsCaptures.forEach((value: any) => {
const keyName = value?.path?.length > 0 ? value.path[0] : 'key'
errorsParsed[keyName as string] = value.message || ''
})
console.log(errorMessage)
}
} catch (e) {
if (e instanceof ZodError) {
const jsonError = e.flatten()
console.log(JSON.stringify(jsonError, null, 2))
}
}
setTimeout(() => {
errors.value = errorsParsed
}, 0)
}
}
</script>
<template>
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
<Icon name="i-lucide-paint-bucket" class="me-2" />
<span class="font-semibold">Tambah</span> Alat Kesehatan
</div>
<AppDeviceEntryForm v-model="data" :errors="errors" />
<div class="my-2 flex justify-end py-2">
<Action @click="onClick" />
</div>
</template>
-65
View File
@@ -1,65 +0,0 @@
<script setup lang="ts">
import type { DataTableLoader } from '~/components/pub/base/data-table/type'
import type { HeaderPrep, RefSearchNav } from '~/components/pub/custom-ui/data/types'
import Header from '~/components/pub/custom-ui/nav-header/prep.vue'
const data = ref([])
const refSearchNav: RefSearchNav = {
onClick: () => {
// open filter modal
},
onInput: (_val: string) => {
// filter patient list
},
onClear: () => {
// clear url param
},
}
const isLoading = reactive<DataTableLoader>({
isTableLoading: false,
})
const recId = ref<number>(0)
const recAction = ref<string>('')
const recItem = ref<any>(null)
const headerPrep: HeaderPrep = {
title: 'Alat Kesehatan',
icon: 'i-lucide-paint-bucket',
addNav: {
label: 'Tambah',
onClick: () => navigateTo('/tools-equipment-src/device/add'),
},
}
async function getMaterialList() {
isLoading.dataListLoading = true
const resp = await xfetch('/api/v1/device')
if (resp.success) {
data.value = (resp.body as Record<string, any>).data
}
isLoading.dataListLoading = false
}
onMounted(() => {
getMaterialList()
})
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
</script>
<template>
<div class="rounded-md border p-4">
<Header :prep="{ ...headerPrep }" :ref-search-nav="refSearchNav" />
<div class="rounded-md border p-4">
<AppMaterialList v-if="!isLoading.dataListLoading" :data="data" />
</div>
</div>
</template>
@@ -0,0 +1,208 @@
<script setup lang="ts">
import type { HeaderPrep } from '~/components/pub/custom-ui/data/types'
import AppDivisionEntryForm from '~/components/app/division/entry-form.vue'
import Dialog from '~/components/pub/base/modal/dialog.vue'
import RecordConfirmation from '~/components/pub/custom-ui/confirmation/record-confirmation.vue'
import { ActionEvents } from '~/components/pub/custom-ui/data/types'
import Header from '~/components/pub/custom-ui/nav-header/header.vue'
import { usePaginatedList } from '~/composables/usePaginatedList'
import { divisionConf, divisionTreeConfig, schema } from './entry'
// #region State & Computed
// Dialog state
const isFormEntryDialogOpen = ref(false)
const isRecordConfirmationOpen = ref(false)
// Table action rowId provider
const recId = ref<number>(0)
const recAction = ref<string>('')
const recItem = ref<any>(null)
async function fetchDivisionData(_params: any) {
const endpoint = '/api/v1/_dev/division/list'
return await xfetch(endpoint)
}
// Menggunakan composable untuk pagination
const {
data,
isLoading,
paginationMeta,
searchInput,
handlePageChange,
handleSearch,
fetchData: getDivisionList,
} = usePaginatedList({
fetchFn: fetchDivisionData,
entityName: 'division',
})
const headerPrep: HeaderPrep = {
title: 'Divisi',
icon: 'i-lucide-box',
refSearchNav: {
placeholder: 'Cari (min. 3 karakter)...',
minLength: 3,
debounceMs: 500,
showValidationFeedback: true,
onInput: (_val: string) => {
// Handle search input - this will be triggered by the header component
},
onClick: () => {
// Handle search button click if needed
},
onClear: () => {
// Handle search clear
},
},
addNav: {
label: 'Tambah Divisi',
icon: 'i-lucide-send',
onClick: () => {
isFormEntryDialogOpen.value = true
},
},
}
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
// #endregion
// #region Functions
async function handleDeleteRow(record: any) {
try {
// TODO : hit backend request untuk delete
console.log('Deleting record:', record)
// Simulate API call
// const response = await xfetch(`/api/v1/division/${record.id}`, {
// method: 'DELETE'
// })
// Refresh data setelah berhasil delete
await getDivisionList()
// TODO: Show success message
console.log('Record deleted successfully')
} catch (error) {
console.error('Error deleting record:', error)
// TODO: Show error message
} finally {
// Reset record state
recId.value = 0
recAction.value = ''
recItem.value = null
}
}
// #endregion region
// #region Form event handlers
function onCancelForm(resetForm: () => void) {
isFormEntryDialogOpen.value = false
setTimeout(() => {
resetForm()
}, 500)
}
async function onSubmitForm(values: any, resetForm: () => void) {
let isSuccess = false
try {
// TODO: Implement form submission logic
console.log('Form submitted:', values)
// Simulate API call
// const response = await xfetch('/api/v1/division', {
// method: 'POST',
// body: JSON.stringify(values)
// })
// If successful, mark as success and close dialog
isFormEntryDialogOpen.value = false
isSuccess = true
// Refresh data after successful submission
await getDivisionList()
// TODO: Show success message
console.log('Division created successfully')
} catch (error: unknown) {
console.warn('Error submitting form:', error)
isSuccess = false
// Don't close dialog or reset form on error
// TODO: Show error message to user
} finally {
if (isSuccess) {
setTimeout(() => {
resetForm()
}, 500)
}
}
}
// #endregion
// #region Watchers
// Watch for row actions
watch(recId, () => {
switch (recAction.value) {
case ActionEvents.showEdit:
// TODO: Handle edit action
// isFormEntryDialogOpen.value = true
break
case ActionEvents.showConfirmDelete:
// Trigger confirmation modal open
isRecordConfirmationOpen.value = true
break
}
})
// Handle confirmation result
function handleConfirmDelete(record: any, action: string) {
console.log('Confirmed action:', action, 'for record:', record)
handleDeleteRow(record)
}
function handleCancelConfirmation() {
// Reset record state when cancelled
recId.value = 0
recAction.value = ''
recItem.value = null
}
// #endregion
</script>
<template>
<Header v-model="searchInput" :prep="headerPrep" @search="handleSearch" />
<AppDivisonList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
<Dialog v-model:open="isFormEntryDialogOpen" title="Tambah Divisi" size="lg" prevent-outside>
<AppDivisionEntryFormPrev
:division="divisionConf" :division-tree="divisionTreeConfig" :schema="schema"
:initial-values="{ name: '', code: '', parentId: '' }" @submit="onSubmitForm" @cancel="onCancelForm"
/>
</Dialog>
<!-- Record Confirmation Modal -->
<RecordConfirmation
v-model:open="isRecordConfirmationOpen" action="delete" :record="recItem"
@confirm="handleConfirmDelete" @cancel="handleCancelConfirmation"
>
<template #default="{ record }">
<div class="text-sm">
<p><strong>ID:</strong> {{ record?.id }}</p>
<p v-if="record?.firstName"><strong>Nama:</strong> {{ record.firstName }}</p>
<p v-if="record?.code"><strong>Kode:</strong> {{ record.cellphone }}</p>
</div>
</template>
</RecordConfirmation>
</template>
<style scoped>
/* component style */
</style>
+92 -138
View File
@@ -1,28 +1,39 @@
<script setup lang="ts"> <script setup lang="ts">
import type { HeaderPrep } from '~/components/pub/custom-ui/data/types' // Components
import AppDivisionEntryForm from '~/components/app/divison/entry-form.vue'
import Dialog from '~/components/pub/base/modal/dialog.vue' import Dialog from '~/components/pub/base/modal/dialog.vue'
import Header from '~/components/pub/custom-ui/nav-header/prep.vue'
import RecordConfirmation from '~/components/pub/custom-ui/confirmation/record-confirmation.vue' import RecordConfirmation from '~/components/pub/custom-ui/confirmation/record-confirmation.vue'
import { ActionEvents } from '~/components/pub/custom-ui/data/types' import AppDivisionList from '~/components/app/division/list.vue'
import Header from '~/components/pub/custom-ui/nav-header/header.vue' import AppDivisionEntryForm from '~/components/app/division/entry-form.vue'
// Helpers
import { usePaginatedList } from '~/composables/usePaginatedList' import { usePaginatedList } from '~/composables/usePaginatedList'
import { divisionConf, divisionTreeConfig, schema } from './entry' import { toast } from '~/components/pub/ui/toast'
// #region State & Computed
// Dialog state
const isFormEntryDialogOpen = ref(false)
const isRecordConfirmationOpen = ref(false)
// Table action rowId provider // Types
const recId = ref<number>(0) import { ActionEvents, type HeaderPrep } from '~/components/pub/custom-ui/data/types'
const recAction = ref<string>('') import { DivisionSchema, type DivisionFormData } from '~/schemas/division.schema'
const recItem = ref<any>(null)
async function fetchDivisionData(_params: any) { // Handlers
const endpoint = '/api/v1/_dev/division/list' import {
return await xfetch(endpoint) recId,
} recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} from '~/handlers/division.handler'
// Services
import { getDivisions, getDivisionDetail } from '~/services/division.service'
const title = ref('')
// Menggunakan composable untuk pagination
const { const {
data, data,
isLoading, isLoading,
@@ -32,7 +43,10 @@ const {
handleSearch, handleSearch,
fetchData: getDivisionList, fetchData: getDivisionList,
} = usePaginatedList({ } = usePaginatedList({
fetchFn: fetchDivisionData, fetchFn: async ({ page, search }) => {
const result = await getDivisions({ search, page })
return { success: result.success || false, body: result.body || {} }
},
entityName: 'division', entityName: 'division',
}) })
@@ -44,21 +58,20 @@ const headerPrep: HeaderPrep = {
minLength: 3, minLength: 3,
debounceMs: 500, debounceMs: 500,
showValidationFeedback: true, showValidationFeedback: true,
onInput: (_val: string) => { onInput: (value: string) => {
// Handle search input - this will be triggered by the header component searchInput.value = value
},
onClick: () => {
// Handle search button click if needed
},
onClear: () => {
// Handle search clear
}, },
onClick: () => {},
onClear: () => {},
}, },
addNav: { addNav: {
label: 'Tambah Divisi', label: 'Tambah',
icon: 'i-lucide-send', icon: 'i-lucide-plus',
onClick: () => { onClick: () => {
recItem.value = null
recId.value = 0
isFormEntryDialogOpen.value = true isFormEntryDialogOpen.value = true
isReadonly.value = false
}, },
}, },
} }
@@ -67,142 +80,83 @@ provide('rec_id', recId)
provide('rec_action', recAction) provide('rec_action', recAction)
provide('rec_item', recItem) provide('rec_item', recItem)
provide('table_data_loader', isLoading) provide('table_data_loader', isLoading)
// #endregion
// #region Functions const getCurrentDivisionDetail = async (id: number | string) => {
const result = await getDivisionDetail(id)
async function handleDeleteRow(record: any) { if (result.success) {
try { const currentValue = result.body?.data || {}
// TODO : hit backend request untuk delete recItem.value = currentValue
console.log('Deleting record:', record) isFormEntryDialogOpen.value = true
// Simulate API call
// const response = await xfetch(`/api/v1/division/${record.id}`, {
// method: 'DELETE'
// })
// Refresh data setelah berhasil delete
await getDivisionList()
// TODO: Show success message
console.log('Record deleted successfully')
} catch (error) {
console.error('Error deleting record:', error)
// TODO: Show error message
} finally {
// Reset record state
recId.value = 0
recAction.value = ''
recItem.value = null
} }
} }
// #endregion region // Watch for row actions when recId or recAction changes
watch([recId, recAction], () => {
// #region Form event handlers
function onCancelForm(resetForm: () => void) {
isFormEntryDialogOpen.value = false
setTimeout(() => {
resetForm()
}, 500)
}
async function onSubmitForm(values: any, resetForm: () => void) {
let isSuccess = false
try {
// TODO: Implement form submission logic
console.log('Form submitted:', values)
// Simulate API call
// const response = await xfetch('/api/v1/division', {
// method: 'POST',
// body: JSON.stringify(values)
// })
// If successful, mark as success and close dialog
isFormEntryDialogOpen.value = false
isSuccess = true
// Refresh data after successful submission
await getDivisionList()
// TODO: Show success message
console.log('Division created successfully')
} catch (error: unknown) {
console.warn('Error submitting form:', error)
isSuccess = false
// Don't close dialog or reset form on error
// TODO: Show error message to user
} finally {
if (isSuccess) {
setTimeout(() => {
resetForm()
}, 500)
}
}
}
// #endregion
// #region Watchers
// Watch for row actions
watch(recId, () => {
switch (recAction.value) { switch (recAction.value) {
case ActionEvents.showDetail:
getCurrentDivisionDetail(recId.value)
title.value = 'Detail Divisi'
isReadonly.value = true
break
case ActionEvents.showEdit: case ActionEvents.showEdit:
// TODO: Handle edit action getCurrentDivisionDetail(recId.value)
// isFormEntryDialogOpen.value = true title.value = 'Edit Divisi'
isReadonly.value = false
break break
case ActionEvents.showConfirmDelete: case ActionEvents.showConfirmDelete:
// Trigger confirmation modal open
isRecordConfirmationOpen.value = true isRecordConfirmationOpen.value = true
break break
} }
}) })
// Handle confirmation result onMounted(async () => {
function handleConfirmDelete(record: any, action: string) { await getDivisionList()
console.log('Confirmed action:', action, 'for record:', record) })
handleDeleteRow(record)
}
function handleCancelConfirmation() {
// Reset record state when cancelled
recId.value = 0
recAction.value = ''
recItem.value = null
}
// #endregion
</script> </script>
<template> <template>
<Header v-model="searchInput" :prep="headerPrep" @search="handleSearch" /> <Header
<AppDivisonList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" /> v-model="searchInput"
:prep="headerPrep"
<Dialog v-model:open="isFormEntryDialogOpen" title="Tambah Divisi" size="lg" prevent-outside> :ref-search-nav="headerPrep.refSearchNav"
@search="handleSearch"
class="mb-4 xl:mb-5"
/>
<AppDivisionList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
<Dialog v-model:open="isFormEntryDialogOpen" :title="!!recItem ? title : 'Tambah Divisi'" size="lg" prevent-outside>
<AppDivisionEntryForm <AppDivisionEntryForm
:division="divisionConf" :division-tree="divisionTreeConfig" :schema="schema" :schema="DivisionSchema"
:initial-values="{ name: '', code: '', parentId: '' }" @submit="onSubmitForm" @cancel="onCancelForm" :values="recItem"
:is-loading="isProcessing"
:is-readonly="isReadonly"
@submit="
(values: DivisionFormData | Record<string, any>, resetForm: () => void) => {
if (recId > 0) {
handleActionEdit(recId, values, getDivisionList, resetForm, toast)
return
}
handleActionSave(values, getDivisionList, resetForm, toast)
}
"
@cancel="handleCancelForm"
/> />
</Dialog> </Dialog>
<!-- Record Confirmation Modal --> <!-- Record Confirmation Modal -->
<RecordConfirmation <RecordConfirmation
v-model:open="isRecordConfirmationOpen" action="delete" :record="recItem" v-model:open="isRecordConfirmationOpen"
@confirm="handleConfirmDelete" @cancel="handleCancelConfirmation" action="delete"
> :record="recItem"
@confirm="() => handleActionRemove(recId, getDivisionList, toast)"
@cancel=""
>
<template #default="{ record }"> <template #default="{ record }">
<div class="text-sm"> <div class="text-sm">
<p><strong>ID:</strong> {{ record?.id }}</p> <p><strong>ID:</strong> {{ record?.id }}</p>
<p v-if="record?.firstName"><strong>Nama:</strong> {{ record.firstName }}</p> <p v-if="record?.name"><strong>Nama:</strong> {{ record.name }}</p>
<p v-if="record?.code"><strong>Kode:</strong> {{ record.cellphone }}</p> <p v-if="record?.code"><strong>Kode:</strong> {{ record.code }}</p>
</div> </div>
</template> </template>
</RecordConfirmation> </RecordConfirmation>
</template> </template>
<style scoped>
/* component style */
</style>
+13 -10
View File
@@ -2,8 +2,9 @@
// Components // Components
import Dialog from '~/components/pub/base/modal/dialog.vue' import Dialog from '~/components/pub/base/modal/dialog.vue'
import Header from '~/components/pub/custom-ui/nav-header/prep.vue' import Header from '~/components/pub/custom-ui/nav-header/prep.vue'
import AppEquipmentEntryForm from '~/components/app/equipment/entry-form.vue'
import RecordConfirmation from '~/components/pub/custom-ui/confirmation/record-confirmation.vue' import RecordConfirmation from '~/components/pub/custom-ui/confirmation/record-confirmation.vue'
import AppEquipmentList from '~/components/app/equipment/list.vue'
import AppEquipmentEntryForm from '~/components/app/equipment/entry-form.vue'
// Helpers // Helpers
import { usePaginatedList } from '~/composables/usePaginatedList' import { usePaginatedList } from '~/composables/usePaginatedList'
@@ -60,15 +61,11 @@ const headerPrep: HeaderPrep = {
minLength: 3, minLength: 3,
debounceMs: 500, debounceMs: 500,
showValidationFeedback: true, showValidationFeedback: true,
onInput: (_val: string) => { onInput: (value: string) => {
// Handle search input - this will be triggered by the header component searchInput.value = value
},
onClick: () => {
// Handle search button click if needed
},
onClear: () => {
// Handle search clear
}, },
onClick: () => {},
onClear: () => {},
}, },
addNav: { addNav: {
label: 'Tambah Perlengkapan', label: 'Tambah Perlengkapan',
@@ -130,7 +127,13 @@ onMounted(async () => {
</script> </script>
<template> <template>
<Header v-model="searchInput" :prep="headerPrep" @search="handleSearch" class="mb-4 xl:mb-5" /> <Header
v-model="searchInput"
:prep="headerPrep"
@search="handleSearch"
:ref-search-nav="headerPrep.refSearchNav"
class="mb-4 xl:mb-5"
/>
<AppEquipmentList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" /> <AppEquipmentList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
<Dialog <Dialog
-34
View File
@@ -1,34 +0,0 @@
<script setup lang="ts">
// types
import type { MaterialFormData } from '~/schemas/material.schema'
import { MaterialSchema } from '~/schemas/material.schema'
const isLoading = ref(false)
const uoms = [
{ value: 'uom-1', label: 'Satuan 1' },
{ value: 'uom-2', label: 'Satuan 2' },
{ value: 'uom-3', label: 'Satuan 3' },
]
const items = [
{ value: 'item-1', label: 'Item 1' },
{ value: 'item-2', label: 'Item 2' },
{ value: 'item-3', label: 'Item 3' },
]
function onBack() {
navigateTo('/tools-equipment-src/equipment')
}
async function onSubmit(data: MaterialFormData) {
console.log(data)
}
</script>
<template>
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
<Icon name="i-lucide-panel-bottom" class="me-2" />
<span class="font-semibold">Tambah</span> Perlengkapan (BMHP)
</div>
<AppMaterialEntryForm :is-loading="isLoading" :schema="MaterialSchema" :uoms="uoms" :items="items" @back="onBack"
@submit="onSubmit" />
</template>
-65
View File
@@ -1,65 +0,0 @@
<script setup lang="ts">
import type { DataTableLoader } from '~/components/pub/base/data-table/type'
import type { HeaderPrep, RefSearchNav } from '~/components/pub/custom-ui/data/types'
import Header from '~/components/pub/custom-ui/nav-header/prep.vue'
const data = ref([])
const refSearchNav: RefSearchNav = {
onClick: () => {
// open filter modal
},
onInput: (_val: string) => {
// filter patient list
},
onClear: () => {
// clear url param
},
}
const isLoading = reactive<DataTableLoader>({
isTableLoading: false,
})
const recId = ref<number>(0)
const recAction = ref<string>('')
const recItem = ref<any>(null)
const headerPrep: HeaderPrep = {
title: 'Perlengkapan (BMHP)',
icon: 'i-lucide-panel-bottom',
addNav: {
label: 'Tambah',
onClick: () => navigateTo('/tools-equipment-src/equipment/add'),
},
}
async function getMaterialList() {
isLoading.dataListLoading = true
// const resp = await xfetch('/api/v1/material')
// if (resp.success) {
// data.value = (resp.body as Record<string, any>).data
// }
isLoading.dataListLoading = false
}
onMounted(() => {
getMaterialList()
})
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
</script>
<template>
<div class="rounded-md border p-4">
<Header :prep="{ ...headerPrep }" :ref-search-nav="refSearchNav" />
<div class="rounded-md border p-4">
<AppMaterialList v-if="!isLoading.dataListLoading" :data="data" />
</div>
</div>
</template>
+15 -6
View File
@@ -2,8 +2,9 @@
// Components // Components
import Dialog from '~/components/pub/base/modal/dialog.vue' import Dialog from '~/components/pub/base/modal/dialog.vue'
import Header from '~/components/pub/custom-ui/nav-header/prep.vue' import Header from '~/components/pub/custom-ui/nav-header/prep.vue'
import AppMedicineGroupEntryForm from '~/components/app/medicine-group/entry-form.vue'
import RecordConfirmation from '~/components/pub/custom-ui/confirmation/record-confirmation.vue' import RecordConfirmation from '~/components/pub/custom-ui/confirmation/record-confirmation.vue'
import AppMedicineGroupList from '~/components/app/medicine-group/list.vue'
import AppMedicineGroupEntryForm from '~/components/app/medicine-group/entry-form.vue'
// Helpers // Helpers
import { usePaginatedList } from '~/composables/usePaginatedList' import { usePaginatedList } from '~/composables/usePaginatedList'
@@ -11,7 +12,7 @@ import { toast } from '~/components/pub/ui/toast'
// Types // Types
import { ActionEvents, type HeaderPrep } from '~/components/pub/custom-ui/data/types' import { ActionEvents, type HeaderPrep } from '~/components/pub/custom-ui/data/types'
import { MedicineBaseSchema, type MedicineBaseFormData } from '~/schemas/medicine.schema' import { BaseSchema, type BaseFormData } from '~/schemas/base.schema'
// Handlers // Handlers
import { import {
@@ -57,7 +58,9 @@ const headerPrep: HeaderPrep = {
minLength: 3, minLength: 3,
debounceMs: 500, debounceMs: 500,
showValidationFeedback: true, showValidationFeedback: true,
onInput: (_val: string) => {}, onInput: (value: string) => {
searchInput.value = value
},
onClick: () => {}, onClick: () => {},
onClear: () => {}, onClear: () => {},
}, },
@@ -112,7 +115,13 @@ onMounted(async () => {
</script> </script>
<template> <template>
<Header v-model="searchInput" :prep="headerPrep" @search="handleSearch" class="mb-4 xl:mb-5" /> <Header
v-model="searchInput"
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
@search="handleSearch"
class="mb-4 xl:mb-5"
/>
<AppMedicineGroupList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" /> <AppMedicineGroupList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
<Dialog <Dialog
@@ -122,12 +131,12 @@ onMounted(async () => {
prevent-outside prevent-outside
> >
<AppMedicineGroupEntryForm <AppMedicineGroupEntryForm
:schema="MedicineBaseSchema" :schema="BaseSchema"
:values="recItem" :values="recItem"
:is-loading="isProcessing" :is-loading="isProcessing"
:is-readonly="isReadonly" :is-readonly="isReadonly"
@submit=" @submit="
(values: MedicineBaseFormData | Record<string, any>, resetForm: () => void) => { (values: BaseFormData | Record<string, any>, resetForm: () => void) => {
if (recId > 0) { if (recId > 0) {
handleActionEdit(recId, values, getMedicineGroupList, resetForm, toast) handleActionEdit(recId, values, getMedicineGroupList, resetForm, toast)
return return
@@ -2,8 +2,9 @@
// Components // Components
import Dialog from '~/components/pub/base/modal/dialog.vue' import Dialog from '~/components/pub/base/modal/dialog.vue'
import Header from '~/components/pub/custom-ui/nav-header/prep.vue' import Header from '~/components/pub/custom-ui/nav-header/prep.vue'
import AppMedicineMethodEntryForm from '~/components/app/medicine-method/entry-form.vue'
import RecordConfirmation from '~/components/pub/custom-ui/confirmation/record-confirmation.vue' import RecordConfirmation from '~/components/pub/custom-ui/confirmation/record-confirmation.vue'
import AppMedicineMethodList from '~/components/app/medicine-method/list.vue'
import AppMedicineMethodEntryForm from '~/components/app/medicine-method/entry-form.vue'
// Helpers // Helpers
import { usePaginatedList } from '~/composables/usePaginatedList' import { usePaginatedList } from '~/composables/usePaginatedList'
@@ -11,7 +12,7 @@ import { toast } from '~/components/pub/ui/toast'
// Types // Types
import { ActionEvents, type HeaderPrep } from '~/components/pub/custom-ui/data/types' import { ActionEvents, type HeaderPrep } from '~/components/pub/custom-ui/data/types'
import { MedicineBaseSchema, type MedicineBaseFormData } from '~/schemas/medicine.schema' import { BaseSchema, type BaseFormData } from '~/schemas/base.schema'
// Handlers // Handlers
import { import {
@@ -57,7 +58,9 @@ const headerPrep: HeaderPrep = {
minLength: 3, minLength: 3,
debounceMs: 500, debounceMs: 500,
showValidationFeedback: true, showValidationFeedback: true,
onInput: (_val: string) => {}, onInput: (value: string) => {
searchInput.value = value
},
onClick: () => {}, onClick: () => {},
onClear: () => {}, onClear: () => {},
}, },
@@ -112,7 +115,13 @@ onMounted(async () => {
</script> </script>
<template> <template>
<Header v-model="searchInput" :prep="headerPrep" @search="handleSearch" class="mb-4 xl:mb-5" /> <Header
v-model="searchInput"
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
@search="handleSearch"
class="mb-4 xl:mb-5"
/>
<AppMedicineMethodList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" /> <AppMedicineMethodList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
<Dialog <Dialog
@@ -122,12 +131,12 @@ onMounted(async () => {
prevent-outside prevent-outside
> >
<AppMedicineMethodEntryForm <AppMedicineMethodEntryForm
:schema="MedicineBaseSchema" :schema="BaseSchema"
:values="recItem" :values="recItem"
:is-loading="isProcessing" :is-loading="isProcessing"
:is-readonly="isReadonly" :is-readonly="isReadonly"
@submit=" @submit="
(values: MedicineBaseFormData | Record<string, any>, resetForm: () => void) => { (values: BaseFormData | Record<string, any>, resetForm: () => void) => {
if (recId > 0) { if (recId > 0) {
handleActionEdit(recId, values, getMedicineMethodList, resetForm, toast) handleActionEdit(recId, values, getMedicineMethodList, resetForm, toast)
return return
+184 -41
View File
@@ -1,63 +1,206 @@
<script setup lang="ts"> <script setup lang="ts">
import type { DataTableLoader } from '~/components/pub/base/data-table/type' // Components
import type { HeaderPrep, RefSearchNav } from '~/components/pub/custom-ui/data/types' import Dialog from '~/components/pub/base/modal/dialog.vue'
import Header from '~/components/pub/custom-ui/nav-header/prep.vue' import Header from '~/components/pub/custom-ui/nav-header/prep.vue'
import RecordConfirmation from '~/components/pub/custom-ui/confirmation/record-confirmation.vue'
import AppMedicineList from '~/components/app/medicine/list.vue'
import AppMedicineEntryForm from '~/components/app/medicine/entry-form.vue'
const data = ref([]) // Helpers
import { usePaginatedList } from '~/composables/usePaginatedList'
import { toast } from '~/components/pub/ui/toast'
const refSearchNav: RefSearchNav = { // Types
onClick: () => { import { ActionEvents, type HeaderPrep } from '~/components/pub/custom-ui/data/types'
// open filter modal import { MedicineSchema, type MedicineFormData } from '~/schemas/medicine.schema'
}, import type { MedicineGroup } from '~/models/medicine-group'
onInput: (_val: string) => { import type { MedicineMethod } from '~/models/medicine-method'
// filter patient list import type { Uom } from '~/models/uom'
},
onClear: () => {
// clear url param
},
}
// Loading state management // Handlers
const isLoading = reactive<DataTableLoader>({ import {
summary: false, recId,
isTableLoading: false, recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} from '~/handlers/medicine.handler'
// Services
import { getMedicines, getMedicineDetail } from '~/services/medicine.service'
import { getMedicineGroups } from '~/services/medicine-group.service'
import { getMedicineMethods } from '~/services/medicine-method.service'
import { getUoms } from '~/services/uom.service'
const medicineGroups = ref<{ value: string; label: string }[]>([])
const medicineMethods = ref<{ value: string; label: string }[]>([])
const uoms = ref<{ value: string; label: string }[]>([])
const title = ref('')
const {
data,
isLoading,
paginationMeta,
searchInput,
handlePageChange,
handleSearch,
fetchData: getMedicineList,
} = usePaginatedList({
fetchFn: async ({ page, search }) => {
const result = await getMedicines({ search, page })
return { success: result.success || false, body: result.body || {} }
},
entityName: 'medicine',
}) })
const recId = ref<number>(0) const headerPrep: HeaderPrep = {
const recAction = ref<string>('')
const recItem = ref<any>(null)
const hreaderPrep: HeaderPrep = {
title: 'Obat', title: 'Obat',
icon: 'i-lucide-medicine-bottle', icon: 'i-lucide-medicine-bottle',
refSearchNav: {
placeholder: 'Cari (min. 3 karakter)...',
minLength: 3,
debounceMs: 500,
showValidationFeedback: true,
onInput: (value: string) => {
searchInput.value = value
},
onClick: () => {},
onClear: () => {},
},
addNav: { addNav: {
label: 'Tambah', label: 'Tambah',
onClick: () => navigateTo('/tools-equipment-src/medicine/add'), icon: 'i-lucide-plus',
onClick: () => {
recItem.value = null
recId.value = 0
isFormEntryDialogOpen.value = true
isReadonly.value = false
},
}, },
} }
async function getPatientList() {
isLoading.isTableLoading = true
const resp = await xfetch('/api/v1/medicine')
if (resp.success) {
data.value = (resp.body as Record<string, any>).data
}
isLoading.isTableLoading = false
}
onMounted(() => {
getPatientList()
})
provide('rec_id', recId) provide('rec_id', recId)
provide('rec_action', recAction) provide('rec_action', recAction)
provide('rec_item', recItem) provide('rec_item', recItem)
provide('table_data_loader', isLoading) provide('table_data_loader', isLoading)
const getCurrentMedicineDetail = async (id: number | string) => {
const result = await getMedicineDetail(id)
if (result.success) {
const currentValue = result.body?.data || {}
recItem.value = currentValue
isFormEntryDialogOpen.value = true
}
}
const getMedicineGroupList = async () => {
const result = await getMedicineGroups()
if (result.success) {
const currentMedicineGroups = result.body?.data || []
medicineGroups.value = currentMedicineGroups.map((medicineGroup: MedicineGroup) => ({
value: medicineGroup.code,
label: medicineGroup.name,
}))
}
}
const getMedicineMethodList = async () => {
const result = await getMedicineMethods()
if (result.success) {
const currentMedicineMethods = result.body?.data || []
medicineMethods.value = currentMedicineMethods.map((medicineMethod: MedicineMethod) => ({
value: medicineMethod.code,
label: medicineMethod.name,
}))
}
}
const getUomList = async () => {
const result = await getUoms()
if (result.success) {
const currentUoms = result.body?.data || []
uoms.value = currentUoms.map((uom: Uom) => ({ value: uom.code || uom.erp_id, label: uom.name }))
}
}
watch([recId, recAction], () => {
switch (recAction.value) {
case ActionEvents.showDetail:
getCurrentMedicineDetail(recId.value)
title.value = 'Detail Obat'
isReadonly.value = true
break
case ActionEvents.showEdit:
getCurrentMedicineDetail(recId.value)
title.value = 'Edit Obat'
isReadonly.value = false
break
case ActionEvents.showConfirmDelete:
isRecordConfirmationOpen.value = true
break
}
})
onMounted(async () => {
await getMedicineGroupList()
await getMedicineMethodList()
await getUomList()
await getMedicineList()
})
</script> </script>
<template> <template>
<Header :prep="{ ...hreaderPrep }" :ref-search-nav="refSearchNav" /> <Header
<div class="my-4 flex flex-1 flex-col gap-4 md:gap-8"> v-model="searchInput"
<AppMedicineList :data="data" /> :prep="headerPrep"
</div> @search="handleSearch"
:ref-search-nav="headerPrep.refSearchNav"
class="mb-4 xl:mb-5"
/>
<AppMedicineList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
<Dialog v-model:open="isFormEntryDialogOpen" :title="!!recItem ? title : 'Tambah Obat'" size="lg" prevent-outside>
<AppMedicineEntryForm
:schema="MedicineSchema"
:values="recItem"
:medicineGroups="medicineGroups"
:medicineMethods="medicineMethods"
:uoms="uoms"
:is-loading="isProcessing"
:is-readonly="isReadonly"
@submit="
(values: MedicineFormData | Record<string, any>, resetForm: () => void) => {
if (recId > 0) {
handleActionEdit(recId, values, getMedicineList, resetForm, toast)
return
}
handleActionSave(values, getMedicineList, resetForm, toast)
}
"
@cancel="handleCancelForm"
/>
</Dialog>
<!-- Record Confirmation Modal -->
<RecordConfirmation
v-model:open="isRecordConfirmationOpen"
action="delete"
:record="recItem"
@confirm="() => handleActionRemove(recId, getMedicineList, 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 }}</p>
</div>
</template>
</RecordConfirmation>
</template> </template>
@@ -0,0 +1,240 @@
<script setup lang="ts">
import type { HeaderPrep } from '~/components/pub/custom-ui/data/types'
import AppSpecialistEntryForm from '~/components/app/specialist/entry-form.vue'
import Dialog from '~/components/pub/base/modal/dialog.vue'
import RecordConfirmation from '~/components/pub/custom-ui/confirmation/record-confirmation.vue'
import { ActionEvents } from '~/components/pub/custom-ui/data/types'
import Header from '~/components/pub/custom-ui/nav-header/header.vue'
import { usePaginatedList } from '~/composables/usePaginatedList'
import { createFilteredUnitConf, installationConf, schemaConf, unitConf } from './entry'
// #region State & Computed
// Dialog state
const isFormEntryDialogOpen = ref(false)
const isRecordConfirmationOpen = ref(false)
// Table action rowId provider
const recId = ref<number>(0)
const recAction = ref<string>('')
const recItem = ref<any>(null)
// State untuk tracking installation yang dipilih di form
const selectedInstallationId = ref<string>('')
// Computed untuk filtered unit berdasarkan installation yang dipilih
const filteredUnitConf = computed(() => {
if (!selectedInstallationId.value) {
return { ...unitConf, items: [] }
}
return createFilteredUnitConf(selectedInstallationId.value)
})
const {
data,
isLoading,
paginationMeta,
searchInput,
handlePageChange,
handleSearch,
fetchData: getSpecialistList,
} = usePaginatedList({
fetchFn: fetchSpecialistData,
entityName: 'specialist',
})
const headerPrep: HeaderPrep = {
title: 'Specialist',
icon: 'i-lucide-box',
refSearchNav: {
placeholder: 'Cari (min. 3 karakter)...',
minLength: 3,
debounceMs: 500,
showValidationFeedback: true,
onInput: (_val: string) => {
// Handle search input - this will be triggered by the header component
},
onClick: () => {
// Handle search button click if needed
},
onClear: () => {
// Handle search clear
},
},
addNav: {
label: 'Tambah Specialist',
icon: 'i-lucide-send',
onClick: () => {
isFormEntryDialogOpen.value = true
},
},
}
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
// #endregion
// #region Functions
async function fetchSpecialistData(params: any) {
const endpoint = transform('/api/v1/patient', params)
return await xfetch(endpoint)
}
async function handleDeleteRow(record: any) {
try {
// TODO : hit backend request untuk delete
console.log('Deleting record:', record)
// Simulate API call
// const response = await xfetch(`/api/v1/Installation/${record.id}`, {
// method: 'DELETE'
// })
// Refresh data setelah berhasil delete
await getSpecialistList()
// TODO: Show success message
console.log('Record deleted successfully')
} catch (error) {
console.error('Error deleting record:', error)
// TODO: Show error message
} finally {
// Reset record state
recId.value = 0
recAction.value = ''
recItem.value = null
}
}
// Handle confirmation result
function handleConfirmDelete(record: any, action: string) {
console.log('Confirmed action:', action, 'for record:', record)
handleDeleteRow(record)
}
function handleCancelConfirmation() {
// Reset record state when cancelled
recId.value = 0
recAction.value = ''
recItem.value = null
}
// #endregion region
// #region Form event handlers
function onCancelForm(resetForm: () => void) {
isFormEntryDialogOpen.value = false
// Reset installation selection ketika form dibatal
selectedInstallationId.value = ''
setTimeout(() => {
resetForm()
}, 500)
}
function onInstallationChanged(installationId: string) {
// Update local state untuk trigger re-render filtered units
selectedInstallationId.value = installationId
// The filteredUnitConf computed will automatically update
// based on the new selectedInstallationId value
}
async function onSubmitForm(values: any, resetForm: () => void) {
let isSuccess = false
try {
// TODO: Implement form submission logic
console.log('Form submitted:', values)
// Simulate API call
// const response = await xfetch('/api/v1/Installation', {
// method: 'POST',
// body: JSON.stringify(values)
// })
// If successful, mark as success and close dialog
isFormEntryDialogOpen.value = false
isSuccess = true
// Reset installation selection ketika form berhasil submit
selectedInstallationId.value = ''
// Refresh data after successful submission
await getSpecialistList()
// TODO: Show success message
console.log('Installation created successfully')
} catch (error: unknown) {
console.warn('Error submitting form:', error)
isSuccess = false
// Don't close dialog or reset form on error
// TODO: Show error message to user
} finally {
if (isSuccess) {
setTimeout(() => {
resetForm()
}, 500)
}
}
}
// #endregion
// #region Watchers
// Watch for row actions
watch(recId, () => {
switch (recAction.value) {
case ActionEvents.showEdit:
// TODO: Handle edit action
// isFormEntryDialogOpen.value = true
break
case ActionEvents.showConfirmDelete:
// Trigger confirmation modal open
isRecordConfirmationOpen.value = true
break
}
})
// Note: Installation change logic is now handled by the entry-form component
// through the onInstallationChanged event handler
// #endregion
</script>
<template>
<Header v-model="searchInput" :prep="headerPrep" @search="handleSearch" />
<AppSpecialistList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
<Dialog v-model:open="isFormEntryDialogOpen" :title="headerPrep.addNav?.label!" size="lg" prevent-outside>
<AppSpecialistEntryFormPrev
:installation="installationConf"
:unit="filteredUnitConf"
:schema="schemaConf"
:disabled-unit="!selectedInstallationId"
:initial-values="{ name: '', code: '', installationId: '', unitId: '' }"
@submit="onSubmitForm"
@cancel="onCancelForm"
@installation-changed="onInstallationChanged"
/>
</Dialog>
<RecordConfirmation
v-model:open="isRecordConfirmationOpen"
action="delete"
:record="recItem"
@confirm="handleConfirmDelete"
@cancel="handleCancelConfirmation"
>
<template #default="{ record }">
<div class="text-sm">
<p><strong>ID:</strong> {{ record?.id }}</p>
<p v-if="record?.firstName"><strong>Nama:</strong> {{ record.firstName }}</p>
<p v-if="record?.code"><strong>Kode:</strong> {{ record.cellphone }}</p>
</div>
</template>
</RecordConfirmation>
</template>
<style scoped>
/* component style */
</style>
+101 -161
View File
@@ -1,33 +1,41 @@
<script setup lang="ts"> <script setup lang="ts">
import type { HeaderPrep } from '~/components/pub/custom-ui/data/types' // Components
import AppSpecialistEntryForm from '~/components/app/specialist/entry-form.vue'
import Dialog from '~/components/pub/base/modal/dialog.vue' import Dialog from '~/components/pub/base/modal/dialog.vue'
import Header from '~/components/pub/custom-ui/nav-header/prep.vue'
import RecordConfirmation from '~/components/pub/custom-ui/confirmation/record-confirmation.vue' import RecordConfirmation from '~/components/pub/custom-ui/confirmation/record-confirmation.vue'
import { ActionEvents } from '~/components/pub/custom-ui/data/types' import AppSpecialistList from '~/components/app/specialist/list.vue'
import Header from '~/components/pub/custom-ui/nav-header/header.vue' import AppSpecialistEntryForm from '~/components/app/specialist/entry-form.vue'
// Helpers
import { usePaginatedList } from '~/composables/usePaginatedList' import { usePaginatedList } from '~/composables/usePaginatedList'
import { createFilteredUnitConf, installationConf, schemaConf, unitConf } from './entry' import { toast } from '~/components/pub/ui/toast'
// #region State & Computed // Types
// Dialog state import { ActionEvents, type HeaderPrep } from '~/components/pub/custom-ui/data/types'
const isFormEntryDialogOpen = ref(false) import { SpecialistSchema, type SpecialistFormData } from '~/schemas/specialist.schema'
const isRecordConfirmationOpen = ref(false) import type { Unit } from '~/models/unit'
// Table action rowId provider // Handlers
const recId = ref<number>(0) import {
const recAction = ref<string>('') recId,
const recItem = ref<any>(null) recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} from '~/handlers/specialist.handler'
// State untuk tracking installation yang dipilih di form // Services
const selectedInstallationId = ref<string>('') import { getSpecialists, getSpecialistDetail } from '~/services/specialist.service'
import { getUnits } from '~/services/unit.service'
// Computed untuk filtered unit berdasarkan installation yang dipilih const units = ref<{ value: string; label: string }[]>([])
const filteredUnitConf = computed(() => { const title = ref('')
if (!selectedInstallationId.value) {
return { ...unitConf, items: [] }
}
return createFilteredUnitConf(selectedInstallationId.value)
})
const { const {
data, data,
@@ -38,7 +46,10 @@ const {
handleSearch, handleSearch,
fetchData: getSpecialistList, fetchData: getSpecialistList,
} = usePaginatedList({ } = usePaginatedList({
fetchFn: fetchSpecialistData, fetchFn: async ({ page, search }) => {
const result = await getSpecialists({ search, page })
return { success: result.success || false, body: result.body || {} }
},
entityName: 'specialist', entityName: 'specialist',
}) })
@@ -50,21 +61,20 @@ const headerPrep: HeaderPrep = {
minLength: 3, minLength: 3,
debounceMs: 500, debounceMs: 500,
showValidationFeedback: true, showValidationFeedback: true,
onInput: (_val: string) => { onInput: (value: string) => {
// Handle search input - this will be triggered by the header component searchInput.value = value
},
onClick: () => {
// Handle search button click if needed
},
onClear: () => {
// Handle search clear
}, },
onClick: () => {},
onClear: () => {},
}, },
addNav: { addNav: {
label: 'Tambah Specialist', label: 'Tambah',
icon: 'i-lucide-send', icon: 'i-lucide-plus',
onClick: () => { onClick: () => {
recItem.value = null
recId.value = 0
isFormEntryDialogOpen.value = true isFormEntryDialogOpen.value = true
isReadonly.value = false
}, },
}, },
} }
@@ -73,168 +83,98 @@ provide('rec_id', recId)
provide('rec_action', recAction) provide('rec_action', recAction)
provide('rec_item', recItem) provide('rec_item', recItem)
provide('table_data_loader', isLoading) provide('table_data_loader', isLoading)
// #endregion
// #region Functions const getCurrentSpecialistDetail = async (id: number | string) => {
async function fetchSpecialistData(params: any) { const result = await getSpecialistDetail(id)
const endpoint = transform('/api/v1/patient', params) if (result.success) {
return await xfetch(endpoint) const currentValue = result.body?.data || {}
} recItem.value = currentValue
isFormEntryDialogOpen.value = true
async function handleDeleteRow(record: any) {
try {
// TODO : hit backend request untuk delete
console.log('Deleting record:', record)
// Simulate API call
// const response = await xfetch(`/api/v1/Installation/${record.id}`, {
// method: 'DELETE'
// })
// Refresh data setelah berhasil delete
await getSpecialistList()
// TODO: Show success message
console.log('Record deleted successfully')
} catch (error) {
console.error('Error deleting record:', error)
// TODO: Show error message
} finally {
// Reset record state
recId.value = 0
recAction.value = ''
recItem.value = null
} }
} }
// Handle confirmation result const getUnitList = async () => {
function handleConfirmDelete(record: any, action: string) { const result = await getUnits()
console.log('Confirmed action:', action, 'for record:', record) if (result.success) {
handleDeleteRow(record) const currentUnits = result.body?.data || []
} units.value = currentUnits.map((item: Unit) => ({ value: item.id ? Number(item.id) : item.code, label: item.name }))
function handleCancelConfirmation() {
// Reset record state when cancelled
recId.value = 0
recAction.value = ''
recItem.value = null
}
// #endregion region
// #region Form event handlers
function onCancelForm(resetForm: () => void) {
isFormEntryDialogOpen.value = false
// Reset installation selection ketika form dibatal
selectedInstallationId.value = ''
setTimeout(() => {
resetForm()
}, 500)
}
function onInstallationChanged(installationId: string) {
// Update local state untuk trigger re-render filtered units
selectedInstallationId.value = installationId
// The filteredUnitConf computed will automatically update
// based on the new selectedInstallationId value
}
async function onSubmitForm(values: any, resetForm: () => void) {
let isSuccess = false
try {
// TODO: Implement form submission logic
console.log('Form submitted:', values)
// Simulate API call
// const response = await xfetch('/api/v1/Installation', {
// method: 'POST',
// body: JSON.stringify(values)
// })
// If successful, mark as success and close dialog
isFormEntryDialogOpen.value = false
isSuccess = true
// Reset installation selection ketika form berhasil submit
selectedInstallationId.value = ''
// Refresh data after successful submission
await getSpecialistList()
// TODO: Show success message
console.log('Installation created successfully')
} catch (error: unknown) {
console.warn('Error submitting form:', error)
isSuccess = false
// Don't close dialog or reset form on error
// TODO: Show error message to user
} finally {
if (isSuccess) {
setTimeout(() => {
resetForm()
}, 500)
}
} }
} }
// #endregion
// #region Watchers // Watch for row actions when recId or recAction changes
watch([recId, recAction], () => {
// Watch for row actions
watch(recId, () => {
switch (recAction.value) { switch (recAction.value) {
case ActionEvents.showDetail:
getCurrentSpecialistDetail(recId.value)
title.value = 'Detail Spesialis'
isReadonly.value = true
break
case ActionEvents.showEdit: case ActionEvents.showEdit:
// TODO: Handle edit action getCurrentSpecialistDetail(recId.value)
// isFormEntryDialogOpen.value = true title.value = 'Edit Spesialis'
isReadonly.value = false
break break
case ActionEvents.showConfirmDelete: case ActionEvents.showConfirmDelete:
// Trigger confirmation modal open
isRecordConfirmationOpen.value = true isRecordConfirmationOpen.value = true
break break
} }
}) })
// Note: Installation change logic is now handled by the entry-form component onMounted(async () => {
// through the onInstallationChanged event handler await getUnitList()
// #endregion await getSpecialistList()
})
</script> </script>
<template> <template>
<Header v-model="searchInput" :prep="headerPrep" @search="handleSearch" /> <Header
v-model="searchInput"
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
@search="handleSearch"
class="mb-4 xl:mb-5"
/>
<AppSpecialistList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" /> <AppSpecialistList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
<Dialog v-model:open="isFormEntryDialogOpen" :title="headerPrep.addNav?.label!" size="lg" prevent-outside> <Dialog
v-model:open="isFormEntryDialogOpen"
:title="!!recItem ? title : 'Tambah Spesialis'"
size="lg"
prevent-outside
>
<AppSpecialistEntryForm <AppSpecialistEntryForm
:installation="installationConf" :schema="SpecialistSchema"
:unit="filteredUnitConf" :units="units"
:schema="schemaConf" :values="recItem"
:disabled-unit="!selectedInstallationId" :is-loading="isProcessing"
:initial-values="{ name: '', code: '', installationId: '', unitId: '' }" :is-readonly="isReadonly"
@submit="onSubmitForm" @submit="
@cancel="onCancelForm" (values: SpecialistFormData | Record<string, any>, resetForm: () => void) => {
@installation-changed="onInstallationChanged" if (recId > 0) {
handleActionEdit(recId, values, getSpecialistList, resetForm, toast)
return
}
handleActionSave(values, getSpecialistList, resetForm, toast)
}
"
@cancel="handleCancelForm"
/> />
</Dialog> </Dialog>
<!-- Record Confirmation Modal -->
<RecordConfirmation <RecordConfirmation
v-model:open="isRecordConfirmationOpen" v-model:open="isRecordConfirmationOpen"
action="delete" action="delete"
:record="recItem" :record="recItem"
@confirm="handleConfirmDelete" @confirm="() => handleActionRemove(recId, getSpecialistList, toast)"
@cancel="handleCancelConfirmation" @cancel=""
> >
<template #default="{ record }"> <template #default="{ record }">
<div class="text-sm"> <div class="text-sm">
<p><strong>ID:</strong> {{ record?.id }}</p> <p><strong>ID:</strong> {{ record?.id }}</p>
<p v-if="record?.firstName"><strong>Nama:</strong> {{ record.firstName }}</p> <p v-if="record?.name"><strong>Nama:</strong> {{ record.name }}</p>
<p v-if="record?.code"><strong>Kode:</strong> {{ record.cellphone }}</p> <p v-if="record?.code"><strong>Kode:</strong> {{ record.code }}</p>
</div> </div>
</template> </template>
</RecordConfirmation> </RecordConfirmation>
</template> </template>
<style scoped>
/* component style */
</style>
@@ -0,0 +1,239 @@
<script setup lang="ts">
import type { HeaderPrep } from '~/components/pub/custom-ui/data/types'
import AppSubspecialistEntryForm from '~/components/app/subspecialist/entry-form.vue'
import Dialog from '~/components/pub/base/modal/dialog.vue'
import RecordConfirmation from '~/components/pub/custom-ui/confirmation/record-confirmation.vue'
import { ActionEvents } from '~/components/pub/custom-ui/data/types'
import Header from '~/components/pub/custom-ui/nav-header/header.vue'
import { usePaginatedList } from '~/composables/usePaginatedList'
import { createFilteredUnitConf, installationConf, schemaConf, specialistConf, unitConf } from './entry'
// #region State & Computed
// Dialog state
const isFormEntryDialogOpen = ref(false)
const isRecordConfirmationOpen = ref(false)
// Table action rowId provider
const recId = ref<number>(0)
const recAction = ref<string>('')
const recItem = ref<any>(null)
// State untuk tracking installation, unit, dan specialist yang dipilih di form
const selectedInstallationId = ref<string>('')
const selectedUnitId = ref<string>('')
// Computed untuk filtered unit berdasarkan installation yang dipilih
const filteredUnitConf = computed(() => {
if (!selectedInstallationId.value) {
return { ...unitConf, items: [] }
}
return createFilteredUnitConf(selectedInstallationId.value)
})
const {
data,
isLoading,
paginationMeta,
searchInput,
handlePageChange,
handleSearch,
fetchData: getSubSpecialistList,
} = usePaginatedList({
fetchFn: fetchSubSpecialistData,
entityName: 'subspecialist',
})
const headerPrep: HeaderPrep = {
title: 'Sub Specialist',
icon: 'i-lucide-box',
refSearchNav: {
placeholder: 'Cari (min. 3 karakter)...',
minLength: 3,
debounceMs: 500,
showValidationFeedback: true,
onInput: (_val: string) => {
// Handle search input - this will be triggered by the header component
},
onClick: () => {
// Handle search button click if needed
},
onClear: () => {
// Handle search clear
},
},
addNav: {
label: 'Tambah Sub Specialist',
icon: 'i-lucide-send',
onClick: () => {
isFormEntryDialogOpen.value = true
},
},
}
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
// #endregion
// #region Functions
async function fetchSubSpecialistData(params: any) {
const endpoint = transform('/api/v1/patient', params)
return await xfetch(endpoint)
}
async function handleDeleteRow(record: any) {
try {
// TODO : hit backend request untuk delete
console.log('Deleting record:', record)
// Simulate API call
// const response = await xfetch(`/api/v1/subspecialist/${record.id}`, {
// method: 'DELETE'
// })
// Refresh data setelah berhasil delete
await getSubSpecialistList()
// TODO: Show success message
console.log('Record deleted successfully')
} catch (error) {
console.error('Error deleting record:', error)
// TODO: Show error message
} finally {
// Reset record state
recId.value = 0
recAction.value = ''
recItem.value = null
}
}
// Handle confirmation result
function handleConfirmDelete(record: any, action: string) {
console.log('Confirmed action:', action, 'for record:', record)
handleDeleteRow(record)
}
function handleCancelConfirmation() {
// Reset record state when cancelled
recId.value = 0
recAction.value = ''
recItem.value = null
}
// #endregion region
// #region Form event handlers
function onCancelForm(resetForm: () => void) {
isFormEntryDialogOpen.value = false
selectedInstallationId.value = ''
selectedUnitId.value = ''
setTimeout(() => {
resetForm()
}, 500)
}
function onInstallationChanged(installationId: string) {
selectedInstallationId.value = installationId
}
function onUnitChanged(unitId: string) {
selectedUnitId.value = unitId
}
async function onSubmitForm(values: any, resetForm: () => void) {
let isSuccess = false
try {
// TODO: Implement form submission logic
console.log('Form submitted:', values)
// Simulate API call
// const response = await xfetch('/api/v1/subspecialist', {
// method: 'POST',
// body: JSON.stringify(values)
// })
// If successful, mark as success and close dialog
isFormEntryDialogOpen.value = false
isSuccess = true
// Reset selection ketika form berhasil submit
selectedInstallationId.value = ''
selectedUnitId.value = ''
// Refresh data after successful submission
await getSubSpecialistList()
// TODO: Show success message
console.log('Sub Specialist created successfully')
} catch (error: unknown) {
console.warn('Error submitting form:', error)
isSuccess = false
// Don't close dialog or reset form on error
// TODO: Show error message to user
} finally {
if (isSuccess) {
setTimeout(() => {
resetForm()
}, 500)
}
}
}
// #endregion
// #region Watchers
// Watch for row actions
watch(recId, () => {
switch (recAction.value) {
case ActionEvents.showEdit:
// TODO: Handle edit action
// isFormEntryDialogOpen.value = true
break
case ActionEvents.showConfirmDelete:
// Trigger confirmation modal open
isRecordConfirmationOpen.value = true
break
}
})
// #endregion
</script>
<template>
<Header v-model="searchInput" :prep="headerPrep" @search="handleSearch" />
<AppSubspecialistList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
<Dialog v-model:open="isFormEntryDialogOpen" :title="headerPrep.addNav?.label!" size="lg" prevent-outside>
<AppSubspecialistEntryFormPrev
:installation="installationConf"
:unit="filteredUnitConf"
:specialist="specialistConf"
:schema="schemaConf"
:initial-values="{ name: '', code: '', installationId: '', unitId: '', specialistId: '' }"
@submit="onSubmitForm"
@cancel="onCancelForm"
@installation-changed="onInstallationChanged"
@unit-changed="onUnitChanged"
/>
</Dialog>
<RecordConfirmation
v-model:open="isRecordConfirmationOpen"
action="delete"
:record="recItem"
@confirm="handleConfirmDelete"
@cancel="handleCancelConfirmation"
>
<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 }}</p>
<p v-if="record?.specialist"><strong>Specialist:</strong> {{ record.specialist?.name }}</p>
</div>
</template>
</RecordConfirmation>
</template>
+105 -161
View File
@@ -1,34 +1,41 @@
<script setup lang="ts"> <script setup lang="ts">
import type { HeaderPrep } from '~/components/pub/custom-ui/data/types' // Components
import AppSubspecialistEntryForm from '~/components/app/subspecialist/entry-form.vue'
import Dialog from '~/components/pub/base/modal/dialog.vue' import Dialog from '~/components/pub/base/modal/dialog.vue'
import Header from '~/components/pub/custom-ui/nav-header/prep.vue'
import RecordConfirmation from '~/components/pub/custom-ui/confirmation/record-confirmation.vue' import RecordConfirmation from '~/components/pub/custom-ui/confirmation/record-confirmation.vue'
import { ActionEvents } from '~/components/pub/custom-ui/data/types' import AppSubSpecialistList from '~/components/app/subspecialist/list.vue'
import Header from '~/components/pub/custom-ui/nav-header/header.vue' import AppSubSpecialistEntryForm from '~/components/app/subspecialist/entry-form.vue'
// Helpers
import { usePaginatedList } from '~/composables/usePaginatedList' import { usePaginatedList } from '~/composables/usePaginatedList'
import { createFilteredUnitConf, installationConf, schemaConf, specialistConf, unitConf } from './entry' import { toast } from '~/components/pub/ui/toast'
// #region State & Computed // Types
// Dialog state import { ActionEvents, type HeaderPrep } from '~/components/pub/custom-ui/data/types'
const isFormEntryDialogOpen = ref(false) import { SubspecialistSchema, type SubspecialistFormData } from '~/schemas/subspecialist.schema'
const isRecordConfirmationOpen = ref(false) import type { Specialist } from '~/models/specialist'
// Table action rowId provider // Handlers
const recId = ref<number>(0) import {
const recAction = ref<string>('') recId,
const recItem = ref<any>(null) recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} from '~/handlers/subspecialist.handler'
// State untuk tracking installation, unit, dan specialist yang dipilih di form // Services
const selectedInstallationId = ref<string>('') import { getSubspecialists, getSubspecialistDetail } from '~/services/subspecialist.service'
const selectedUnitId = ref<string>('') import { getSpecialists } from '~/services/specialist.service'
// Computed untuk filtered unit berdasarkan installation yang dipilih const specialists = ref<{ value: string; label: string }[]>([])
const filteredUnitConf = computed(() => { const title = ref('')
if (!selectedInstallationId.value) {
return { ...unitConf, items: [] }
}
return createFilteredUnitConf(selectedInstallationId.value)
})
const { const {
data, data,
@@ -39,33 +46,35 @@ const {
handleSearch, handleSearch,
fetchData: getSubSpecialistList, fetchData: getSubSpecialistList,
} = usePaginatedList({ } = usePaginatedList({
fetchFn: fetchSubSpecialistData, fetchFn: async ({ page, search }) => {
const result = await getSubspecialists({ search, page })
return { success: result.success || false, body: result.body || {} }
},
entityName: 'subspecialist', entityName: 'subspecialist',
}) })
const headerPrep: HeaderPrep = { const headerPrep: HeaderPrep = {
title: 'Sub Specialist', title: 'SubSpecialist',
icon: 'i-lucide-box', icon: 'i-lucide-box',
refSearchNav: { refSearchNav: {
placeholder: 'Cari (min. 3 karakter)...', placeholder: 'Cari (min. 3 karakter)...',
minLength: 3, minLength: 3,
debounceMs: 500, debounceMs: 500,
showValidationFeedback: true, showValidationFeedback: true,
onInput: (_val: string) => { onInput: (value: string) => {
// Handle search input - this will be triggered by the header component searchInput.value = value
},
onClick: () => {
// Handle search button click if needed
},
onClear: () => {
// Handle search clear
}, },
onClick: () => {},
onClear: () => {},
}, },
addNav: { addNav: {
label: 'Tambah Sub Specialist', label: 'Tambah',
icon: 'i-lucide-send', icon: 'i-lucide-plus',
onClick: () => { onClick: () => {
recItem.value = null
recId.value = 0
isFormEntryDialogOpen.value = true isFormEntryDialogOpen.value = true
isReadonly.value = false
}, },
}, },
} }
@@ -74,165 +83,100 @@ provide('rec_id', recId)
provide('rec_action', recAction) provide('rec_action', recAction)
provide('rec_item', recItem) provide('rec_item', recItem)
provide('table_data_loader', isLoading) provide('table_data_loader', isLoading)
// #endregion
// #region Functions const getCurrentSubSpecialistDetail = async (id: number | string) => {
async function fetchSubSpecialistData(params: any) { const result = await getSubspecialistDetail(id)
const endpoint = transform('/api/v1/patient', params) if (result.success) {
return await xfetch(endpoint) const currentValue = result.body?.data || {}
} recItem.value = currentValue
isFormEntryDialogOpen.value = true
async function handleDeleteRow(record: any) {
try {
// TODO : hit backend request untuk delete
console.log('Deleting record:', record)
// Simulate API call
// const response = await xfetch(`/api/v1/subspecialist/${record.id}`, {
// method: 'DELETE'
// })
// Refresh data setelah berhasil delete
await getSubSpecialistList()
// TODO: Show success message
console.log('Record deleted successfully')
} catch (error) {
console.error('Error deleting record:', error)
// TODO: Show error message
} finally {
// Reset record state
recId.value = 0
recAction.value = ''
recItem.value = null
} }
} }
// Handle confirmation result const getSpecialistsList = async () => {
function handleConfirmDelete(record: any, action: string) { const result = await getSpecialists()
console.log('Confirmed action:', action, 'for record:', record) if (result.success) {
handleDeleteRow(record) const currentSpecialists = result.body?.data || []
} specialists.value = currentSpecialists.map((item: Specialist) => ({
value: item.id ? Number(item.id) : item.code,
function handleCancelConfirmation() { label: item.name,
// Reset record state when cancelled }))
recId.value = 0
recAction.value = ''
recItem.value = null
}
// #endregion region
// #region Form event handlers
function onCancelForm(resetForm: () => void) {
isFormEntryDialogOpen.value = false
selectedInstallationId.value = ''
selectedUnitId.value = ''
setTimeout(() => {
resetForm()
}, 500)
}
function onInstallationChanged(installationId: string) {
selectedInstallationId.value = installationId
}
function onUnitChanged(unitId: string) {
selectedUnitId.value = unitId
}
async function onSubmitForm(values: any, resetForm: () => void) {
let isSuccess = false
try {
// TODO: Implement form submission logic
console.log('Form submitted:', values)
// Simulate API call
// const response = await xfetch('/api/v1/subspecialist', {
// method: 'POST',
// body: JSON.stringify(values)
// })
// If successful, mark as success and close dialog
isFormEntryDialogOpen.value = false
isSuccess = true
// Reset selection ketika form berhasil submit
selectedInstallationId.value = ''
selectedUnitId.value = ''
// Refresh data after successful submission
await getSubSpecialistList()
// TODO: Show success message
console.log('Sub Specialist created successfully')
} catch (error: unknown) {
console.warn('Error submitting form:', error)
isSuccess = false
// Don't close dialog or reset form on error
// TODO: Show error message to user
} finally {
if (isSuccess) {
setTimeout(() => {
resetForm()
}, 500)
}
} }
} }
// #endregion
// #region Watchers // Watch for row actions when recId or recAction changes
watch([recId, recAction], () => {
// Watch for row actions
watch(recId, () => {
switch (recAction.value) { switch (recAction.value) {
case ActionEvents.showDetail:
getCurrentSubSpecialistDetail(recId.value)
title.value = 'Detail Sub Spesialis'
isReadonly.value = true
break
case ActionEvents.showEdit: case ActionEvents.showEdit:
// TODO: Handle edit action getCurrentSubSpecialistDetail(recId.value)
// isFormEntryDialogOpen.value = true title.value = 'Edit Sub Spesialis'
isReadonly.value = false
break break
case ActionEvents.showConfirmDelete: case ActionEvents.showConfirmDelete:
// Trigger confirmation modal open
isRecordConfirmationOpen.value = true isRecordConfirmationOpen.value = true
break break
} }
}) })
// #endregion onMounted(async () => {
await getSpecialistsList()
await getSubSpecialistList()
})
</script> </script>
<template> <template>
<Header v-model="searchInput" :prep="headerPrep" @search="handleSearch" /> <Header
<AppSubspecialistList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" /> v-model="searchInput"
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
@search="handleSearch"
class="mb-4 xl:mb-5"
/>
<AppSubSpecialistList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
<Dialog v-model:open="isFormEntryDialogOpen" :title="headerPrep.addNav?.label!" size="lg" prevent-outside> <Dialog
<AppSubspecialistEntryForm v-model:open="isFormEntryDialogOpen"
:installation="installationConf" :title="!!recItem ? title : 'Tambah Sub Spesialis'"
:unit="filteredUnitConf" size="lg"
:specialist="specialistConf" prevent-outside
:schema="schemaConf" >
:initial-values="{ name: '', code: '', installationId: '', unitId: '', specialistId: '' }" <AppSubSpecialistEntryForm
@submit="onSubmitForm" :schema="SubspecialistSchema"
@cancel="onCancelForm" :specialists="specialists"
@installation-changed="onInstallationChanged" :values="recItem"
@unit-changed="onUnitChanged" :is-loading="isProcessing"
:is-readonly="isReadonly"
@submit="
(values: SubspecialistFormData | Record<string, any>, resetForm: () => void) => {
if (recId > 0) {
handleActionEdit(recId, values, getSubSpecialistList, resetForm, toast)
return
}
handleActionSave(values, getSubSpecialistList, resetForm, toast)
}
"
@cancel="handleCancelForm"
/> />
</Dialog> </Dialog>
<!-- Record Confirmation Modal -->
<RecordConfirmation <RecordConfirmation
v-model:open="isRecordConfirmationOpen" v-model:open="isRecordConfirmationOpen"
action="delete" action="delete"
:record="recItem" :record="recItem"
@confirm="handleConfirmDelete" @confirm="() => handleActionRemove(recId, getSubSpecialistList, toast)"
@cancel="handleCancelConfirmation" @cancel=""
> >
<template #default="{ record }"> <template #default="{ record }">
<div class="text-sm"> <div class="text-sm">
<p><strong>ID:</strong> {{ record?.id }}</p> <p><strong>ID:</strong> {{ record?.id }}</p>
<p v-if="record?.name"><strong>Nama:</strong> {{ record.name }}</p> <p v-if="record?.name"><strong>Nama:</strong> {{ record.name }}</p>
<p v-if="record?.code"><strong>Kode:</strong> {{ record.code }}</p> <p v-if="record?.code"><strong>Kode:</strong> {{ record.code }}</p>
<p v-if="record?.specialist"><strong>Specialist:</strong> {{ record.specialist?.name }}</p>
</div> </div>
</template> </template>
</RecordConfirmation> </RecordConfirmation>
+11 -4
View File
@@ -2,8 +2,9 @@
// Components // Components
import Dialog from '~/components/pub/base/modal/dialog.vue' import Dialog from '~/components/pub/base/modal/dialog.vue'
import Header from '~/components/pub/custom-ui/nav-header/prep.vue' import Header from '~/components/pub/custom-ui/nav-header/prep.vue'
import AppToolsEntryForm from '~/components/app/tools/entry-form.vue'
import RecordConfirmation from '~/components/pub/custom-ui/confirmation/record-confirmation.vue' import RecordConfirmation from '~/components/pub/custom-ui/confirmation/record-confirmation.vue'
import AppToolsList from '~/components/app/tools/list.vue'
import AppToolsEntryForm from '~/components/app/tools/entry-form.vue'
// Helpers // Helpers
import { usePaginatedList } from '~/composables/usePaginatedList' import { usePaginatedList } from '~/composables/usePaginatedList'
@@ -60,8 +61,8 @@ const headerPrep: HeaderPrep = {
minLength: 3, minLength: 3,
debounceMs: 500, debounceMs: 500,
showValidationFeedback: true, showValidationFeedback: true,
onInput: (_val: string) => { onInput: (value: string) => {
// Handle search input - this will be triggered by the header component searchInput.value = value
}, },
onClick: () => { onClick: () => {
// Handle search button click if needed // Handle search button click if needed
@@ -131,7 +132,13 @@ onMounted(async () => {
</script> </script>
<template> <template>
<Header v-model="searchInput" :prep="headerPrep" @search="handleSearch" class="mb-4 xl:mb-5" /> <Header
v-model="searchInput"
:prep="headerPrep"
@search="handleSearch"
:ref-search-nav="headerPrep.refSearchNav"
class="mb-4 xl:mb-5"
/>
<AppToolsList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" /> <AppToolsList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
<Dialog <Dialog
+207
View File
@@ -0,0 +1,207 @@
<script setup lang="ts">
import type { HeaderPrep } from '~/components/pub/custom-ui/data/types'
import AppUnitEntryForm from '~/components/app/unit/entry-form.vue'
import Dialog from '~/components/pub/base/modal/dialog.vue'
import RecordConfirmation from '~/components/pub/custom-ui/confirmation/record-confirmation.vue'
import { ActionEvents } from '~/components/pub/custom-ui/data/types'
import Header from '~/components/pub/custom-ui/nav-header/header.vue'
import { usePaginatedList } from '~/composables/usePaginatedList'
import { schemaConf, unitConf } from './entry'
// #region State & Computed
// Dialog state
const isFormEntryDialogOpen = ref(false)
const isRecordConfirmationOpen = ref(false)
// Table action rowId provider
const recId = ref<number>(0)
const recAction = ref<string>('')
const recItem = ref<any>(null)
async function fetchUnitData(params: any) {
const endpoint = transform('/api/v1/patient', params)
return await xfetch(endpoint)
}
// Menggunakan composable untuk pagination
const {
data,
isLoading,
paginationMeta,
searchInput,
handlePageChange,
handleSearch,
fetchData: getUnitList,
} = usePaginatedList({
fetchFn: fetchUnitData,
entityName: 'unit',
})
const headerPrep: HeaderPrep = {
title: 'Unit',
icon: 'i-lucide-box',
refSearchNav: {
placeholder: 'Cari (min. 3 karakter)...',
minLength: 3,
debounceMs: 500,
showValidationFeedback: true,
onInput: (_val: string) => {
// Handle search input - this will be triggered by the header component
},
onClick: () => {
// Handle search button click if needed
},
onClear: () => {
// Handle search clear
},
},
addNav: {
label: 'Tambah Unit',
icon: 'i-lucide-send',
onClick: () => {
isFormEntryDialogOpen.value = true
},
},
}
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
// #endregion
// #region Functions
async function handleDeleteRow(record: any) {
try {
// TODO : hit backend request untuk delete
console.log('Deleting record:', record)
// Simulate API call
// const response = await xfetch(`/api/v1/unit/${record.id}`, {
// method: 'DELETE'
// })
// Refresh data setelah berhasil delete
await getUnitList()
// TODO: Show success message
console.log('Record deleted successfully')
} catch (error) {
console.error('Error deleting record:', error)
// TODO: Show error message
} finally {
// Reset record state
recId.value = 0
recAction.value = ''
recItem.value = null
}
}
// #endregion region
// #region Form event handlers
function onCancelForm(resetForm: () => void) {
isFormEntryDialogOpen.value = false
setTimeout(() => {
resetForm()
}, 500)
}
async function onSubmitForm(values: any, resetForm: () => void) {
let isSuccess = false
try {
// TODO: Implement form submission logic
console.log('Form submitted:', values)
// Simulate API call
// const response = await xfetch('/api/v1/unit', {
// method: 'POST',
// body: JSON.stringify(values)
// })
// If successful, mark as success and close dialog
isFormEntryDialogOpen.value = false
isSuccess = true
// Refresh data after successful submission
await getUnitList()
// TODO: Show success message
console.log('Unit created successfully')
} catch (error: unknown) {
console.warn('Error submitting form:', error)
isSuccess = false
// Don't close dialog or reset form on error
// TODO: Show error message to user
} finally {
if (isSuccess) {
setTimeout(() => {
resetForm()
}, 500)
}
}
}
// #endregion
// #region Watchers
// Watch for row actions
watch(recId, () => {
switch (recAction.value) {
case ActionEvents.showEdit:
// TODO: Handle edit action
// isFormEntryDialogOpen.value = true
break
case ActionEvents.showConfirmDelete:
// Trigger confirmation modal open
isRecordConfirmationOpen.value = true
break
}
})
// Handle confirmation result
function handleConfirmDelete(record: any, action: string) {
console.log('Confirmed action:', action, 'for record:', record)
handleDeleteRow(record)
}
function handleCancelConfirmation() {
// Reset record state when cancelled
recId.value = 0
recAction.value = ''
recItem.value = null
}
// #endregion
</script>
<template>
<Header v-model="searchInput" :prep="headerPrep" @search="handleSearch" />
<AppUnitList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
<Dialog v-model:open="isFormEntryDialogOpen" title="Tambah Unit" size="lg" prevent-outside>
<AppUnitEntryFormPrev
:unit="unitConf" :schema="schemaConf" :initial-values="{ name: '', code: '', parentId: '' }"
@submit="onSubmitForm" @cancel="onCancelForm"
/>
</Dialog>
<!-- Record Confirmation Modal -->
<RecordConfirmation
v-model:open="isRecordConfirmationOpen" action="delete" :record="recItem"
@confirm="handleConfirmDelete" @cancel="handleCancelConfirmation"
>
<template #default="{ record }">
<div class="text-sm">
<p><strong>ID:</strong> {{ record?.id }}</p>
<p v-if="record?.firstName"><strong>Nama:</strong> {{ record.firstName }}</p>
<p v-if="record?.code"><strong>Kode:</strong> {{ record.cellphone }}</p>
</div>
</template>
</RecordConfirmation>
</template>
<style scoped>
/* component style */
</style>
+91 -136
View File
@@ -1,29 +1,39 @@
<script setup lang="ts"> <script setup lang="ts">
import type { HeaderPrep } from '~/components/pub/custom-ui/data/types' // Components
import AppUnitEntryForm from '~/components/app/unit/entry-form.vue'
import Dialog from '~/components/pub/base/modal/dialog.vue' import Dialog from '~/components/pub/base/modal/dialog.vue'
import Header from '~/components/pub/custom-ui/nav-header/prep.vue'
import RecordConfirmation from '~/components/pub/custom-ui/confirmation/record-confirmation.vue' import RecordConfirmation from '~/components/pub/custom-ui/confirmation/record-confirmation.vue'
import { ActionEvents } from '~/components/pub/custom-ui/data/types' import AppUnitList from '~/components/app/unit/list.vue'
import Header from '~/components/pub/custom-ui/nav-header/header.vue' import AppUnitEntryForm from '~/components/app/unit/entry-form.vue'
// Helpers
import { usePaginatedList } from '~/composables/usePaginatedList' import { usePaginatedList } from '~/composables/usePaginatedList'
import { schemaConf, unitConf } from './entry' import { toast } from '~/components/pub/ui/toast'
// #region State & Computed // Types
// Dialog state import { ActionEvents, type HeaderPrep } from '~/components/pub/custom-ui/data/types'
const isFormEntryDialogOpen = ref(false) import { UnitSchema, type UnitFormData } from '~/schemas/unit.schema'
const isRecordConfirmationOpen = ref(false)
// Table action rowId provider // Handlers
const recId = ref<number>(0) import {
const recAction = ref<string>('') recId,
const recItem = ref<any>(null) recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} from '~/handlers/unit.handler'
async function fetchUnitData(params: any) { // Services
const endpoint = transform('/api/v1/patient', params) import { getUnits, getUnitDetail } from '~/services/unit.service'
return await xfetch(endpoint)
} const title = ref('')
// Menggunakan composable untuk pagination
const { const {
data, data,
isLoading, isLoading,
@@ -33,7 +43,10 @@ const {
handleSearch, handleSearch,
fetchData: getUnitList, fetchData: getUnitList,
} = usePaginatedList({ } = usePaginatedList({
fetchFn: fetchUnitData, fetchFn: async ({ page, search }) => {
const result = await getUnits({ search, page })
return { success: result.success || false, body: result.body || {} }
},
entityName: 'unit', entityName: 'unit',
}) })
@@ -45,21 +58,20 @@ const headerPrep: HeaderPrep = {
minLength: 3, minLength: 3,
debounceMs: 500, debounceMs: 500,
showValidationFeedback: true, showValidationFeedback: true,
onInput: (_val: string) => { onInput: (value: string) => {
// Handle search input - this will be triggered by the header component searchInput.value = value
},
onClick: () => {
// Handle search button click if needed
},
onClear: () => {
// Handle search clear
}, },
onClick: () => {},
onClear: () => {},
}, },
addNav: { addNav: {
label: 'Tambah Unit', label: 'Tambah',
icon: 'i-lucide-send', icon: 'i-lucide-plus',
onClick: () => { onClick: () => {
recItem.value = null
recId.value = 0
isFormEntryDialogOpen.value = true isFormEntryDialogOpen.value = true
isReadonly.value = false
}, },
}, },
} }
@@ -68,140 +80,83 @@ provide('rec_id', recId)
provide('rec_action', recAction) provide('rec_action', recAction)
provide('rec_item', recItem) provide('rec_item', recItem)
provide('table_data_loader', isLoading) provide('table_data_loader', isLoading)
// #endregion
// #region Functions const getCurrentUnitDetail = async (id: number | string) => {
const result = await getUnitDetail(id)
async function handleDeleteRow(record: any) { if (result.success) {
try { const currentValue = result.body?.data || {}
// TODO : hit backend request untuk delete recItem.value = currentValue
console.log('Deleting record:', record) isFormEntryDialogOpen.value = true
// Simulate API call
// const response = await xfetch(`/api/v1/unit/${record.id}`, {
// method: 'DELETE'
// })
// Refresh data setelah berhasil delete
await getUnitList()
// TODO: Show success message
console.log('Record deleted successfully')
} catch (error) {
console.error('Error deleting record:', error)
// TODO: Show error message
} finally {
// Reset record state
recId.value = 0
recAction.value = ''
recItem.value = null
} }
} }
// #endregion region // Watch for row actions when recId or recAction changes
watch([recId, recAction], () => {
// #region Form event handlers
function onCancelForm(resetForm: () => void) {
isFormEntryDialogOpen.value = false
setTimeout(() => {
resetForm()
}, 500)
}
async function onSubmitForm(values: any, resetForm: () => void) {
let isSuccess = false
try {
// TODO: Implement form submission logic
console.log('Form submitted:', values)
// Simulate API call
// const response = await xfetch('/api/v1/unit', {
// method: 'POST',
// body: JSON.stringify(values)
// })
// If successful, mark as success and close dialog
isFormEntryDialogOpen.value = false
isSuccess = true
// Refresh data after successful submission
await getUnitList()
// TODO: Show success message
console.log('Unit created successfully')
} catch (error: unknown) {
console.warn('Error submitting form:', error)
isSuccess = false
// Don't close dialog or reset form on error
// TODO: Show error message to user
} finally {
if (isSuccess) {
setTimeout(() => {
resetForm()
}, 500)
}
}
}
// #endregion
// #region Watchers
// Watch for row actions
watch(recId, () => {
switch (recAction.value) { switch (recAction.value) {
case ActionEvents.showDetail:
getCurrentUnitDetail(recId.value)
title.value = 'Detail Unit'
isReadonly.value = true
break
case ActionEvents.showEdit: case ActionEvents.showEdit:
// TODO: Handle edit action getCurrentUnitDetail(recId.value)
// isFormEntryDialogOpen.value = true title.value = 'Edit Unit'
isReadonly.value = false
break break
case ActionEvents.showConfirmDelete: case ActionEvents.showConfirmDelete:
// Trigger confirmation modal open
isRecordConfirmationOpen.value = true isRecordConfirmationOpen.value = true
break break
} }
}) })
// Handle confirmation result onMounted(async () => {
function handleConfirmDelete(record: any, action: string) { await getUnitList()
console.log('Confirmed action:', action, 'for record:', record) })
handleDeleteRow(record)
}
function handleCancelConfirmation() {
// Reset record state when cancelled
recId.value = 0
recAction.value = ''
recItem.value = null
}
// #endregion
</script> </script>
<template> <template>
<Header v-model="searchInput" :prep="headerPrep" @search="handleSearch" /> <Header
v-model="searchInput"
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
@search="handleSearch"
class="mb-4 xl:mb-5"
/>
<AppUnitList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" /> <AppUnitList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
<Dialog v-model:open="isFormEntryDialogOpen" title="Tambah Unit" size="lg" prevent-outside> <Dialog v-model:open="isFormEntryDialogOpen" :title="!!recItem ? title : 'Tambah Unit'" size="lg" prevent-outside>
<AppUnitEntryForm <AppUnitEntryForm
:unit="unitConf" :schema="schemaConf" :initial-values="{ name: '', code: '', parentId: '' }" :schema="UnitSchema"
@submit="onSubmitForm" @cancel="onCancelForm" :values="recItem"
/> :is-loading="isProcessing"
:is-readonly="isReadonly"
@submit="
(values: UnitFormData | Record<string, any>, resetForm: () => void) => {
if (recId > 0) {
handleActionEdit(recId, values, getUnitList, resetForm, toast)
return
}
handleActionSave(values, getUnitList, resetForm, toast)
}
"
@cancel="handleCancelForm"
/>
</Dialog> </Dialog>
<!-- Record Confirmation Modal --> <!-- Record Confirmation Modal -->
<RecordConfirmation <RecordConfirmation
v-model:open="isRecordConfirmationOpen" action="delete" :record="recItem" v-model:open="isRecordConfirmationOpen"
@confirm="handleConfirmDelete" @cancel="handleCancelConfirmation" action="delete"
> :record="recItem"
@confirm="() => handleActionRemove(recId, getUnitList, toast)"
@cancel=""
>
<template #default="{ record }"> <template #default="{ record }">
<div class="text-sm"> <div class="text-sm">
<p><strong>ID:</strong> {{ record?.id }}</p> <p><strong>ID:</strong> {{ record?.id }}</p>
<p v-if="record?.firstName"><strong>Nama:</strong> {{ record.firstName }}</p> <p v-if="record?.name"><strong>Nama:</strong> {{ record.name }}</p>
<p v-if="record?.code"><strong>Kode:</strong> {{ record.cellphone }}</p> <p v-if="record?.code"><strong>Kode:</strong> {{ record.code }}</p>
</div> </div>
</template> </template>
</RecordConfirmation> </RecordConfirmation>
</template> </template>
<style scoped>
/* component style */
</style>
+12 -9
View File
@@ -2,8 +2,8 @@
// Components // Components
import Dialog from '~/components/pub/base/modal/dialog.vue' import Dialog from '~/components/pub/base/modal/dialog.vue'
import Header from '~/components/pub/custom-ui/nav-header/prep.vue' import Header from '~/components/pub/custom-ui/nav-header/prep.vue'
import AppUomEntryForm from '~/components/app/uom/entry-form.vue'
import RecordConfirmation from '~/components/pub/custom-ui/confirmation/record-confirmation.vue' import RecordConfirmation from '~/components/pub/custom-ui/confirmation/record-confirmation.vue'
import AppUomEntryForm from '~/components/app/uom/entry-form.vue'
// Helpers // Helpers
import { usePaginatedList } from '~/composables/usePaginatedList' import { usePaginatedList } from '~/composables/usePaginatedList'
@@ -57,7 +57,9 @@ const headerPrep: HeaderPrep = {
minLength: 3, minLength: 3,
debounceMs: 500, debounceMs: 500,
showValidationFeedback: true, showValidationFeedback: true,
onInput: (_val: string) => {}, onInput: (value: string) => {
searchInput.value = value
},
onClick: () => {}, onClick: () => {},
onClear: () => {}, onClear: () => {},
}, },
@@ -113,17 +115,18 @@ onMounted(async () => {
<template> <template>
<div class="p-4"> <div class="p-4">
<Header v-model="searchInput" :prep="headerPrep" @search="handleSearch" /> <Header
v-model="searchInput"
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
@search="handleSearch"
class="mb-4 xl:mb-5"
/>
<div class="rounded-md border p-4"> <div class="rounded-md border p-4">
<AppUomList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" /> <AppUomList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
</div> </div>
<Dialog <Dialog v-model:open="isFormEntryDialogOpen" :title="!!recItem ? title : 'Tambah Uom'" size="lg" prevent-outside>
v-model:open="isFormEntryDialogOpen"
:title="!!recItem ? title : 'Tambah Uom'"
size="lg"
prevent-outside
>
<AppUomEntryForm <AppUomEntryForm
:schema="UomSchema" :schema="UomSchema"
:values="recItem" :values="recItem"
+106
View File
@@ -1,3 +1,109 @@
import { ref } from 'vue'
// Factory for CRUD handler state and actions
export function createCrudHandler<T = any>(crud: {
post: (...args: any[]) => Promise<any>
patch: (...args: any[]) => Promise<any>
remove: (...args: any[]) => Promise<any>
}) {
const recId = ref<number>(0)
const recAction = ref<string>('')
const recItem = ref<T | null>(null)
const isReadonly = ref(false)
const isProcessing = ref(false)
const isFormEntryDialogOpen = ref(false)
const isRecordConfirmationOpen = ref(false)
function onResetState() {
recId.value = 0
recAction.value = ''
recItem.value = null
}
async function handleActionSave(values: any, refresh: () => void, reset: () => void, toast: ToastFn) {
isProcessing.value = true
await handleAsyncAction<[any], any>({
action: crud.post,
args: [values],
toast,
successMessage: 'Data berhasil disimpan',
errorMessage: 'Gagal menyimpan data',
onSuccess: () => {
isFormEntryDialogOpen.value = false
if (refresh) refresh()
},
onFinally: (isSuccess: boolean) => {
if (isSuccess) setTimeout(reset, 500)
isProcessing.value = false
},
})
}
async function handleActionEdit(
id: number | string,
values: any,
refresh: () => void,
reset: () => void,
toast: ToastFn,
) {
isProcessing.value = true
await handleAsyncAction<[number | string, any], any>({
action: crud.patch,
args: [id, values],
toast,
successMessage: 'Data berhasil diubah',
errorMessage: 'Gagal mengubah data',
onSuccess: () => {
isFormEntryDialogOpen.value = false
if (refresh) refresh()
},
onFinally: (isSuccess: boolean) => {
if (isSuccess) setTimeout(reset, 500)
isProcessing.value = false
},
})
}
async function handleActionRemove(id: number | string, refresh: () => void, toast: ToastFn) {
isProcessing.value = true
await handleAsyncAction<[number | string], any>({
action: crud.remove,
args: [id],
toast,
successMessage: 'Data berhasil dihapus',
errorMessage: 'Gagal menghapus data',
onSuccess: () => {
isRecordConfirmationOpen.value = false
if (refresh) refresh()
},
onFinally: () => {
isProcessing.value = false
},
})
}
function handleCancelForm(reset: () => void) {
isFormEntryDialogOpen.value = false
isReadonly.value = false
setTimeout(reset, 300)
}
return {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
}
}
// Reusable async handler for CRUD actions with toast and state management // Reusable async handler for CRUD actions with toast and state management
export type ToastFn = (params: { title: string; description: string; variant: 'default' | 'destructive' }) => void export type ToastFn = (params: { title: string; description: string; variant: 'default' | 'destructive' }) => void
+19 -99
View File
@@ -1,101 +1,21 @@
import { ref } from 'vue' import { createCrudHandler } from '~/handlers/_handler'
// Handlers
import { type ToastFn, handleAsyncAction } from '~/handlers/_handler'
// Services
import { postDevice, patchDevice, removeDevice } from '~/services/device.service' import { postDevice, patchDevice, removeDevice } from '~/services/device.service'
const recId = ref<number>(0) export const {
const recAction = ref<string>('') recId,
const recItem = ref<any>(null) recAction,
const isReadonly = ref(false) recItem,
const isProcessing = ref(false) isReadonly,
const isFormEntryDialogOpen = ref(false) isProcessing,
const isRecordConfirmationOpen = ref(false) isFormEntryDialogOpen,
isRecordConfirmationOpen,
function onResetState() { onResetState,
recId.value = 0 handleActionSave,
recAction.value = '' handleActionEdit,
recItem.value = null handleActionRemove,
} handleCancelForm,
} = createCrudHandler({
export async function handleActionSave( post: postDevice,
values: any, patch: patchDevice,
refresh: () => void, remove: removeDevice,
reset: () => void, })
toast: ToastFn
) {
isProcessing.value = true;
await handleAsyncAction<[any], any>({
action: postDevice,
args: [values],
toast,
successMessage: 'Data berhasil disimpan',
errorMessage: 'Gagal menyimpan data',
onSuccess: () => {
isFormEntryDialogOpen.value = false;
if (refresh) refresh();
},
onFinally: (isSuccess: boolean) => {
if (isSuccess) setTimeout(reset, 500);
isProcessing.value = false;
},
});
}
export async function handleActionEdit(
id: number | string,
values: any,
refresh: () => void,
reset: () => void,
toast: ToastFn
) {
isProcessing.value = true;
await handleAsyncAction<[number | string, any], any>({
action: patchDevice,
args: [id, values],
toast,
successMessage: 'Data berhasil diubah',
errorMessage: 'Gagal mengubah data',
onSuccess: () => {
isFormEntryDialogOpen.value = false;
if (refresh) refresh();
},
onFinally: (isSuccess: boolean) => {
if (isSuccess) setTimeout(reset, 500);
isProcessing.value = false;
},
});
}
export async function handleActionRemove(
id: number | string,
refresh: () => void,
toast: ToastFn
) {
isProcessing.value = true;
await handleAsyncAction<[number | string], any>({
action: removeDevice,
args: [id],
toast,
successMessage: 'Data berhasil dihapus',
errorMessage: 'Gagal menghapus data',
onSuccess: () => {
if (refresh) refresh();
},
onFinally: () => {
onResetState();
isProcessing.value = false;
},
});
}
export function handleCancelForm(reset: () => void) {
isFormEntryDialogOpen.value = false
setTimeout(() => {
reset()
}, 500)
}
export { recId, recAction, recItem, isReadonly, isProcessing, isFormEntryDialogOpen, isRecordConfirmationOpen }
+21
View File
@@ -0,0 +1,21 @@
import { createCrudHandler } from '~/handlers/_handler'
import { postDivisionPosition, patchDivisionPosition, removeDivisionPosition } from '~/services/division-position.service'
export const {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} = createCrudHandler({
post: postDivisionPosition,
patch: patchDivisionPosition,
remove: removeDivisionPosition,
})
+21
View File
@@ -0,0 +1,21 @@
import { createCrudHandler } from '~/handlers/_handler'
import { postDivision, patchDivision, removeDivision } from '~/services/division.service'
export const {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} = createCrudHandler({
post: postDivision,
patch: patchDivision,
remove: removeDivision,
})
+21
View File
@@ -0,0 +1,21 @@
import { createCrudHandler } from '~/handlers/_handler'
import { postInstallation, patchInstallation, removeInstallation } from '~/services/installation.service'
export const {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} = createCrudHandler({
post: postInstallation,
patch: patchInstallation,
remove: removeInstallation,
})
+19 -90
View File
@@ -1,92 +1,21 @@
import { ref } from 'vue' import { createCrudHandler } from '~/handlers/_handler'
// Handlers
import { type ToastFn, handleAsyncAction } from '~/handlers/_handler'
// Services
import { postMaterial, patchMaterial, removeMaterial } from '~/services/material.service' import { postMaterial, patchMaterial, removeMaterial } from '~/services/material.service'
const recId = ref<number>(0) export const {
const recAction = ref<string>('') recId,
const recItem = ref<any>(null) recAction,
const isReadonly = ref(false) recItem,
const isProcessing = ref(false) isReadonly,
const isFormEntryDialogOpen = ref(false) isProcessing,
const isRecordConfirmationOpen = ref(false) isFormEntryDialogOpen,
isRecordConfirmationOpen,
function onResetState() { onResetState,
recId.value = 0 handleActionSave,
recAction.value = '' handleActionEdit,
recItem.value = null handleActionRemove,
} handleCancelForm,
} = createCrudHandler({
export async function handleActionSave(values: any, refresh: () => void, reset: () => void, toast: ToastFn) { post: postMaterial,
isProcessing.value = true patch: patchMaterial,
await handleAsyncAction<[any], any>({ remove: removeMaterial,
action: postMaterial, })
args: [values],
toast,
successMessage: 'Data berhasil disimpan',
errorMessage: 'Gagal menyimpan data',
onSuccess: () => {
isFormEntryDialogOpen.value = false
if (refresh) refresh()
},
onFinally: (isSuccess: boolean) => {
if (isSuccess) setTimeout(reset, 500)
isProcessing.value = false
},
})
}
export async function handleActionEdit(
id: number | string,
values: any,
refresh: () => void,
reset: () => void,
toast: ToastFn,
) {
isProcessing.value = true
await handleAsyncAction<[number | string, any], any>({
action: patchMaterial,
args: [id, values],
toast,
successMessage: 'Data berhasil diubah',
errorMessage: 'Gagal mengubah data',
onSuccess: () => {
isFormEntryDialogOpen.value = false
if (refresh) refresh()
},
onFinally: (isSuccess: boolean) => {
if (isSuccess) setTimeout(reset, 500)
isProcessing.value = false
},
})
}
export async function handleActionRemove(id: number | string, refresh: () => void, toast: ToastFn) {
isProcessing.value = true
await handleAsyncAction<[number | string], any>({
action: removeMaterial,
args: [id],
toast,
successMessage: 'Data berhasil dihapus',
errorMessage: 'Gagal menghapus data',
onSuccess: () => {
if (refresh) refresh()
},
onFinally: () => {
onResetState()
isProcessing.value = false
},
})
}
export function handleCancelForm(reset: () => void) {
isFormEntryDialogOpen.value = false
setTimeout(() => {
reset()
}, 500)
}
export { recId, recAction, recItem, isReadonly, isProcessing, isFormEntryDialogOpen, isRecordConfirmationOpen }
+19 -99
View File
@@ -1,101 +1,21 @@
import { ref } from 'vue' import { createCrudHandler } from '~/handlers/_handler'
// Handlers
import { type ToastFn, handleAsyncAction } from '~/handlers/_handler'
// Services
import { postMedicineGroup, patchMedicineGroup, removeMedicineGroup } from '~/services/medicine-group.service' import { postMedicineGroup, patchMedicineGroup, removeMedicineGroup } from '~/services/medicine-group.service'
const recId = ref<number>(0) export const {
const recAction = ref<string>('') recId,
const recItem = ref<any>(null) recAction,
const isReadonly = ref(false) recItem,
const isProcessing = ref(false) isReadonly,
const isFormEntryDialogOpen = ref(false) isProcessing,
const isRecordConfirmationOpen = ref(false) isFormEntryDialogOpen,
isRecordConfirmationOpen,
function onResetState() { onResetState,
recId.value = 0 handleActionSave,
recAction.value = '' handleActionEdit,
recItem.value = null handleActionRemove,
} handleCancelForm,
} = createCrudHandler({
export async function handleActionSave( post: postMedicineGroup,
values: any, patch: patchMedicineGroup,
refresh: () => void, remove: removeMedicineGroup,
reset: () => void, })
toast: ToastFn
) {
isProcessing.value = true;
await handleAsyncAction<[any], any>({
action: postMedicineGroup,
args: [values],
toast,
successMessage: 'Data berhasil disimpan',
errorMessage: 'Gagal menyimpan data',
onSuccess: () => {
isFormEntryDialogOpen.value = false;
if (refresh) refresh();
},
onFinally: (isSuccess: boolean) => {
if (isSuccess) setTimeout(reset, 500);
isProcessing.value = false;
},
});
}
export async function handleActionEdit(
id: number | string,
values: any,
refresh: () => void,
reset: () => void,
toast: ToastFn
) {
isProcessing.value = true;
await handleAsyncAction<[number | string, any], any>({
action: patchMedicineGroup,
args: [id, values],
toast,
successMessage: 'Data berhasil diubah',
errorMessage: 'Gagal mengubah data',
onSuccess: () => {
isFormEntryDialogOpen.value = false;
if (refresh) refresh();
},
onFinally: (isSuccess: boolean) => {
if (isSuccess) setTimeout(reset, 500);
isProcessing.value = false;
},
});
}
export async function handleActionRemove(
id: number | string,
refresh: () => void,
toast: ToastFn
) {
isProcessing.value = true;
await handleAsyncAction<[number | string], any>({
action: removeMedicineGroup,
args: [id],
toast,
successMessage: 'Data berhasil dihapus',
errorMessage: 'Gagal menghapus data',
onSuccess: () => {
if (refresh) refresh();
},
onFinally: () => {
onResetState();
isProcessing.value = false;
},
});
}
export function handleCancelForm(reset: () => void) {
isFormEntryDialogOpen.value = false
setTimeout(() => {
reset()
}, 500)
}
export { recId, recAction, recItem, isReadonly, isProcessing, isFormEntryDialogOpen, isRecordConfirmationOpen }
+19 -99
View File
@@ -1,101 +1,21 @@
import { ref } from 'vue' import { createCrudHandler } from '~/handlers/_handler'
// Handlers
import { type ToastFn, handleAsyncAction } from '~/handlers/_handler'
// Services
import { postMedicineMethod, patchMedicineMethod, removeMedicineMethod } from '~/services/medicine-method.service' import { postMedicineMethod, patchMedicineMethod, removeMedicineMethod } from '~/services/medicine-method.service'
const recId = ref<number>(0) export const {
const recAction = ref<string>('') recId,
const recItem = ref<any>(null) recAction,
const isReadonly = ref(false) recItem,
const isProcessing = ref(false) isReadonly,
const isFormEntryDialogOpen = ref(false) isProcessing,
const isRecordConfirmationOpen = ref(false) isFormEntryDialogOpen,
isRecordConfirmationOpen,
function onResetState() { onResetState,
recId.value = 0 handleActionSave,
recAction.value = '' handleActionEdit,
recItem.value = null handleActionRemove,
} handleCancelForm,
} = createCrudHandler({
export async function handleActionSave( post: postMedicineMethod,
values: any, patch: patchMedicineMethod,
refresh: () => void, remove: removeMedicineMethod,
reset: () => void, })
toast: ToastFn
) {
isProcessing.value = true;
await handleAsyncAction<[any], any>({
action: postMedicineMethod,
args: [values],
toast,
successMessage: 'Data berhasil disimpan',
errorMessage: 'Gagal menyimpan data',
onSuccess: () => {
isFormEntryDialogOpen.value = false;
if (refresh) refresh();
},
onFinally: (isSuccess: boolean) => {
if (isSuccess) setTimeout(reset, 500);
isProcessing.value = false;
},
});
}
export async function handleActionEdit(
id: number | string,
values: any,
refresh: () => void,
reset: () => void,
toast: ToastFn
) {
isProcessing.value = true;
await handleAsyncAction<[number | string, any], any>({
action: patchMedicineMethod,
args: [id, values],
toast,
successMessage: 'Data berhasil diubah',
errorMessage: 'Gagal mengubah data',
onSuccess: () => {
isFormEntryDialogOpen.value = false;
if (refresh) refresh();
},
onFinally: (isSuccess: boolean) => {
if (isSuccess) setTimeout(reset, 500);
isProcessing.value = false;
},
});
}
export async function handleActionRemove(
id: number | string,
refresh: () => void,
toast: ToastFn
) {
isProcessing.value = true;
await handleAsyncAction<[number | string], any>({
action: removeMedicineMethod,
args: [id],
toast,
successMessage: 'Data berhasil dihapus',
errorMessage: 'Gagal menghapus data',
onSuccess: () => {
if (refresh) refresh();
},
onFinally: () => {
onResetState();
isProcessing.value = false;
},
});
}
export function handleCancelForm(reset: () => void) {
isFormEntryDialogOpen.value = false
setTimeout(() => {
reset()
}, 500)
}
export { recId, recAction, recItem, isReadonly, isProcessing, isFormEntryDialogOpen, isRecordConfirmationOpen }
+21
View File
@@ -0,0 +1,21 @@
import { createCrudHandler } from '~/handlers/_handler'
import { postMedicine, patchMedicine, removeMedicine } from '~/services/medicine.service'
export const {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} = createCrudHandler({
post: postMedicine,
patch: patchMedicine,
remove: removeMedicine,
})
+21
View File
@@ -0,0 +1,21 @@
import { createCrudHandler } from '~/handlers/_handler'
import { postSpecialist, patchSpecialist, removeSpecialist } from '~/services/specialist.service'
export const {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} = createCrudHandler({
post: postSpecialist,
patch: patchSpecialist,
remove: removeSpecialist,
})
+21
View File
@@ -0,0 +1,21 @@
import { createCrudHandler } from '~/handlers/_handler'
import { postSubspecialist, patchSubspecialist, removeSubspecialist } from '~/services/subspecialist.service'
export const {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} = createCrudHandler({
post: postSubspecialist,
patch: patchSubspecialist,
remove: removeSubspecialist,
})
+21
View File
@@ -0,0 +1,21 @@
import { createCrudHandler } from '~/handlers/_handler'
import { postUnit, patchUnit, removeUnit } from '~/services/unit.service'
export const {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} = createCrudHandler({
post: postUnit,
patch: patchUnit,
remove: removeUnit,
})
+19 -90
View File
@@ -1,92 +1,21 @@
import { ref } from 'vue' import { createCrudHandler } from '~/handlers/_handler'
// Handlers
import { type ToastFn, handleAsyncAction } from '~/handlers/_handler'
// Services
import { postUom, patchUom, removeUom } from '~/services/uom.service' import { postUom, patchUom, removeUom } from '~/services/uom.service'
const recId = ref<number>(0) export const {
const recAction = ref<string>('') recId,
const recItem = ref<any>(null) recAction,
const isReadonly = ref(false) recItem,
const isProcessing = ref(false) isReadonly,
const isFormEntryDialogOpen = ref(false) isProcessing,
const isRecordConfirmationOpen = ref(false) isFormEntryDialogOpen,
isRecordConfirmationOpen,
function onResetState() { onResetState,
recId.value = 0 handleActionSave,
recAction.value = '' handleActionEdit,
recItem.value = null handleActionRemove,
} handleCancelForm,
} = createCrudHandler({
export async function handleActionSave(values: any, refresh: () => void, reset: () => void, toast: ToastFn) { post: postUom,
isProcessing.value = true patch: patchUom,
await handleAsyncAction<[any], any>({ remove: removeUom,
action: postUom, })
args: [values],
toast,
successMessage: 'Data berhasil disimpan',
errorMessage: 'Gagal menyimpan data',
onSuccess: () => {
isFormEntryDialogOpen.value = false
if (refresh) refresh()
},
onFinally: (isSuccess: boolean) => {
if (isSuccess) setTimeout(reset, 500)
isProcessing.value = false
},
})
}
export async function handleActionEdit(
id: number | string,
values: any,
refresh: () => void,
reset: () => void,
toast: ToastFn,
) {
isProcessing.value = true
await handleAsyncAction<[number | string, any], any>({
action: patchUom,
args: [id, values],
toast,
successMessage: 'Data berhasil diubah',
errorMessage: 'Gagal mengubah data',
onSuccess: () => {
isFormEntryDialogOpen.value = false
if (refresh) refresh()
},
onFinally: (isSuccess: boolean) => {
if (isSuccess) setTimeout(reset, 500)
isProcessing.value = false
},
})
}
export async function handleActionRemove(id: number | string, refresh: () => void, toast: ToastFn) {
isProcessing.value = true
await handleAsyncAction<[number | string], any>({
action: removeUom,
args: [id],
toast,
successMessage: 'Data berhasil dihapus',
errorMessage: 'Gagal menghapus data',
onSuccess: () => {
if (refresh) refresh()
},
onFinally: () => {
onResetState()
isProcessing.value = false
},
})
}
export function handleCancelForm(reset: () => void) {
isFormEntryDialogOpen.value = false
setTimeout(() => {
reset()
}, 500)
}
export { recId, recAction, recItem, isReadonly, isProcessing, isFormEntryDialogOpen, isRecordConfirmationOpen }
+10
View File
@@ -0,0 +1,10 @@
export interface Division {
code: string
name: string
}
export interface DivisionPosition {
code: string
name: string
division_id: number
}
+5
View File
@@ -0,0 +1,5 @@
export interface Installation {
code: string
name: string
encounterClass_code: string
}
+13 -25
View File
@@ -4,30 +4,21 @@ export interface MedicineBase {
} }
export interface Medicine { export interface Medicine {
id: string
name: string
code: string code: string
name: string
medicineGroup_code: string medicineGroup_code: string
medicineMethod_code: string medicineMethod_code: string
uom_code: string uom_code: string
type: string infra_id?: string | null
dose: string stock: number
infra_id: string
stock: string
status: string
} }
export interface CreateDto { export interface CreateMedicineDto extends Medicine {
name: string
code: string }
medicineGroup_code: string
medicineMethod_code: string export interface UpdateMedicineDto extends CreateMedicineDto {
uom_code: string id: string | number
type: string
dose: string
infra_id: string
stock: string
status: string
} }
export interface GetListDto { export interface GetListDto {
@@ -49,7 +40,7 @@ export interface GetDetailDto {
id?: string id?: string
} }
export interface UpdateDto extends CreateDto { export interface UpdateDto extends CreateMedicineDto {
id?: number id?: number
} }
@@ -57,17 +48,14 @@ export interface DeleteDto {
id?: string id?: string
} }
export function genMedicine(): CreateDto { export function genMedicine(): CreateMedicineDto {
return { return {
name: 'name', name: 'name',
code: 'code', code: 'code',
medicineGroup_code: 'medicineGroup_code', medicineGroup_code: 'medicineGroup_code',
medicineMethod_code: 'medicineMethod_code', medicineMethod_code: 'medicineMethod_code',
uom_code: 'uom_code', uom_code: 'uom_code',
type: 'type', infra_id: null,
dose: 'dose', stock: 0
infra_id: 'infra_id',
stock: 'stock',
status: 'status',
} }
} }
+6
View File
@@ -0,0 +1,6 @@
export interface Specialist {
id?: number
code: string
name: string
unit_id: number | string
}
+5
View File
@@ -0,0 +1,5 @@
export interface Subspecialist {
code: string
name: string
specialist_id: number | string
}
+6
View File
@@ -0,0 +1,6 @@
export interface Unit {
id?: number
code: string
name: string
installation?: string | number
}
@@ -7,7 +7,7 @@ definePageMeta({
// middleware: ['rbac'], // middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'], roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Daftar Divisi', title: 'Daftar Divisi',
contentFrame: 'cf-full-width', contentFrame: 'cf-container-lg',
}) })
const route = useRoute() const route = useRoute()
@@ -6,8 +6,8 @@ import Error from '~/components/pub/base/error/error.vue'
definePageMeta({ definePageMeta({
// middleware: ['rbac'], // middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'], roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'List Specialist', title: 'Daftar Specialist',
contentFrame: 'cf-full-width', contentFrame: 'cf-container-lg',
}) })
const route = useRoute() const route = useRoute()
@@ -6,8 +6,8 @@ import Error from '~/components/pub/base/error/error.vue'
definePageMeta({ definePageMeta({
// middleware: ['rbac'], // middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'], roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'List Specialist', title: 'Daftar Subspecialist',
contentFrame: 'cf-full-width', contentFrame: 'cf-container-lg',
}) })
const route = useRoute() const route = useRoute()
+2 -2
View File
@@ -6,8 +6,8 @@ import Error from '~/components/pub/base/error/error.vue'
definePageMeta({ definePageMeta({
// middleware: ['rbac'], // middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'], roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'List Unit', title: 'Daftar Unit',
contentFrame: 'cf-full-width', contentFrame: 'cf-container-lg',
}) })
const route = useRoute() const route = useRoute()
+12
View File
@@ -0,0 +1,12 @@
import { z } from 'zod'
import type { Division, DivisionPosition } from '~/models/division'
const DivisionSchema = 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'),
})
type DivisionFormData = z.infer<typeof DivisionSchema> & (Division | DivisionPosition)
export { DivisionSchema }
export type { DivisionFormData }
+9 -8
View File
@@ -1,12 +1,13 @@
import { z } from 'zod' import { z } from 'zod'
import type { MedicineBase } from '~/models/medicine'
const MedicineBaseSchema = z.object({ export const MedicineSchema = z.object({
code: z.string({ required_error: 'Kode harus diisi' }).min(1, 'Kode minimum 1 karakter'), code: z.string({ required_error: 'Kode harus diisi' }).min(1, 'Kode minimal 1 karakter'),
name: z.string({ required_error: 'Nama harus diisi' }).min(1, 'Nama minimum 1 karakter') name: z.string({ required_error: 'Nama harus diisi' }).min(1, 'Nama minimal 1 karakter'),
medicineGroup_code: z.string({ required_error: 'Kelompok obat harus diisi' }).min(1, 'Kelompok obat harus diisi'),
medicineMethod_code: z.string({ required_error: 'Metode pemberian harus diisi' }).min(1, 'Metode pemberian harus diisi'),
uom_code: z.string({ required_error: 'Satuan harus diisi' }).min(1, 'Satuan harus diisi'),
infra_id: z.number().nullable().optional(),
stock: z.preprocess((val) => Number(val), z.number({ invalid_type_error: 'Stok harus berupa angka' }).min(1, 'Stok harus lebih besar dari 0')),
}) })
type MedicineBaseFormData = z.infer<typeof MedicineBaseSchema> & MedicineBase export type MedicineFormData = z.infer<typeof MedicineSchema>
export { MedicineBaseSchema }
export type { MedicineBaseFormData }
+13
View File
@@ -0,0 +1,13 @@
import { z } from 'zod'
import type { Specialist } from '~/models/specialist'
const SpecialistSchema = 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'),
unit_id: z.number().positive('Unit harus diisi'),
})
type SpecialistFormData = z.infer<typeof SpecialistSchema> & Specialist
export { SpecialistSchema }
export type { SpecialistFormData }
+13
View File
@@ -0,0 +1,13 @@
import { z } from 'zod'
import type { Subspecialist } from '~/models/subspecialist'
const SubspecialistSchema = 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'),
specialist_id: z.number().positive('Spesialis harus diisi'),
})
type SubspecialistFormData = z.infer<typeof SubspecialistSchema> & Subspecialist
export { SubspecialistSchema }
export type { SubspecialistFormData }
+12
View File
@@ -0,0 +1,12 @@
import { z } from 'zod'
import type { Unit } from '~/models/unit'
const UnitSchema = 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'),
})
type UnitFormData = z.infer<typeof UnitSchema> & Unit
export { UnitSchema }
export type { UnitFormData }
+79
View File
@@ -0,0 +1,79 @@
import { xfetch } from '~/composables/useXfetch'
const mainUrl = '/api/v1/division-position'
export async function getDivisionPositions(params: any = null) {
try {
let url = mainUrl
if (params && typeof params === 'object' && Object.keys(params).length > 0) {
const searchParams = new URLSearchParams()
for (const key in params) {
if (params[key] !== null && params[key] !== undefined && params[key] !== '') {
searchParams.append(key, params[key])
}
}
const queryString = searchParams.toString()
if (queryString) url += `?${queryString}`
}
const resp = await xfetch(mainUrl, 'GET')
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error fetching division-positions:', error)
throw new Error('Failed to fetch division-positions')
}
}
export async function getDivisionPositionDetail(id: number | string) {
try {
const resp = await xfetch(`${mainUrl}/${id}`, 'GET')
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error fetching division-position detail:', error)
throw new Error('Failed to get division-position detail')
}
}
export async function postDivisionPosition(record: any) {
try {
const resp = await xfetch(mainUrl, 'POST', record)
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error posting division-position:', error)
throw new Error('Failed to post division-position')
}
}
export async function patchDivisionPosition(id: number | string, record: any) {
try {
const resp = await xfetch(`${mainUrl}/${id}`, 'PATCH', record)
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error putting division-position:', error)
throw new Error('Failed to put division-position')
}
}
export async function removeDivisionPosition(id: number | string) {
try {
const resp = await xfetch(`${mainUrl}/${id}`, 'DELETE')
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error deleting record:', error)
throw new Error('Failed to delete division-position')
}
}
+79
View File
@@ -0,0 +1,79 @@
import { xfetch } from '~/composables/useXfetch'
const mainUrl = '/api/v1/division'
export async function getDivisions(params: any = null) {
try {
let url = mainUrl
if (params && typeof params === 'object' && Object.keys(params).length > 0) {
const searchParams = new URLSearchParams()
for (const key in params) {
if (params[key] !== null && params[key] !== undefined && params[key] !== '') {
searchParams.append(key, params[key])
}
}
const queryString = searchParams.toString()
if (queryString) url += `?${queryString}`
}
const resp = await xfetch(mainUrl, 'GET')
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error fetching divisions:', error)
throw new Error('Failed to fetch divisions')
}
}
export async function getDivisionDetail(id: number | string) {
try {
const resp = await xfetch(`${mainUrl}/${id}`, 'GET')
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error fetching division detail:', error)
throw new Error('Failed to get division detail')
}
}
export async function postDivision(record: any) {
try {
const resp = await xfetch(mainUrl, 'POST', record)
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error posting division:', error)
throw new Error('Failed to post division')
}
}
export async function patchDivision(id: number | string, record: any) {
try {
const resp = await xfetch(`${mainUrl}/${id}`, 'PATCH', record)
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error putting division:', error)
throw new Error('Failed to put division')
}
}
export async function removeDivision(id: number | string) {
try {
const resp = await xfetch(`${mainUrl}/${id}`, 'DELETE')
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error deleting record:', error)
throw new Error('Failed to delete division')
}
}
+79
View File
@@ -0,0 +1,79 @@
import { xfetch } from '~/composables/useXfetch'
const mainUrl = '/api/v1/installation'
export async function getInstallations(params: any = null) {
try {
let url = mainUrl
if (params && typeof params === 'object' && Object.keys(params).length > 0) {
const searchParams = new URLSearchParams()
for (const key in params) {
if (params[key] !== null && params[key] !== undefined && params[key] !== '') {
searchParams.append(key, params[key])
}
}
const queryString = searchParams.toString()
if (queryString) url += `?${queryString}`
}
const resp = await xfetch(mainUrl, 'GET')
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error fetching installations:', error)
throw new Error('Failed to fetch installations')
}
}
export async function getInstallationDetail(id: number | string) {
try {
const resp = await xfetch(`${mainUrl}/${id}`, 'GET')
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error fetching installation detail:', error)
throw new Error('Failed to get installation detail')
}
}
export async function postInstallation(record: any) {
try {
const resp = await xfetch(mainUrl, 'POST', record)
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error posting installation:', error)
throw new Error('Failed to post installation')
}
}
export async function patchInstallation(id: number | string, record: any) {
try {
const resp = await xfetch(`${mainUrl}/${id}`, 'PATCH', record)
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error putting installation:', error)
throw new Error('Failed to put installation')
}
}
export async function removeInstallation(id: number | string) {
try {
const resp = await xfetch(`${mainUrl}/${id}`, 'DELETE')
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error deleting record:', error)
throw new Error('Failed to delete installation')
}
}
+79
View File
@@ -0,0 +1,79 @@
import { xfetch } from '~/composables/useXfetch'
const mainUrl = '/api/v1/medicine'
export async function getMedicines(params: any = null) {
try {
let url = mainUrl
if (params && typeof params === 'object' && Object.keys(params).length > 0) {
const searchParams = new URLSearchParams()
for (const key in params) {
if (params[key] !== null && params[key] !== undefined && params[key] !== '') {
searchParams.append(key, params[key])
}
}
const queryString = searchParams.toString()
if (queryString) url += `?${queryString}`
}
const resp = await xfetch(url, 'GET')
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error fetching medicines:', error)
throw new Error('Failed to fetch medicines')
}
}
export async function getMedicineDetail(id: number | string) {
try {
const resp = await xfetch(`${mainUrl}/${id}`, 'GET')
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error fetching medicine detail:', error)
throw new Error('Failed to get medicine detail')
}
}
export async function postMedicine(record: any) {
try {
const resp = await xfetch(mainUrl, 'POST', record)
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error posting medicine:', error)
throw new Error('Failed to post medicine')
}
}
export async function patchMedicine(id: number | string, record: any) {
try {
const resp = await xfetch(`${mainUrl}/${id}`, 'PATCH', record)
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error patching medicine:', error)
throw new Error('Failed to patch medicine')
}
}
export async function removeMedicine(id: number | string) {
try {
const resp = await xfetch(`${mainUrl}/${id}`, 'DELETE')
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error deleting medicine:', error)
throw new Error('Failed to delete medicine')
}
}
+79
View File
@@ -0,0 +1,79 @@
import { xfetch } from '~/composables/useXfetch'
const mainUrl = '/api/v1/specialist'
export async function getSpecialists(params: any = null) {
try {
let url = mainUrl
if (params && typeof params === 'object' && Object.keys(params).length > 0) {
const searchParams = new URLSearchParams()
for (const key in params) {
if (params[key] !== null && params[key] !== undefined && params[key] !== '') {
searchParams.append(key, params[key])
}
}
const queryString = searchParams.toString()
if (queryString) url += `?${queryString}`
}
const resp = await xfetch(mainUrl, 'GET')
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error fetching specialists:', error)
throw new Error('Failed to fetch specialists')
}
}
export async function getSpecialistDetail(id: number | string) {
try {
const resp = await xfetch(`${mainUrl}/${id}`, 'GET')
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error fetching specialist detail:', error)
throw new Error('Failed to get specialist detail')
}
}
export async function postSpecialist(record: any) {
try {
const resp = await xfetch(mainUrl, 'POST', record)
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error posting specialist:', error)
throw new Error('Failed to post specialist')
}
}
export async function patchSpecialist(id: number | string, record: any) {
try {
const resp = await xfetch(`${mainUrl}/${id}`, 'PATCH', record)
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error putting specialist:', error)
throw new Error('Failed to put specialist')
}
}
export async function removeSpecialist(id: number | string) {
try {
const resp = await xfetch(`${mainUrl}/${id}`, 'DELETE')
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error deleting record:', error)
throw new Error('Failed to delete specialist')
}
}
+79
View File
@@ -0,0 +1,79 @@
import { xfetch } from '~/composables/useXfetch'
const mainUrl = '/api/v1/subspecialist'
export async function getSubspecialists(params: any = null) {
try {
let url = mainUrl
if (params && typeof params === 'object' && Object.keys(params).length > 0) {
const searchParams = new URLSearchParams()
for (const key in params) {
if (params[key] !== null && params[key] !== undefined && params[key] !== '') {
searchParams.append(key, params[key])
}
}
const queryString = searchParams.toString()
if (queryString) url += `?${queryString}`
}
const resp = await xfetch(mainUrl, 'GET')
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error fetching subspecialists:', error)
throw new Error('Failed to fetch subspecialists')
}
}
export async function getSubspecialistDetail(id: number | string) {
try {
const resp = await xfetch(`${mainUrl}/${id}`, 'GET')
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error fetching subspecialist detail:', error)
throw new Error('Failed to get subspecialist detail')
}
}
export async function postSubspecialist(record: any) {
try {
const resp = await xfetch(mainUrl, 'POST', record)
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error posting subspecialist:', error)
throw new Error('Failed to post subspecialist')
}
}
export async function patchSubspecialist(id: number | string, record: any) {
try {
const resp = await xfetch(`${mainUrl}/${id}`, 'PATCH', record)
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error putting subspecialist:', error)
throw new Error('Failed to put subspecialist')
}
}
export async function removeSubspecialist(id: number | string) {
try {
const resp = await xfetch(`${mainUrl}/${id}`, 'DELETE')
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error deleting record:', error)
throw new Error('Failed to delete subspecialist')
}
}
+79
View File
@@ -0,0 +1,79 @@
import { xfetch } from '~/composables/useXfetch'
const mainUrl = '/api/v1/unit'
export async function getUnits(params: any = null) {
try {
let url = mainUrl
if (params && typeof params === 'object' && Object.keys(params).length > 0) {
const searchParams = new URLSearchParams()
for (const key in params) {
if (params[key] !== null && params[key] !== undefined && params[key] !== '') {
searchParams.append(key, params[key])
}
}
const queryString = searchParams.toString()
if (queryString) url += `?${queryString}`
}
const resp = await xfetch(mainUrl, 'GET')
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error fetching units:', error)
throw new Error('Failed to fetch units')
}
}
export async function getUnitDetail(id: number | string) {
try {
const resp = await xfetch(`${mainUrl}/${id}`, 'GET')
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error fetching unit detail:', error)
throw new Error('Failed to get unit detail')
}
}
export async function postUnit(record: any) {
try {
const resp = await xfetch(mainUrl, 'POST', record)
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error posting unit:', error)
throw new Error('Failed to post unit')
}
}
export async function patchUnit(id: number | string, record: any) {
try {
const resp = await xfetch(`${mainUrl}/${id}`, 'PATCH', record)
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error putting unit:', error)
throw new Error('Failed to put unit')
}
}
export async function removeUnit(id: number | string) {
try {
const resp = await xfetch(`${mainUrl}/${id}`, 'DELETE')
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error('Error deleting record:', error)
throw new Error('Failed to delete unit')
}
}