wip: add form data pelaksanaan operasi

todo: blood input section

* ui: patch focus ring state
* layouts-pages: fix width layout calculation

feat(treatment-report): add fill-notes component and validation messages
- Add new FillNotes component for tissue notes input with dynamic fields
- Update schema validation with required error messages for operation and specimen fields
- Adjust form layout to include new FillNotes component and improve field organization

cherry-pick arrangement procedure from feat/protokol-terapi-116
This commit is contained in:
Khafid Prayoga
2025-11-26 10:18:29 +07:00
parent f5704536d1
commit 5ad286a44e
21 changed files with 1198 additions and 67 deletions
@@ -0,0 +1,64 @@
<script setup lang="ts">
import { ActionEvents, type LinkItem, type ListItemDto } from '~/components/pub/my-ui/data/types';
const props = defineProps<{
rec: ListItemDto
}>()
const recId = inject<Ref<number>>('ap_rec_id')!
const recAction = inject<Ref<string>>('ap_rec_action')!
const recItem = inject<Ref<any>>('ap_rec_item')!
const activeKey = ref<string | null>(null)
const linkItems: LinkItem[] = [
{
label: 'Process',
onClick: () => {
process()
},
icon: 'i-lucide-arrow-right',
},
]
function process() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showProcess
recItem.value = props.rec
}
</script>
<template>
<div>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuButton
size="lg"
class="data-[state=open]:text-sidebar-accent-foreground data-[state=open]:bg-white dark:data-[state=open]:bg-slate-800"
>
<Icon
name="i-lucide-chevrons-up-down"
class="ml-auto size-4"
/>
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
class="w-[--radix-dropdown-menu-trigger-width] min-w-40 rounded-lg border border-slate-200 bg-white text-black dark:border-slate-700 dark:bg-slate-800 dark:text-white"
align="end"
>
<DropdownMenuGroup>
<DropdownMenuItem
v-for="item in linkItems"
:key="item.label"
class="hover:bg-gray-100 dark:hover:bg-slate-700"
@click="item.onClick"
@mouseenter="activeKey = item.label"
@mouseleave="activeKey = null"
>
<Icon :name="item.icon ?? ''" />
<span :class="activeKey === item.label ? 'text-sidebar-accent-foreground' : ''">{{ item.label }}</span>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</template>
@@ -0,0 +1,33 @@
import type { Config, RecComponent } from '~/components/pub/my-ui/data-table'
import { defineAsyncComponent } from 'vue'
const action = defineAsyncComponent(() => import('./dropdown-action-p.vue'))
export const config: Config = {
cols: [{}, {}, {}, { width: 50 }],
headers: [[{ label: 'Kode' }, { label: 'Nama (FHIR)' }, { label: 'Nama (ID)' }, { label: '' }]],
keys: ['code', 'name', 'indName', 'action'],
delKeyNames: [
{ key: 'code', label: 'Kode' },
{ key: 'name', label: 'Nama (FHIR)' },
{ key: 'indName', label: 'Nama (ID)' },
],
parses: {},
components: {
action(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: action,
}
return res
},
},
htmls: {},
}
@@ -0,0 +1,120 @@
<script setup lang="ts">
// Components
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
// Helpers
import { usePaginatedList } from '~/composables/usePaginatedList'
import { toast } from '~/components/pub/ui/toast'
import { config } from './procedure-list-cfg'
// Types
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
import { ProcedureSrcSchema, type ProcedureSrcFormData } from '~/schemas/procedure-src.schema'
// Handlers
import {
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} from '~/handlers/procedure-src.handler'
// Services
import { getList, getDetail } from '~/services/procedure-src.service'
const title = ref('')
interface Props {
processFn: (input: unknown) => void
}
const props = defineProps<Props>()
const {
data,
isLoading,
paginationMeta,
searchInput,
handlePageChange,
handleSearch,
fetchData: getItemList,
} = usePaginatedList({
fetchFn: async (params: any) => {
const result = await getList({
search: params.search,
sort: 'createdAt:desc',
'page-number': params['page-number'] || 0,
'page-size': params['page-size'] || 10,
})
return { success: result.success || false, body: result.body || {} }
},
entityName: 'procedure-src',
})
const headerPrep: HeaderPrep = {
title: 'MCU Prosedur',
icon: 'i-lucide-clipboard-list',
refSearchNav: {
placeholder: 'Cari (min. 3 karakter)...',
minLength: 3,
debounceMs: 500,
showValidationFeedback: true,
onInput: (val: string) => {
searchInput.value = val
},
onClick: () => {},
onClear: () => {},
},
}
const recId = ref<string>(``)
const recAction = ref<string>(``)
const recItem = ref<any>({})
provide('ap_rec_id', recId)
provide('ap_rec_action', recAction)
provide('ap_rec_item', recItem)
provide('table_data_loader', isLoading)
const isModalOpen = inject(`isProcedurePickerDialogOpen`) as Ref<boolean>
watch([recId, recAction], () => {
switch (recAction.value) {
case ActionEvents.showProcess:
props.processFn({
id: recId.value,
code: recItem.value.code,
name: recItem.value.name,
})
isModalOpen.value = false
break
}
})
onMounted(async () => {
await getItemList()
})
</script>
<template>
<Dialog v-model:open="isModalOpen" title="" size="xl">
<Header
v-model="searchInput"
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
@search="handleSearch"
class=""
/>
<AppProcedureSrcList
:table-config="config"
:data="data"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
</Dialog>
</template>
@@ -0,0 +1,62 @@
<script setup lang="ts">
import ProcedureListDialog from './procedure-list.vue'
import type { FormErrors } from '~/types/error'
import { toTypedSchema } from '@vee-validate/zod'
import { Form } from '~/components/pub/ui/form'
import { FieldArray } from 'vee-validate'
import InputBase from '~/components/pub/my-ui/form/input-base.vue'
import * as DE from '~/components/pub/my-ui/doc-entry'
import TextAreaInput from '~/components/pub/my-ui/form/text-area-input.vue'
import { cn } from '~/lib/utils'
import TableHeader from '~/components/pub/ui/table/TableHeader.vue'
import { is } from 'date-fns/locale'
interface Props {
fieldName: string
title: string
}
const props = defineProps<Props>()
const isProcedurePickerDialogOpen = ref<boolean>(false)
provide(`isProcedurePickerDialogOpen`, isProcedurePickerDialogOpen)
</script>
<template>
<div class="">
<div class="mb-2 flex items-center justify-between">
<h1 class="mb-2 font-medium">{{ title }}</h1>
<Button @click="isProcedurePickerDialogOpen = true" size="xs" variant="outline"
class="text-orange-400 border-orange-400 bg-transparent">
<Icon name="i-lucide-search" class="h-4 w-4 align-middle transition-colors" />
Pilih Diagnosis
</Button>
</div>
<FieldArray v-slot="{ fields, push, remove }" :name="props.fieldName">
<ProcedureListDialog :process-fn="push" />
<div class="border border-gray-200 rounded-lg overflow-hidden">
<Table>
<TableHeader class="bg-gray-100">
<TableRow>
<TableHead class="w-1/2">Prosedur</TableHead>
<TableHead class="w-1/2">ICD-X</TableHead>
<TableHead class="w-1/2">Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="(field, idx) in fields" :key="idx">
<TableCell class="">{{ field.value?.name }}</TableCell>
<TableCell class="">{{ field.value?.code }}</TableCell>
<TableCell class="">
<Button type="button" variant="destructive" size="sm" @click="remove(idx)">
<Icon name="i-lucide-trash-2" class="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</FieldArray>
</div>
</template>
@@ -14,7 +14,18 @@ import * as DE from '~/components/pub/my-ui/doc-entry'
import Separator from '~/components/pub/ui/separator/Separator.vue'
// form field components
import { ButtonAction, Fragment, InputBase } from '~/components/pub/my-ui/form/'
import {
FillNotes,
SelectBilling,
SelectBirthPlace,
SelectBirthType,
SelectOperationSystem,
SelectOperationType,
SelectSpecimen,
SelectSurgeryCounter,
SelectSurgeryType,
} from './fields'
import { ButtonAction, Fragment, InputBase, TextAreaInput } from '~/components/pub/my-ui/form/'
import { SelectDoctor } from '~/components/app/doctor/fields'
// Helpers
@@ -38,6 +49,7 @@ const emit = defineEmits<{
(e: 'error', errors: Error): void
}>()
const tissueNotesLimit = 5
const { isLoading, mode = 'create' } = props
const isReadonly = computed(() => {
if (isLoading) {
@@ -113,39 +125,45 @@ const onSubmit = handleSubmit(
field-name="operatorTeam.operatorId"
label="Operator"
placeholder="Masukkan operator"
:is-disabled="isReadonly"
/>
<InputBase
field-name="assistantOperatorId"
label="Asisten Operator"
placeholder="Masukkan asisten operator"
:is-disabled="isReadonly"
/>
<InputBase
field-name="instrumentNurseId"
label="Instrumentir"
placeholder="Masukkan instrumentir"
:is-disabled="isReadonly"
/>
<InputBase
field-name="surgeryDate"
label="Tanggal Pembedahan"
placeholder="Pilih Tanggal"
icon-name="i-lucide-calendar"
is-disabled
:is-disabled="isReadonly"
/>
</DE.Block>
<DE.Block
:col-count="4"
:cell-flex="false"
>
<InputBase
<TextAreaInput
field-name="actionDiagnosis"
label="Diagnosa Tindakan"
placeholder="Masukkan diagnosa tindakan"
:col-span="2"
:rows="5"
:is-disabled="isReadonly"
/>
<InputBase
field-name="postSurgeryNurseId"
label="Perawat Pasca Bedah"
placeholder="Masukkan perawat pasca bedah"
:is-disabled="isReadonly"
/>
</DE.Block>
</Fragment>
@@ -183,13 +201,194 @@ const onSubmit = handleSubmit(
:col-count="3"
:cell-flex="false"
>
<InputBase
field-name="operationType"
label="Tindakan Opearatif/Non"
placeholder="Masukkan tindakan operatif/non"
:col-span="2"
<SelectSurgeryType
field-name="operationExecution.surgeryType"
label="Jenis Operasi"
placeholder="Pilih jenis operasi"
:is-disabled="isReadonly"
/>
<SelectBilling
field-name="operationExecution.billingCode"
label="Kode Billing"
placeholder="Pilih kode billing"
:is-disabled="isReadonly"
/>
<SelectOperationSystem
field-name="operationExecution.operationSystem"
label="Sistem Operasi"
placeholder="Pilih sistem operasi"
:is-disabled="isReadonly"
/>
</DE.Block>
<DE.Block
:col-count="3"
:cell-flex="false"
>
<InputBase
field-name="operationExecution.operationStartAt"
label="Operasi Mulai"
placeholder="Pilih Tanggal"
icon-name="i-lucide-calendar"
:is-disabled="isReadonly"
/>
<InputBase
field-name="operationExecution.operationEndAt"
label="Operasi Selesai"
placeholder="Pilih Tanggal"
icon-name="i-lucide-calendar"
:is-disabled="isReadonly"
/>
<InputBase
field-name="_operationDuration"
label="Lama Operasi"
placeholder="03 Jam 20 Menit"
is-disabled
/>
</DE.Block>
<DE.Block
:col-count="3"
:cell-flex="false"
>
<InputBase
field-name="operationExecution.anesthesiaStartAt"
label="Pembiusan Mulai"
placeholder="Pilih Tanggal"
icon-name="i-lucide-calendar"
:is-disabled="isReadonly"
/>
<InputBase
field-name="operationExecution.anesthesiaEndAt"
label="Pembiusan Selesai"
placeholder="Pilih Tanggal"
icon-name="i-lucide-calendar"
:is-disabled="isReadonly"
/>
<InputBase
field-name="_anesthesiaDuration"
label="Lama Pembiusan"
placeholder="03 Jam 20 Menit"
is-disabled
/>
</DE.Block>
<DE.Block
:col-count="3"
:cell-flex="false"
>
<SelectOperationType
field-name="operationExecution.surgeryCleanType"
label="Jenis Pembedahan"
placeholder="Pilih jenis pembedahan"
:is-disabled="isReadonly"
/>
<SelectSurgeryCounter
field-name="operationExecution.surgeryNumber"
label="Operasi Ke"
placeholder="Pilih"
:is-disabled="isReadonly"
/>
<SelectBirthType
field-name="operationExecution.birthRemark"
label="Keterangan Lahir"
placeholder="Pilih"
:is-disabled="isReadonly"
/>
<SelectBirthPlace
field-name="operationExecution.birthPlaceNote"
label="Ket. Tempat Lahir"
placeholder="Pilih keterangan tempat lahir"
:is-disabled="isReadonly"
/>
<InputBase
field-name="operationExecution.personWeight"
label="Berat Badan"
placeholder="Masukkan berat badan"
numeric-only
suffix-msg="Gram"
:is-disabled="isReadonly"
/>
<InputBase
field-name="operationExecution.birthCondition"
label="Ket. Saat Lahir"
placeholder="Tambah catatan"
:is-disabled="isReadonly"
/>
</DE.Block>
<DE.Block
:col-count="4"
:cell-flex="false"
>
<TextAreaInput
field-name="operationExecution.operationDescription"
label="Uraian Operasi"
placeholder="Masukkan uraian"
:rows="3"
:col-span="2"
:is-disabled="isReadonly"
/>
<InputBase
field-name="operationExecution.bleedingAmountCc"
label="Jumlah Pendarahan"
placeholder="Masukkan jumlah pendarahan"
suffix-msg="CC"
numeric-only
:is-disabled="isReadonly"
/>
</DE.Block>
<DE.Block
:col-count="3"
:cell-flex="false"
>
bloods
</DE.Block>
<DE.Block
:col-count="3"
:cell-flex="false"
>
<InputBase
field-name="implant.brand"
label="Merk"
placeholder="Masukkan merk"
:is-disabled="isReadonly"
/>
<InputBase
field-name="implant.name"
label="Nama Implant"
placeholder="Masukkan nama implant"
:is-disabled="isReadonly"
/>
<InputBase
field-name="implant.sticker"
label="Sticker/Nomer Register Implant"
placeholder="Masukkan sticker/nomor register implant"
:is-disabled="isReadonly"
/>
<InputBase
field-name="implant.companionName"
label="Nama Pendamping Implant"
placeholder="Masukkan nama pendamping implant"
:is-disabled="isReadonly"
/>
<SelectSpecimen
field-name="specimen.destination"
label="Specimen/Jaringan dikirim ke"
placeholder="Pilih"
:is-disabled="isReadonly"
/>
</DE.Block>
<FillNotes
title="Keterangan Jaringan"
:limit="tissueNotesLimit"
:is-disabled="isReadonly"
/>
</Fragment>
<div class="mt-4 flex justify-end">
<button
@@ -0,0 +1,65 @@
<script setup lang="ts">
// components
import { FieldArray } from 'vee-validate'
import * as DE from '~/components/pub/my-ui/doc-entry'
// form field components
import { ButtonAction, InputBase } from '~/components/pub/my-ui/form'
interface Props {
isDisabled: boolean
limit: number
title: string
}
const props = defineProps<Props>()
const isReadonly = computed(() => props.isDisabled)
</script>
<template>
<FieldArray
v-slot="{ fields, push, remove }"
name="tissueNotes"
>
<template v-if="fields.length === 0">
{{ push({ note: '' }) }}
</template>
<div class="space-y-4">
<DE.Block
v-for="(field, idx) in fields"
:key="field.key"
:col-count="3"
:cell-flex="false"
>
<InputBase
:label="idx === 0 ? 'Keterangan Jaringan' : undefined"
:field-name="`tissueNotes[${idx}].note`"
placeholder="Masukkan catatan"
:is-disabled="isReadonly"
/>
<DE.Cell class="flex items-start justify-start">
<DE.Field :class="idx === 0 ? 'mt-[30px]' : 'mt-0'">
<ButtonAction
v-if="idx !== 0"
:disabled="isReadonly"
preset="delete"
:title="`Hapus Kontak ${idx + 1}`"
icon-only
@click="remove(idx)"
/>
</DE.Field>
</DE.Cell>
</DE.Block>
</div>
<ButtonAction
preset="add"
label="Tambah Catatan"
title="Tambah Catatan Keterangan Jaringan"
:disabled="fields.length >= limit || isReadonly"
:full-width-mobile="true"
class="mt-4"
@click="push({ name: '', dose: '', unit: '' })"
/>
</FieldArray>
</template>
@@ -0,0 +1,9 @@
export { default as FillNotes } from './fill-notes.vue'
export { default as SelectBilling } from './select-billing.vue'
export { default as SelectBirthPlace } from './select-birth-place.vue'
export { default as SelectBirthType } from './select-birth-type.vue'
export { default as SelectOperationSystem } from './select-operation-system.vue'
export { default as SelectOperationType } from './select-operation-type.vue'
export { default as SelectSpecimen } from './select-specimen.vue'
export { default as SelectSurgeryCounter } from './select-surgery-counter.vue'
export { default as SelectSurgeryType } from './select-surgery-type.vue'
@@ -0,0 +1,68 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Select from '~/components/pub/my-ui/form/select.vue'
import { cn } from '~/lib/utils'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName: string
label: string
isDisabled?: boolean
isRequired?: boolean
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
}>()
const { errors, class: containerClass, selectClass, fieldGroupClass } = props
const opts = [
{ label: 'General', value: 'general' },
{ label: 'Regional', value: 'regional' },
{ label: 'Local', value: 'local' },
]
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
label-for="fieldName"
:is-required="isRequired"
>
{{ label }}
</DE.Label>
<DE.Field
:id="fieldName"
:errors="errors"
:class="cn('select-field-wrapper')"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Select
:id="fieldName"
:is-disabled="isDisabled"
v-bind="componentField"
:items="opts"
:placeholder="placeholder"
:preserve-order="false"
:class="
cn(
'text-sm transition-all duration-200 focus:outline-none focus:ring-1 focus:ring-black focus:ring-offset-0',
selectClass,
)
"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,71 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Select from '~/components/pub/my-ui/form/select.vue'
import { cn } from '~/lib/utils'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName: string
label: string
isDisabled?: boolean
isRequired?: boolean
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
}>()
const { errors, class: containerClass, selectClass, fieldGroupClass } = props
const opts = [
{ label: 'RSSA', value: 'rssa' },
{ label: 'Bidan Luar', value: 'out1' },
{ label: 'Dokter Luar', value: 'out2' },
{ label: 'Dukun Bayi', value: 'out3' },
{ label: 'Puskesmas', value: 'out4' },
{ label: 'Paramedis Luar', value: 'out5' },
]
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
label-for="fieldName"
:is-required="isRequired"
>
{{ label }}
</DE.Label>
<DE.Field
:id="fieldName"
:errors="errors"
:class="cn('select-field-wrapper')"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Select
:id="fieldName"
:is-disabled="isDisabled"
v-bind="componentField"
:items="opts"
:placeholder="placeholder"
:preserve-order="false"
:class="
cn(
'text-sm transition-all duration-200 focus:outline-none focus:ring-1 focus:ring-black focus:ring-offset-0',
selectClass,
)
"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,67 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Select from '~/components/pub/my-ui/form/select.vue'
import { cn } from '~/lib/utils'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName: string
label: string
isDisabled?: boolean
isRequired?: boolean
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
}>()
const { errors, class: containerClass, selectClass, fieldGroupClass } = props
const opts = [
{ label: 'Lahir Hidup', value: 'lahir_hidup' },
{ label: 'Lahir Mati', value: 'lahir_mati' },
]
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
label-for="fieldName"
:is-required="isRequired"
>
{{ label }}
</DE.Label>
<DE.Field
:id="fieldName"
:errors="errors"
:class="cn('select-field-wrapper')"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Select
:id="fieldName"
:is-disabled="isDisabled"
v-bind="componentField"
:items="opts"
:placeholder="placeholder"
:preserve-order="false"
:class="
cn(
'text-sm transition-all duration-200 focus:outline-none focus:ring-1 focus:ring-black focus:ring-offset-0',
selectClass,
)
"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,69 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Select from '~/components/pub/my-ui/form/select.vue'
import { cn } from '~/lib/utils'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName: string
label: string
isDisabled?: boolean
isRequired?: boolean
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
}>()
const { errors, class: containerClass, selectClass, fieldGroupClass } = props
const opts = [
{ label: 'Cito', value: 'cito' },
{ label: 'Urgent', value: 'urgent' },
{ label: 'Efektif', value: 'efektif' },
{ label: 'Khusus', value: 'khusus' },
]
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
label-for="fieldName"
:is-required="isRequired"
>
{{ label }}
</DE.Label>
<DE.Field
:id="fieldName"
:errors="errors"
:class="cn('select-field-wrapper')"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Select
:id="fieldName"
:is-disabled="isDisabled"
v-bind="componentField"
:items="opts"
:placeholder="placeholder"
:preserve-order="false"
:class="
cn(
'text-sm transition-all duration-200 focus:outline-none focus:ring-1 focus:ring-black focus:ring-offset-0',
selectClass,
)
"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,69 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Select from '~/components/pub/my-ui/form/select.vue'
import { cn } from '~/lib/utils'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName: string
label: string
isDisabled?: boolean
isRequired?: boolean
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
}>()
const { errors, class: containerClass, selectClass, fieldGroupClass } = props
const opts = [
{ label: 'Bersih', value: 'bersih' },
{ label: 'Bersih Terkontaminasi', value: 'bersih_terkontaminasi' },
{ label: 'Terkontaminasi Kotor', value: 'terkontaminasi' },
{ label: 'Kotor', value: 'kotor' },
]
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
label-for="fieldName"
:is-required="isRequired"
>
{{ label }}
</DE.Label>
<DE.Field
:id="fieldName"
:errors="errors"
:class="cn('select-field-wrapper')"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Select
:id="fieldName"
:is-disabled="isDisabled"
v-bind="componentField"
:items="opts"
:placeholder="placeholder"
:preserve-order="false"
:class="
cn(
'text-sm transition-all duration-200 focus:outline-none focus:ring-1 focus:ring-black focus:ring-offset-0',
selectClass,
)
"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,69 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Select from '~/components/pub/my-ui/form/select.vue'
import { cn } from '~/lib/utils'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName: string
label: string
isDisabled?: boolean
isRequired?: boolean
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
}>()
const { errors, class: containerClass, selectClass, fieldGroupClass } = props
const opts = [
{ label: 'PA', value: 'pa' },
{ label: 'Mikrobiologi', value: 'microbiology' },
{ label: 'Laborat', value: 'laboratory' },
{ label: 'Tidak Perlu', value: 'none' },
]
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
label-for="fieldName"
:is-required="isRequired"
>
{{ label }}
</DE.Label>
<DE.Field
:id="fieldName"
:errors="errors"
:class="cn('select-field-wrapper')"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Select
:id="fieldName"
:is-disabled="isDisabled"
v-bind="componentField"
:items="opts"
:placeholder="placeholder"
:preserve-order="false"
:class="
cn(
'text-sm transition-all duration-200 focus:outline-none focus:ring-1 focus:ring-black focus:ring-offset-0',
selectClass,
)
"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,67 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Select from '~/components/pub/my-ui/form/select.vue'
import { cn } from '~/lib/utils'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName: string
label: string
isDisabled?: boolean
isRequired?: boolean
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
}>()
const { errors, class: containerClass, selectClass, fieldGroupClass } = props
const opts = [
{ label: '1 (Satu)', value: 'first' },
{ label: 'Ulangan', value: 'retry' },
]
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
label-for="fieldName"
:is-required="isRequired"
>
{{ label }}
</DE.Label>
<DE.Field
:id="fieldName"
:errors="errors"
:class="cn('select-field-wrapper')"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Select
:id="fieldName"
:is-disabled="isDisabled"
v-bind="componentField"
:items="opts"
:placeholder="placeholder"
:preserve-order="false"
:class="
cn(
'text-sm transition-all duration-200 focus:outline-none focus:ring-1 focus:ring-black focus:ring-offset-0',
selectClass,
)
"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,69 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Select from '~/components/pub/my-ui/form/select.vue'
import { cn } from '~/lib/utils'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName: string
label: string
isDisabled?: boolean
isRequired?: boolean
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
}>()
const { errors, class: containerClass, selectClass, fieldGroupClass } = props
const opts = [
{ label: 'Kecil', value: 'kecil' },
{ label: 'Sedang', value: 'sedang' },
{ label: 'Besar', value: 'besar' },
{ label: 'Khusus', value: 'khusus' },
]
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
label-for="fieldName"
:is-required="isRequired"
>
{{ label }}
</DE.Label>
<DE.Field
:id="fieldName"
:errors="errors"
:class="cn('select-field-wrapper')"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Select
:id="fieldName"
:is-disabled="isDisabled"
v-bind="componentField"
:items="opts"
:placeholder="placeholder"
:preserve-order="false"
:class="
cn(
'text-sm transition-all duration-200 focus:outline-none focus:ring-1 focus:ring-black focus:ring-offset-0',
selectClass,
)
"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
+5 -4
View File
@@ -76,10 +76,11 @@ const hasError = computed(() => !!fieldError.value)
v-bind="componentField"
:placeholder="placeholder"
:maxlength="maxLength"
:class="cn(
'focus:border-primary focus:ring-2 focus:ring-primary focus:ring-offset-0',
hasError && 'border-red-500 focus:border-red-500 focus:ring-red-500'
)"
:class="
cn(
hasError && 'border-red-500 focus-visible:border-red-500 focus-visible:ring-red-500',
)
"
autocomplete="off"
aria-autocomplete="none"
autocorrect="off"
@@ -19,8 +19,11 @@ const props = defineProps<{
maxLength?: number
isRequired?: boolean
isDisabled?: boolean
resize?: 'none' | 'y' | 'x'
rows?: number
}>()
const { resize = 'none' } = props
function handleInput(event: Event) {
const target = event.target as HTMLInputElement
let value = target.value
@@ -47,7 +50,7 @@ function handleInput(event: Event) {
<template>
<DE.Cell :col-span="colSpan || 1">
<DE.Label
class="mb-1"
:class="class"
v-if="label !== ''"
:label-for="fieldName"
:is-required="isRequired && !isDisabled"
@@ -65,11 +68,19 @@ function handleInput(event: Event) {
<FormItem>
<FormControl>
<Textarea
:rows="rows"
:disabled="isDisabled"
v-bind="componentField"
:placeholder="placeholder"
:maxlength="maxLength"
:class="cn('focus:border-primary focus:ring-2 focus:ring-primary focus:ring-offset-0')"
:class="
cn(
'focus:border-primary focus:ring-primary focus:ring-offset-0',
resize === 'y' && 'resize-y',
resize === 'x' && 'resize-x',
resize === 'none' && 'resize-none',
)
"
autocomplete="off"
aria-autocomplete="none"
autocorrect="off"
@@ -83,4 +94,4 @@ function handleInput(event: Event) {
</FormField>
</DE.Field>
</DE.Cell>
</template>
</template>
+1 -1
View File
@@ -24,7 +24,7 @@ const modelValue = useVModel(props, 'modelValue', emits, {
v-model="modelValue"
:class="
cn(
'border-input dark:bg-slate-950 ring-offset-background placeholder:text-muted-foreground flex h-9 md:h-8 2xl:h-9 w-full rounded-md border border-gray-400 px-3 py-2 md:text-xs 2xl:text-sm file:border-0 file:bg-transparent md:file:!text-xs xl:file:!text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50',
'border-input dark:bg-slate-950 ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary focus-visible:ring-offset-0 focus-visible:border-primary flex h-9 md:h-8 2xl:h-9 w-full rounded-md border border-gray-400 px-3 py-2 md:text-xs 2xl:text-sm file:border-0 file:bg-transparent md:file:!text-xs xl:file:!text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50',
props.class,
)
"
+48 -40
View File
@@ -2,76 +2,84 @@
import CardContent from '~/components/pub/ui/card/CardContent.vue'
const route = useRoute()
const contentFrame = computed(() => route.meta.contentFrame)
// Ambil meta dan set default
const contentFrame = computed(() => route.meta.contentFrame ?? 'cf-container-lg')
// Mapping aman supaya tidak ada class invalid
const contentFrameClass = computed(() => {
switch (contentFrame.value) {
case 'cf-container-2xl':
return 'cf-container-2xl'
case 'cf-container-xl':
return 'cf-container-xl'
case 'cf-container-lg':
return 'cf-container-lg'
case 'cf-container-md':
return 'cf-container-md'
case 'cf-container-sm':
return 'cf-container-sm'
case 'cf-full-width':
return 'cf-full-width'
default:
return 'cf-container-lg'
}
const allowed = [
'cf-container-2xl',
'cf-container-xl',
'cf-container-lg',
'cf-container-md',
'cf-container-sm',
'cf-full-width',
'cf-no-frame',
]
return allowed.includes(contentFrame.value as string) ? contentFrame.value : 'cf-container-lg'
})
</script>
<template>
<SidebarProvider>
<LayoutAppSidebar />
<SidebarInset>
<LayoutHeader />
<div :class="`w-full p-4 2xl:p-5 flex justify-center ${contentFrameClass}`">
<div v-if="contentFrame !== 'cf-no-frame'">
<Card>
<CardContent>
<slot />
</CardContent>
</Card>
</div>
<slot v-else />
<!-- Outer wrapper always full width -->
<div class="flex w-full justify-center p-4 2xl:p-5">
<!-- Frame mode (container + card) -->
<template v-if="contentFrameClass !== 'cf-no-frame'">
<div :class="['content-frame', contentFrameClass]">
<Card>
<CardContent>
<slot />
</CardContent>
</Card>
</div>
</template>
<!-- No frame direct slot -->
<template v-else>
<slot />
</template>
</div>
</SidebarInset>
</SidebarProvider>
</template>
<style scoped>
.cf-container > *,
.cf-container-lg > *,
.cf-container-md > *,
.cf-container-sm > *,
.cf-full-width {
/* Base: semua frame full width by default */
.content-frame {
width: 100%;
}
.cf-full-width > * {
width: 100%;
}
.cf-container-2xl > * {
/* Max-width container variants */
.cf-container-2xl {
max-width: 1400px;
}
.cf-container-xl > * {
.cf-container-xl {
max-width: 1200px;
}
.cf-container-lg > * {
.cf-container-lg {
max-width: 992px;
}
.cf-container-md > * {
.cf-container-md {
max-width: 768px;
}
.cf-container-sm > * {
.cf-container-sm {
max-width: 576px;
}
/* Full width (no max width) */
.cf-full-width {
width: 100%;
max-width: 100%;
}
</style>
@@ -7,7 +7,7 @@ definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Tambah Laporan',
contentFrame: 'cf-full-width',
contentFrame: 'cf-container-2xl',
})
const route = useRoute()
+20 -10
View File
@@ -1,7 +1,7 @@
import { z } from 'zod'
const isoDateTime = z.string().min(1, 'Tanggal / waktu wajib diisi')
const positiveInt = z.number().int().nonnegative()
const positiveInt = z.coerce.number().int().nonnegative()
const OperatorTeamSchema = z.object({
dpjpId: z.coerce
@@ -30,9 +30,11 @@ const ProcedureSchema = z.object({
})
const OperationExecutionSchema = z.object({
surgeryType: z.enum(['kecil', 'sedang', 'besar', 'khusus']),
billingCode: z.string().min(1),
operationSystem: z.enum(['khusus', 'cito', 'elektif']),
surgeryType: z.enum(['kecil', 'sedang', 'besar', 'khusus'], { required_error: 'Silahkan pilih jenis operasi' }),
billingCode: z.string({
required_error: 'Silahkan pilih kode billing',
}),
operationSystem: z.enum(['khusus', 'cito', 'efektif', 'urgent'], { required_error: 'Silahkan pilih sistem operasi' }),
operationStartAt: isoDateTime,
operationEndAt: isoDateTime,
@@ -41,17 +43,19 @@ const OperationExecutionSchema = z.object({
anesthesiaEndAt: isoDateTime,
surgeryCleanType: z.enum(['bersih', 'bersih_terkontaminasi', 'terkontaminasi', 'kotor']).optional(),
surgeryNumber: z.enum(['1', '2', '3', '4+', 'tidak_diketahui']).optional(),
surgeryNumber: z.enum(['first', 'retry']).optional(),
birthPlaceNote: z.string().optional(),
babyWeightGram: positiveInt.optional(),
personWeight: positiveInt.optional(),
birthCondition: z.string().optional(),
operationDescription: z.string().min(1),
operationDescription: z.string({
required_error: 'Mohon lengkapi uraian operasi',
}),
bleedingAmountCc: positiveInt.optional(),
birthRemark: z.enum(['lahir_hidup', 'lahir_mati', 'abortus', 'tidak_diketahui']).optional(),
birthRemark: z.enum(['lahir_hidup', 'lahir_mati']).optional(),
})
const BloodComponentSchema = z.object({
@@ -74,11 +78,17 @@ const ImplantSchema = z.object({
})
const SpecimenSchema = z.object({
destination: z.string().min(1),
destination: z.string({
required_error: 'Silahkan pilih specimen',
}),
})
const TissueNoteSchema = z.object({
note: z.string().min(1),
note: z
.string({
required_error: 'Masukkan deskripsi catatan',
})
.min(1, { message: 'Setidaknya diperlukan 1 catatan' }),
})
export const TreatmentReportSchema = z.object({