refactor(treatment-report): restructure treatment report form and components
- Replace SelectDPJP with SelectDoctor component - Update schema naming from ActionReport to TreatmentReport - Add doctor selection functionality to treatment report form - Improve form layout and field organization - Update related model imports to use single quotes - add fragment for better form grouping - cherry pick form field from another branch
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
export { default as SelectDoctor } from './select-doctor.vue'
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
// type
|
||||||
|
import type { Doctor } from '~/models/doctor'
|
||||||
|
import { type Person, parseName } from '~/models/person'
|
||||||
|
|
||||||
|
// componenets
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fieldName: string
|
||||||
|
label: string
|
||||||
|
placeholder: string
|
||||||
|
doctors?: Doctor[]
|
||||||
|
class?: string
|
||||||
|
selectClass?: string
|
||||||
|
fieldGroupClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
isRequired?: boolean
|
||||||
|
isDisabled?: boolean
|
||||||
|
colSpan?: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { class: containerClass, labelClass, colSpan = 1, doctors = [] } = props
|
||||||
|
|
||||||
|
const opts = computed(() => {
|
||||||
|
return doctors.map((doc) => ({
|
||||||
|
value: doc.id.toString(),
|
||||||
|
label: parseName(doc.employee.person as Person),
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DE.Cell
|
||||||
|
:col-span="colSpan"
|
||||||
|
:class="cn('select-field-group', fieldGroupClass, containerClass)"
|
||||||
|
>
|
||||||
|
<DE.Label
|
||||||
|
:label-for="fieldName"
|
||||||
|
:is-required="isRequired"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</DE.Label>
|
||||||
|
<DE.Field
|
||||||
|
:id="fieldName"
|
||||||
|
:class="cn('select-field-wrapper')"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
:name="fieldName"
|
||||||
|
>
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Combobox
|
||||||
|
:id="fieldName"
|
||||||
|
v-bind="componentField"
|
||||||
|
:items="opts"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:is-disabled="isDisabled"
|
||||||
|
search-placeholder="Cari dokter..."
|
||||||
|
empty-message="Dokter tidak ditemukan"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</DE.Field>
|
||||||
|
</DE.Cell>
|
||||||
|
</template>
|
||||||
@@ -1,7 +1,164 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SelectDPJP } from './fields'
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { TreatmentReportSchema } from '~/schemas/treatment-report.schema'
|
||||||
|
// type
|
||||||
|
import type { Doctor } from '~/models/doctor'
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { Form } from '~/components/pub/ui/form'
|
||||||
|
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 { SelectDoctor } from '~/components/app/doctor/fields'
|
||||||
|
// Helpers
|
||||||
|
|
||||||
|
// #region Props & Emits
|
||||||
|
interface Props {
|
||||||
|
isLoading: boolean
|
||||||
|
mode?: 'create' | 'update' | 'view'
|
||||||
|
initialValues?: any
|
||||||
|
|
||||||
|
// form related
|
||||||
|
doctors: Doctor[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const { mode = 'create' } = props
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region State & Computed
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Lifecycle Hooks
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Functions
|
||||||
|
// #endregion region
|
||||||
|
|
||||||
|
// #region Utilities & event handlers
|
||||||
|
// #endregion
|
||||||
|
const formSchema = toTypedSchema(TreatmentReportSchema)
|
||||||
|
const formRef = ref()
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
validate: () => formRef.value?.validate(),
|
||||||
|
resetForm: () => formRef.value?.resetForm(),
|
||||||
|
setValues: (values: any, shouldValidate = true) => formRef.value?.setValues(values, shouldValidate),
|
||||||
|
values: computed(() => formRef.value?.values),
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SelectDPJP />
|
<Form
|
||||||
|
ref="formRef"
|
||||||
|
as=""
|
||||||
|
keep-values
|
||||||
|
:validation-schema="formSchema"
|
||||||
|
:validate-on-mount="false"
|
||||||
|
:initial-values="initialValues ? initialValues : {}"
|
||||||
|
validation-mode="onSubmit"
|
||||||
|
>
|
||||||
|
<Fragment
|
||||||
|
v-slot="{ section }"
|
||||||
|
title="Tim Pelaksana Tindakan"
|
||||||
|
>
|
||||||
|
<p class="text-lg font-semibold">{{ section }}</p>
|
||||||
|
|
||||||
|
<DE.Block
|
||||||
|
:col-count="4"
|
||||||
|
:cell-flex="false"
|
||||||
|
>
|
||||||
|
<SelectDoctor
|
||||||
|
:doctors="doctors"
|
||||||
|
fieldName="dpjp"
|
||||||
|
label="Dokter Pemeriksa"
|
||||||
|
placeholder="Pilih dokter"
|
||||||
|
/>
|
||||||
|
<InputBase
|
||||||
|
field-name="operatorId"
|
||||||
|
label="Operator"
|
||||||
|
placeholder="Masukkan operator"
|
||||||
|
/>
|
||||||
|
<InputBase
|
||||||
|
field-name="assistantOperatorId"
|
||||||
|
label="Asisten Operator"
|
||||||
|
placeholder="Masukkan asisten operator"
|
||||||
|
/>
|
||||||
|
<InputBase
|
||||||
|
field-name="instrumentNurseId"
|
||||||
|
label="Instrumentir"
|
||||||
|
placeholder="Masukkan instrumentir"
|
||||||
|
/>
|
||||||
|
<InputBase
|
||||||
|
field-name="surgeryDate"
|
||||||
|
label="Tanggal Pembedahan"
|
||||||
|
placeholder="Pilih Tanggal"
|
||||||
|
icon-name="i-lucide-calendar"
|
||||||
|
is-disabled
|
||||||
|
/>
|
||||||
|
</DE.Block>
|
||||||
|
<DE.Block
|
||||||
|
:col-count="4"
|
||||||
|
:cell-flex="false"
|
||||||
|
>
|
||||||
|
<InputBase
|
||||||
|
field-name="actionDiagnosis"
|
||||||
|
label="Diagnosa Tindakan"
|
||||||
|
placeholder="Masukkan diagnosa tindakan"
|
||||||
|
:col-span="2"
|
||||||
|
/>
|
||||||
|
<InputBase
|
||||||
|
field-name="postSurgeryNurseId"
|
||||||
|
label="Perawat Pasca Bedah"
|
||||||
|
placeholder="Masukkan perawat pasca bedah"
|
||||||
|
/>
|
||||||
|
</DE.Block>
|
||||||
|
</Fragment>
|
||||||
|
|
||||||
|
<Separator class="my-4" />
|
||||||
|
|
||||||
|
<Fragment
|
||||||
|
v-slot="{ section }"
|
||||||
|
title="Tindakan Operatif/Non Operatif Lain"
|
||||||
|
>
|
||||||
|
<p class="text-lg font-semibold">{{ section }}</p>
|
||||||
|
|
||||||
|
<DE.Block
|
||||||
|
:col-count="4"
|
||||||
|
:cell-flex="false"
|
||||||
|
>
|
||||||
|
<InputBase
|
||||||
|
field-name="operationType"
|
||||||
|
label="Tindakan Opearatif/Non"
|
||||||
|
placeholder="Masukkan tindakan operatif/non"
|
||||||
|
:col-span="2"
|
||||||
|
/>
|
||||||
|
</DE.Block>
|
||||||
|
</Fragment>
|
||||||
|
|
||||||
|
<Separator class="my-4" />
|
||||||
|
|
||||||
|
<Fragment
|
||||||
|
v-slot="{ section }"
|
||||||
|
title="Data Pelaksanaan Operasi"
|
||||||
|
>
|
||||||
|
<p class="text-lg font-semibold">{{ section }}</p>
|
||||||
|
|
||||||
|
<DE.Block
|
||||||
|
:col-count="3"
|
||||||
|
:cell-flex="false"
|
||||||
|
>
|
||||||
|
<InputBase
|
||||||
|
field-name="operationType"
|
||||||
|
label="Tindakan Opearatif/Non"
|
||||||
|
placeholder="Masukkan tindakan operatif/non"
|
||||||
|
:col-span="2"
|
||||||
|
/>
|
||||||
|
</DE.Block>
|
||||||
|
</Fragment>
|
||||||
|
</Form>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export { default as SelectDPJP } from './select-dpjp.vue'
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<template>select dpjp</template>
|
|
||||||
@@ -1,7 +1,21 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
// type
|
||||||
|
import { genDoctor, type Doctor } from '~/models/doctor'
|
||||||
|
|
||||||
|
// components
|
||||||
import AppTreatmentReportEntry from '~/components/app/treatment-report/entry-form.vue'
|
import AppTreatmentReportEntry from '~/components/app/treatment-report/entry-form.vue'
|
||||||
|
|
||||||
|
const doctors = ref<Doctor[]>([])
|
||||||
|
|
||||||
|
// TODO: dummy data
|
||||||
|
;(() => {
|
||||||
|
doctors.value = [genDoctor()]
|
||||||
|
})()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AppTreatmentReportEntry />
|
<AppTreatmentReportEntry
|
||||||
|
:isLoading="false"
|
||||||
|
:doctors="doctors"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Button } from '~/components/pub/ui/button'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button Action Component untuk form
|
||||||
|
* Support preset: add, delete, save, cancel
|
||||||
|
*/
|
||||||
|
interface Props {
|
||||||
|
/**
|
||||||
|
* Preset button type dengan styling bawaan
|
||||||
|
* - add: Button tambah (outline primary)
|
||||||
|
* - delete: Button hapus (ghost)
|
||||||
|
* - save: Button simpan (primary)
|
||||||
|
* - cancel: Button batal (secondary)
|
||||||
|
* - custom: Custom styling
|
||||||
|
*/
|
||||||
|
preset?: 'add' | 'delete' | 'save' | 'cancel' | 'custom'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon name (UnoCSS/Lucide)
|
||||||
|
* Default akan diset berdasarkan preset
|
||||||
|
*/
|
||||||
|
icon?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button text
|
||||||
|
* Set ke empty string ('') untuk icon-only mode
|
||||||
|
*/
|
||||||
|
label?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button title (tooltip)
|
||||||
|
* Wajib untuk icon-only buttons (accessibility)
|
||||||
|
*/
|
||||||
|
title?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon only mode (no label)
|
||||||
|
* Otomatis true jika label kosong
|
||||||
|
*/
|
||||||
|
iconOnly?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button type
|
||||||
|
*/
|
||||||
|
type?: 'button' | 'submit' | 'reset'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disabled state
|
||||||
|
*/
|
||||||
|
disabled?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom class untuk override styling
|
||||||
|
*/
|
||||||
|
class?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responsive width (full width on mobile)
|
||||||
|
*/
|
||||||
|
fullWidthMobile?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button size
|
||||||
|
*/
|
||||||
|
size?: 'default' | 'sm' | 'lg' | 'icon'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button variant (override preset variant)
|
||||||
|
*/
|
||||||
|
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
preset: 'custom',
|
||||||
|
type: 'button',
|
||||||
|
disabled: false,
|
||||||
|
fullWidthMobile: false,
|
||||||
|
iconOnly: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Preset configurations
|
||||||
|
const presetConfig = {
|
||||||
|
add: {
|
||||||
|
variant: 'outline' as const,
|
||||||
|
icon: 'i-lucide-plus',
|
||||||
|
label: 'Tambah',
|
||||||
|
classes:
|
||||||
|
'border-primary bg-white text-primary hover:bg-primary hover:text-white dark:bg-slate-800 dark:border-primary dark:text-primary dark:hover:bg-primary dark:hover:text-white',
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
variant: 'ghost' as const,
|
||||||
|
icon: 'i-lucide-trash-2',
|
||||||
|
label: '', // Default kosong untuk icon-only
|
||||||
|
classes:
|
||||||
|
'hover:bg-destructive hover:text-white hover:border-destructive dark:hover:bg-destructive dark:hover:text-white',
|
||||||
|
},
|
||||||
|
save: {
|
||||||
|
variant: 'default' as const,
|
||||||
|
icon: 'i-lucide-save',
|
||||||
|
label: 'Simpan',
|
||||||
|
classes: 'bg-primary text-primary-foreground hover:bg-primary/90 dark:bg-primary dark:hover:bg-primary/90',
|
||||||
|
},
|
||||||
|
cancel: {
|
||||||
|
variant: 'secondary' as const,
|
||||||
|
icon: 'i-lucide-x',
|
||||||
|
label: 'Batal',
|
||||||
|
classes: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 dark:bg-slate-700 dark:hover:bg-slate-600',
|
||||||
|
},
|
||||||
|
custom: {
|
||||||
|
variant: 'default' as const,
|
||||||
|
icon: '',
|
||||||
|
label: '',
|
||||||
|
classes: '',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPreset = computed(() => presetConfig[props.preset])
|
||||||
|
const buttonVariant = computed(() => props.variant || currentPreset.value.variant)
|
||||||
|
const buttonIcon = computed(() => props.icon || currentPreset.value.icon)
|
||||||
|
|
||||||
|
// Label handling: gunakan prop label jika ada, fallback ke preset, atau undefined jika iconOnly
|
||||||
|
const buttonLabel = computed(() => {
|
||||||
|
if (props.label !== undefined) return props.label
|
||||||
|
return currentPreset.value.label
|
||||||
|
})
|
||||||
|
|
||||||
|
const buttonTitle = computed(() => props.title || buttonLabel.value)
|
||||||
|
|
||||||
|
// Deteksi icon-only mode
|
||||||
|
const isIconOnly = computed(() => {
|
||||||
|
return props.iconOnly || buttonLabel.value === '' || !buttonLabel.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const buttonClasses = computed(() => {
|
||||||
|
// Base classes berbeda untuk icon-only vs with-label
|
||||||
|
const baseClasses = isIconOnly.value
|
||||||
|
? 'rounded-md p-2 w-9 h-9 transition-colors flex items-center justify-center' // Icon only: square button
|
||||||
|
: 'rounded-md px-4 py-2 transition-colors sm:text-sm' // With label: padding horizontal lebih besar
|
||||||
|
|
||||||
|
const widthClasses = props.fullWidthMobile && !isIconOnly.value ? 'w-full sm:w-auto' : ''
|
||||||
|
const presetClasses = currentPreset.value.classes
|
||||||
|
|
||||||
|
return cn(baseClasses, widthClasses, presetClasses, props.class)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Button
|
||||||
|
:title="buttonTitle"
|
||||||
|
:type="type"
|
||||||
|
:variant="buttonVariant"
|
||||||
|
:size="size"
|
||||||
|
:disabled="disabled"
|
||||||
|
:class="buttonClasses"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
v-if="buttonIcon"
|
||||||
|
:name="buttonIcon"
|
||||||
|
:class="cn('h-4 w-4 align-middle transition-colors', !isIconOnly ? 'mr-2' : '')"
|
||||||
|
/>
|
||||||
|
<slot v-if="!isIconOnly">{{ buttonLabel }}</slot>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormErrors } from '~/types/error'
|
import type { FormErrors } from '~/types/error'
|
||||||
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
|
|
||||||
import Field from '~/components/pub/my-ui/form/field.vue'
|
|
||||||
import Label from '~/components/pub/my-ui/form/label.vue'
|
|
||||||
import { Input } from '~/components/pub/ui/input'
|
import { Input } from '~/components/pub/ui/input'
|
||||||
import { cn } from '~/lib/utils'
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
@@ -62,7 +59,7 @@ async function onFileChange(event: Event, handleChange: (value: any) => void) {
|
|||||||
@change="onFileChange($event, handleChange)"
|
@change="onFileChange($event, handleChange)"
|
||||||
type="file"
|
type="file"
|
||||||
:disabled="isDisabled"
|
:disabled="isDisabled"
|
||||||
v-bind="{ onBlur: componentField.onBlur }"
|
v-bind="{ onBlur: componentField.onBlur }"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:class="cn('focus:border-primary focus:ring-2 focus:ring-primary focus:ring-offset-0')"
|
:class="cn('focus:border-primary focus:ring-2 focus:ring-primary focus:ring-offset-0')"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* A "Ghost" wrapper component designed to group related form fields
|
||||||
|
* without adding unnecessary depth to the DOM tree.
|
||||||
|
* * Unlike a standard <div> wrapper, this component leverages Vue 3's
|
||||||
|
* multi-root feature to render the title and slot content as direct siblings.
|
||||||
|
* This ensures the parent Form's grid or flex layout remains intact.
|
||||||
|
* * @property {string} [title] for compiler marker.
|
||||||
|
*
|
||||||
|
* Example Usage:
|
||||||
|
*
|
||||||
|
<Fragment
|
||||||
|
v-slot="{ section }"
|
||||||
|
title="Tim Pelaksana Tindakan"
|
||||||
|
>
|
||||||
|
<p class="text-lg font-semibold">{{ section }}</p>
|
||||||
|
</Fragment>
|
||||||
|
*/
|
||||||
|
defineProps<{
|
||||||
|
title?: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<slot :section="title" />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export { default as Block } from './block.vue'
|
||||||
|
export { default as ButtonAction } from './button-action.vue'
|
||||||
|
export { default as FieldGroup } from './field-group.vue'
|
||||||
|
export { default as Field } from './field.vue'
|
||||||
|
export { default as FileField } from './file-field.vue'
|
||||||
|
export { default as Fragment } from './fragment.vue'
|
||||||
|
export { default as InputBase } from './input-base.vue'
|
||||||
|
export { default as Label } from './label.vue'
|
||||||
|
export { default as Select } from './select.vue'
|
||||||
|
export { default as TextAreaInput } from './text-area-input.vue'
|
||||||
|
export { default as TextCaptcha } from './text-captcha.vue'
|
||||||
@@ -1,17 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormErrors } from '~/types/error'
|
import type { FormErrors } from '~/types/error'
|
||||||
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
|
|
||||||
import Field from '~/components/pub/my-ui/form/field.vue'
|
|
||||||
import Label from '~/components/pub/my-ui/form/label.vue'
|
|
||||||
import { Input } from '~/components/pub/ui/input'
|
import { Input } from '~/components/pub/ui/input'
|
||||||
import { cn } from '~/lib/utils'
|
import { cn } from '~/lib/utils'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useFieldError } from 'vee-validate'
|
||||||
|
|
||||||
import * as DE from '~/components/pub/my-ui/doc-entry'
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
fieldName: string
|
fieldName: string
|
||||||
placeholder: string
|
placeholder: string
|
||||||
label: string
|
label?: string
|
||||||
errors?: FormErrors
|
errors?: FormErrors
|
||||||
class?: string
|
class?: string
|
||||||
colSpan?: number
|
colSpan?: number
|
||||||
@@ -21,6 +20,8 @@ const props = defineProps<{
|
|||||||
isDisabled?: boolean
|
isDisabled?: boolean
|
||||||
rightLabel?: string
|
rightLabel?: string
|
||||||
bottomLabel?: string
|
bottomLabel?: string
|
||||||
|
suffixMsg?: string
|
||||||
|
iconName?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
function handleInput(event: Event) {
|
function handleInput(event: Event) {
|
||||||
@@ -44,12 +45,16 @@ function handleInput(event: Event) {
|
|||||||
target.dispatchEvent(new Event('input', { bubbles: true }))
|
target.dispatchEvent(new Event('input', { bubbles: true }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get error state from vee-validate
|
||||||
|
const fieldError = useFieldError(() => props.fieldName)
|
||||||
|
const hasError = computed(() => !!fieldError.value)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DE.Cell :col-span="colSpan || 1">
|
<DE.Cell :col-span="colSpan || 1">
|
||||||
<DE.Label
|
<DE.Label
|
||||||
v-if="label !== ''"
|
v-if="label"
|
||||||
:label-for="fieldName"
|
:label-for="fieldName"
|
||||||
:is-required="isRequired && !isDisabled"
|
:is-required="isRequired && !isDisabled"
|
||||||
>
|
>
|
||||||
@@ -63,27 +68,53 @@ function handleInput(event: Event) {
|
|||||||
v-slot="{ componentField }"
|
v-slot="{ componentField }"
|
||||||
:name="fieldName"
|
:name="fieldName"
|
||||||
>
|
>
|
||||||
<FormItem :class="cn(`relative`,)">
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl class="relative">
|
||||||
<Input
|
<div class="relative w-full max-w-sm items-center">
|
||||||
:disabled="isDisabled"
|
<Input
|
||||||
v-bind="componentField"
|
:disabled="isDisabled"
|
||||||
:placeholder="placeholder"
|
v-bind="componentField"
|
||||||
:maxlength="maxLength"
|
:placeholder="placeholder"
|
||||||
:class="cn('focus:border-primary focus:ring-2 focus:ring-primary focus:ring-offset-0', props.class)"
|
:maxlength="maxLength"
|
||||||
autocomplete="off"
|
:class="cn(
|
||||||
aria-autocomplete="none"
|
'focus:border-primary focus:ring-2 focus:ring-primary focus:ring-offset-0',
|
||||||
autocorrect="off"
|
hasError && 'border-red-500 focus:border-red-500 focus:ring-red-500'
|
||||||
autocapitalize="off"
|
)"
|
||||||
spellcheck="false"
|
autocomplete="off"
|
||||||
@input="handleInput"
|
aria-autocomplete="none"
|
||||||
/>
|
autocorrect="off"
|
||||||
<p v-show="rightLabel" class="text-gray-400 absolute top-0 right-3">{{ rightLabel }}</p>
|
autocapitalize="off"
|
||||||
|
spellcheck="false"
|
||||||
|
@input="handleInput"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="suffixMsg"
|
||||||
|
class="absolute inset-y-0 end-0 flex items-center justify-center px-2 text-muted-foreground"
|
||||||
|
>
|
||||||
|
{{ suffixMsg }}
|
||||||
|
</span>
|
||||||
|
<Icon
|
||||||
|
v-if="iconName"
|
||||||
|
:name="iconName"
|
||||||
|
class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-500 dark:text-gray-400"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
v-show="rightLabel"
|
||||||
|
class="absolute right-3 top-0 text-gray-400"
|
||||||
|
>
|
||||||
|
{{ rightLabel }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
</DE.Field>
|
</DE.Field>
|
||||||
<p v-show="bottomLabel" class="text-gray-400 mt-1">{{ bottomLabel }}</p>
|
<p
|
||||||
|
v-show="bottomLabel"
|
||||||
|
class="text-gray-400"
|
||||||
|
>
|
||||||
|
{{ bottomLabel }}
|
||||||
|
</p>
|
||||||
</DE.Cell>
|
</DE.Cell>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SelectRoot } from 'radix-vue'
|
import { SelectRoot } from 'radix-vue'
|
||||||
import { watch } from 'vue'
|
import { watch, computed } from 'vue'
|
||||||
|
import { useFieldError } from 'vee-validate'
|
||||||
import SelectContent from '~/components/pub/ui/select/SelectContent.vue'
|
import SelectContent from '~/components/pub/ui/select/SelectContent.vue'
|
||||||
import SelectGroup from '~/components/pub/ui/select/SelectGroup.vue'
|
import SelectGroup from '~/components/pub/ui/select/SelectGroup.vue'
|
||||||
import SelectItem from '~/components/pub/ui/select/SelectItem.vue'
|
import SelectItem from '~/components/pub/ui/select/SelectItem.vue'
|
||||||
@@ -29,9 +30,14 @@ const props = defineProps<{
|
|||||||
isDisabled?: boolean
|
isDisabled?: boolean
|
||||||
autoWidth?: boolean
|
autoWidth?: boolean
|
||||||
autoFill?: boolean
|
autoFill?: boolean
|
||||||
|
id?: string
|
||||||
// otherPlacement sudah tidak digunakan, diganti dengan priority system di Item interface
|
// otherPlacement sudah tidak digunakan, diganti dengan priority system di Item interface
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
// Get error state from vee-validate if id is provided
|
||||||
|
const fieldError = props.id ? useFieldError(() => props.id!) : ref(null)
|
||||||
|
const hasError = computed(() => !!fieldError.value)
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:modelValue': [value: string]
|
'update:modelValue': [value: string]
|
||||||
}>()
|
}>()
|
||||||
@@ -121,6 +127,7 @@ watch(
|
|||||||
'cursor-not-allowed bg-gray-100 opacity-50': isDisabled,
|
'cursor-not-allowed bg-gray-100 opacity-50': isDisabled,
|
||||||
'bg-white text-black dark:bg-gray-800 dark:text-white': !isDisabled,
|
'bg-white text-black dark:bg-gray-800 dark:text-white': !isDisabled,
|
||||||
'w-full': !autoWidth,
|
'w-full': !autoWidth,
|
||||||
|
'border-red-500 focus:ring-red-500': hasError,
|
||||||
},
|
},
|
||||||
props.class,
|
props.class,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { type Base, genBase } from "./_base"
|
import { type Base, genBase } from './_base'
|
||||||
import { type Employee, genEmployee } from "./employee"
|
import { type Employee, genEmployee } from './employee'
|
||||||
import type { Specialist } from "./specialist"
|
import type { Specialist } from './specialist'
|
||||||
import type { Subspecialist } from "./subspecialist"
|
import type { Subspecialist } from './subspecialist'
|
||||||
|
|
||||||
export interface Doctor extends Base {
|
export interface Doctor extends Base {
|
||||||
employee_id: number
|
employee_id: number
|
||||||
|
|||||||
+14
-5
@@ -1,7 +1,7 @@
|
|||||||
import { type Base, genBase } from "./_base"
|
import { type Base, genBase } from './_base'
|
||||||
import type { PersonAddress } from "./person-address"
|
import type { PersonAddress } from './person-address'
|
||||||
import type { PersonContact } from "./person-contact"
|
import type { PersonContact } from './person-contact'
|
||||||
import type { PersonRelative } from "./person-relative"
|
import type { PersonRelative } from './person-relative'
|
||||||
import type { Ethnic } from './ethnic'
|
import type { Ethnic } from './ethnic'
|
||||||
import type { Language } from './language'
|
import type { Language } from './language'
|
||||||
import type { Regency } from './regency'
|
import type { Regency } from './regency'
|
||||||
@@ -43,6 +43,15 @@ export interface Person extends Base {
|
|||||||
export function genPerson(): Person {
|
export function genPerson(): Person {
|
||||||
return {
|
return {
|
||||||
...genBase(),
|
...genBase(),
|
||||||
name: '',
|
frontTitle: '[MOCK] dr. ',
|
||||||
|
name: 'Agus Iwan Setiawan',
|
||||||
|
endTitle: 'Sp.Bo',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseName(person: Person): string {
|
||||||
|
if (!person) return ''
|
||||||
|
const fullName = [person.frontTitle, person.name, person.endTitle].filter(Boolean).join(' ').trim()
|
||||||
|
|
||||||
|
return fullName
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const OperatorTeamSchema = z.object({
|
|||||||
surgeryDate: isoDateTime,
|
surgeryDate: isoDateTime,
|
||||||
actionDiagnosis: z.string().min(1),
|
actionDiagnosis: z.string().min(1),
|
||||||
|
|
||||||
postOpNurseId: z.number().int().optional().nullable(),
|
postSurgeryNurseId: z.number().int().optional().nullable(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const ProcedureSchema = z.object({
|
const ProcedureSchema = z.object({
|
||||||
@@ -73,7 +73,7 @@ const TissueNoteSchema = z.object({
|
|||||||
note: z.string().min(1),
|
note: z.string().min(1),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const ActionReportSchema = z.object({
|
export const TreatmentReportSchema = z.object({
|
||||||
operatorTeam: OperatorTeamSchema,
|
operatorTeam: OperatorTeamSchema,
|
||||||
procedures: z.array(ProcedureSchema).min(1),
|
procedures: z.array(ProcedureSchema).min(1),
|
||||||
|
|
||||||
@@ -86,4 +86,4 @@ export const ActionReportSchema = z.object({
|
|||||||
tissueNotes: z.array(TissueNoteSchema).optional(),
|
tissueNotes: z.array(TissueNoteSchema).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export type ActionReportFormData = z.infer<typeof ActionReportSchema>
|
export type TreatmentReportFormData = z.infer<typeof TreatmentReportSchema>
|
||||||
|
|||||||
Reference in New Issue
Block a user