Feat: add UI Rehab Medik > Proses > Resume

This commit is contained in:
hasyim_kai
2025-10-29 14:57:19 +07:00
parent 9b383a5437
commit fc308809b8
16 changed files with 1224 additions and 2 deletions
@@ -0,0 +1,72 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
import { cn, mapToComboboxOptList } from '~/lib/utils'
import { occupationCodes } from '~/lib/constants'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName?: string
label?: string
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
labelClass?: string
isRequired?: boolean
}>()
const {
fieldName = 'job',
label,
placeholder,
errors,
class: containerClass,
fieldGroupClass,
labelClass,
} = props
const arrangementTypeOpts = [
{ label: 'KRS', value: "krs" },
{ label: 'MRS', value: "mrs" },
{ label: 'Pindah IGD', value: "pindahIgd" },
{ label: 'Rujuk', value: "rujuk" },
{ label: 'Rujuk Balik', value: "rujukBalik" },
{ label: 'Meninggal', value: "meninggal" },
{ label: 'Lain Lain', value: "other" },
]
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
v-show="label"
:label-for="fieldName"
:class="cn('select-field-label', labelClass)"
:is-required="isRequired"
>
{{ label }}
</DE.Label>
<DE.Field
:id="fieldName"
:errors="errors"
:class="cn('select-field-wrapper')"
>
<FormField :name="fieldName" v-slot="{ componentField, value }">
<FormItem>
<FormControl>
<Select
v-bind="componentField"
:model-value="value"
:items="arrangementTypeOpts"
:defaultValue='arrangementTypeOpts[0]?.value'
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,121 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import { differenceInDays, differenceInMonths, differenceInYears, parseISO } from 'date-fns'
import { Input } from '~/components/pub/ui/input'
import { cn } from '~/lib/utils'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName?: string
label?: string
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
labelClass?: string
isRequired?: boolean
isDisabled?: boolean
isWithTime?: boolean
}>()
const {
fieldName = 'birthDate',
label = 'Tanggal Lahir',
placeholder = 'Pilih tanggal lahir',
errors,
class: containerClass,
fieldGroupClass,
labelClass,
isWithTime = false,
} = props
// Reactive variables for age calculation
const patientAge = ref<string>('Masukkan tanggal lahir')
// Function to calculate age with years, months, and days
function calculateAge(birthDate: string | Date | undefined): string {
if (!birthDate) {
return 'Masukkan tanggal lahir'
}
try {
let dateObj: Date
if (typeof birthDate === 'string') {
dateObj = parseISO(birthDate)
} else {
dateObj = birthDate
}
const today = new Date()
// Calculate years, months, and days
const totalYears = differenceInYears(today, dateObj)
// Calculate remaining months after years
const yearsPassed = new Date(dateObj)
yearsPassed.setFullYear(yearsPassed.getFullYear() + totalYears)
const remainingMonths = differenceInMonths(today, yearsPassed)
// Calculate remaining days after years and months
const monthsPassed = new Date(yearsPassed)
monthsPassed.setMonth(monthsPassed.getMonth() + remainingMonths)
const remainingDays = differenceInDays(today, monthsPassed)
// Format the result
const parts = []
if (totalYears > 0) parts.push(`${totalYears} Tahun`)
if (remainingMonths > 0) parts.push(`${remainingMonths} Bulan`)
if (remainingDays > 0) parts.push(`${remainingDays} Hari`)
return parts.length > 0 ? parts.join(' ') : '0 Hari'
} catch {
return 'Masukkan tanggal lahir'
}
}
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
:label-for="fieldName"
:class="cn('select-field-label', labelClass)"
:is-required="isRequired && !isDisabled"
>
{{ label }}
</DE.Label>
<DE.Field
:id="fieldName"
:errors="errors"
:class="cn('select-field-wrapper')"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Input
id="birthDate"
:type="isWithTime ? 'datetime-local' : 'date'"
min="1900-01-01"
:max="new Date().toISOString().split('T')[0]"
v-bind="componentField"
:placeholder="placeholder"
:disabled="isDisabled"
@update:model-value="
(value: string | number) => {
const dateStr = typeof value === 'number' ? String(value) : value
patientAge = calculateAge(dateStr)
}
"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,71 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
import { cn, mapToComboboxOptList } from '~/lib/utils'
import { occupationCodes } from '~/lib/constants'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName?: string
label?: string
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
labelClass?: string
isRequired?: boolean
}>()
const {
fieldName = 'job',
label,
placeholder,
errors,
class: containerClass,
fieldGroupClass,
labelClass,
} = props
// Generate job options from constants, sama seperti pola genderCodes
const jobOptions = mapToComboboxOptList(occupationCodes)
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
v-show="label"
:label-for="fieldName"
:class="cn('select-field-label', labelClass)"
: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>
<Combobox
class="focus:ring-0 focus:ring-offset-0"
:id="fieldName"
v-bind="componentField"
:items="jobOptions"
:placeholder="placeholder"
search-placeholder="Cari..."
empty-message="Data tidak ditemukan"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,70 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
import { cn, mapToComboboxOptList } from '~/lib/utils'
import { occupationCodes } from '~/lib/constants'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName?: string
label?: string
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
labelClass?: string
isRequired?: boolean
}>()
const {
fieldName = 'job',
label = 'Pekerjaan',
placeholder = 'Pilih pekerjaan',
errors,
class: containerClass,
fieldGroupClass,
labelClass,
} = props
// Generate job options from constants, sama seperti pola genderCodes
const jobOptions = mapToComboboxOptList(occupationCodes)
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
:label-for="fieldName"
:class="cn('select-field-label', labelClass)"
: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>
<Combobox
class="focus:ring-0 focus:ring-offset-0"
:id="fieldName"
v-bind="componentField"
:items="jobOptions"
:placeholder="placeholder"
search-placeholder="Cari..."
empty-message="Data tidak ditemukan"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,70 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
import { cn, mapToComboboxOptList } from '~/lib/utils'
import { occupationCodes } from '~/lib/constants'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName?: string
label?: string
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
labelClass?: string
isRequired?: boolean
}>()
const {
fieldName = 'job',
label = 'Pekerjaan',
placeholder = 'Pilih pekerjaan',
errors,
class: containerClass,
fieldGroupClass,
labelClass,
} = props
// Generate job options from constants, sama seperti pola genderCodes
const jobOptions = mapToComboboxOptList(occupationCodes)
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
:label-for="fieldName"
:class="cn('select-field-label', labelClass)"
: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>
<Combobox
class="focus:ring-0 focus:ring-offset-0"
:id="fieldName"
v-bind="componentField"
:items="jobOptions"
:placeholder="placeholder"
search-placeholder="Cari..."
empty-message="Data tidak ditemukan"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,71 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
import { cn, mapToComboboxOptList } from '~/lib/utils'
import { occupationCodes } from '~/lib/constants'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName?: string
label?: string
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
labelClass?: string
isRequired?: boolean
}>()
const {
fieldName = 'job',
label,
placeholder,
errors,
class: containerClass,
fieldGroupClass,
labelClass,
} = props
// Generate job options from constants, sama seperti pola genderCodes
const jobOptions = mapToComboboxOptList(occupationCodes)
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
v-show="label"
:label-for="fieldName"
:class="cn('select-field-label', labelClass)"
: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>
<Combobox
class="focus:ring-0 focus:ring-offset-0"
:id="fieldName"
v-bind="componentField"
:items="jobOptions"
:placeholder="placeholder"
search-placeholder="Cari..."
empty-message="Data tidak ditemukan"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
+185
View File
@@ -0,0 +1,185 @@
<script setup lang="ts">
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 SelectDate from './_common/select-date.vue'
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 SelectSecondaryDiagnosis from './_common/select-secondary-diagnosis.vue'
import SelectPrimaryDiagnosis from './_common/select-primary-diagnosis.vue'
import SelectArrangement from './_common/select-arrangement.vue'
import type { ResumeArrangementType } from '~/schemas/resume.schema'
import SelectFaskes from './_common/select-faskes.vue'
import SelectDeathCause from './_common/select-death-cause.vue'
const props = defineProps<{
schema: any
initialValues?: any
resumeArrangementType: ResumeArrangementType
errors?: FormErrors
}>()
const formSchema = toTypedSchema(props.schema)
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>
<template>
<Form ref="formRef" v-slot="{ values }" as="" keep-values :validation-schema="formSchema" :validate-on-mount="false"
validation-mode="onSubmit" :initial-values="initialValues ? initialValues : {}">
<h1 class="mb-3 text-base font-medium">Pemeriksaan Pasien</h1>
<DE.Block :col-count="3" :cell-flex="false">
<SelectDate field-name="inDate" label="Tanggal Masuk" :errors="errors" :is-disabled="true" />
<SelectDate field-name="outDate" label="Tanggal Keluar" :errors="errors" :is-disabled="true" />
</DE.Block>
<DE.Block :col-count="3" :cell-flex="false">
<TextAreaInput field-name="anamnesis" label="Anamnesis" placeholder="Anamnesis" :errors="errors" />
<TextAreaInput field-name="physicalCheckup" label="Pemeriksaan Fisik" placeholder="Pemeriksaan Fisik"
:errors="errors" />
<TextAreaInput field-name="supplementCheckup" label="Pemeriksaan Penunjang" placeholder="Pemeriksaan Penunjang"
:errors="errors" />
</DE.Block>
<Separator class="my-4" />
<!-- DIAGNOSIS -->
<h1 class="mb-3 text-base font-medium">Diagnosis</h1>
<DE.Block class="w-3/5 space-y-2" :cell-flex="false">
<SelectPrimaryDiagnosis
field-name="primaryDiagnosis"
label="Diagnosis Utama"
placeholder="Diagnosis Utama"
:errors="errors" is-required />
<div class="w-full rounded-md border bg-gray-50/50 p-4 shadow-sm dark:bg-neutral-950">
<h1 class="mb-3 font-medium">Diagnosis Sekunder</h1>
<FieldArray v-slot="{ fields, push, remove }" name="secondaryDiagnosis">
<div v-for="(field, idx) in fields" :key="idx" class="flex items-center gap-3 mb-3">
<SelectSecondaryDiagnosis
:field-name="`secondaryDiagnosis[${idx}]`"
:errors="errors"
/>
<Button type="button" variant="destructive" size="sm" @click="remove(idx)">
<Icon name="i-lucide-trash-2" class="h-4 w-4" />
</Button>
</div>
<Button type="button" variant="outline"
class="w-full rounded-md border border-primary bg-white px-4 py-2 text-primary hover:border-primary hover:bg-primary hover:text-white sm:w-auto sm:text-sm"
@click="push(``)">
<Icon name="i-lucide-plus" class="mr-2 h-4 w-4 align-middle transition-colors" />
Tambah Diagnosis
</Button>
</FieldArray>
</div>
</DE.Block>
<Separator class="my-4" />
<!-- DIAGNOSIS -->
<h1 class="mb-3 text-base font-medium">Tindakan Operatif/Non Operatif</h1>
<DE.Block class="w-3/5 space-y-2" :cell-flex="false">
<SelectPrimaryDiagnosis
field-name="primaryOperativeNonOperativeAct"
label="Tindakan Operatif Utama"
placeholder="Tindakan Operatif Utama"
:errors="errors" is-required />
<div class="w-full rounded-md border bg-gray-50/50 p-4 shadow-sm dark:bg-neutral-950">
<h1 class="mb-3 font-medium">Tindakan / Prosedur</h1>
<FieldArray v-slot="{ fields, push, remove }" name="secondaryOperativeNonOperativeAct">
<div v-for="(field, idx) in fields" :key="idx" class="flex items-center gap-3 mb-3">
<SelectSecondaryDiagnosis
:field-name="`secondaryOperativeNonOperativeAct[${idx}]`"
:errors="errors"
/>
<Button type="button" variant="destructive" size="sm" @click="remove(idx)">
<Icon name="i-lucide-trash-2" class="h-4 w-4" />
</Button>
</div>
<Button type="button" variant="outline"
class="w-full rounded-md border border-primary bg-white px-4 py-2 text-primary hover:border-primary hover:bg-primary hover:text-white sm:w-auto sm:text-sm"
@click="push(``)">
<Icon name="i-lucide-plus" class="mr-2 h-4 w-4 align-middle transition-colors" />
Tambah Tindakan / Prosedur
</Button>
</FieldArray>
</div>
</DE.Block>
<Separator class="my-4" />
<DE.Block class="w-3/5" :cell-flex="false">
<TextAreaInput field-name="medikamentosa" label="Medikamentosa" placeholder="Medikamentosa" :errors="errors" />
</DE.Block>
<Separator class="my-4" />
<h1 class="mb-3 font-medium">Penatalaksanaan</h1>
<DE.Block :col-count="3" :cell-flex="false">
<SelectArrangement field-name="arrangement" label="Lanjutan Penatalaksanaan" placeholder="Pilih Arrangement" :errors="errors" />
<SelectFaskes
v-show="resumeArrangementType === `rujuk` || resumeArrangementType === `rujukBalik` "
field-name="faskes"
label="Faskes"
placeholder="Pilih Faskes"
:errors="errors"
/>
<InputBase
v-show="resumeArrangementType === `rujuk` || resumeArrangementType === `rujukBalik` "
field-name="clinic"
label="Klinik"
placeholder="Masukkan Klinik"
:errors="errors"
/>
<SelectDate
v-show="resumeArrangementType === `meninggal`"
field-name="deathDate"
label="Jam Tanggal Meninggal"
:errors="errors"
:is-with-time="true" />
<DE.Cell class="mt-2" :col-span="2" v-show="resumeArrangementType === `meninggal`">
<div class="w-full rounded-md border bg-gray-50/50 p-4 shadow-sm dark:bg-neutral-950">
<h1 class="mb-3 font-medium">Sebab Meninggal</h1>
<FieldArray v-slot="{ fields, push, remove }" name="deathCause">
<div v-for="(field, idx) in fields" :key="idx" class="flex items-center gap-3 mb-3">
<SelectDeathCause
:field-name="`deathCause[${idx}]`"
:errors="errors"
/>
<Button type="button" variant="destructive" size="sm" @click="remove(idx)">
<Icon name="i-lucide-trash-2" class="h-4 w-4" />
</Button>
</div>
<Button type="button" variant="outline"
class="w-full rounded-md border border-primary bg-white px-4 py-2 text-primary hover:border-primary hover:bg-primary hover:text-white sm:w-auto sm:text-sm"
@click="push(``)">
<Icon name="i-lucide-plus" class="mr-2 h-4 w-4 align-middle transition-colors" />
Tambah Sebab Meninggal
</Button>
</FieldArray>
</div>
</DE.Cell>
<TextAreaInput
v-show="resumeArrangementType === `other`"
field-name="keterangan"
label="Keterangan"
placeholder="Keterangan"
:errors="errors" />
</DE.Block>
</Form>
</template>
+93
View File
@@ -0,0 +1,93 @@
import type { Config } from '~/components/pub/my-ui/data-table'
import type { Patient } from '~/models/patient'
import { defineAsyncComponent } from 'vue'
import { educationCodes, genderCodes } from '~/lib/constants'
import { calculateAge } from '~/lib/utils'
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue'))
export const config: Config = {
cols: [{}, {}, {}, {}, {width: 3},],
headers: [
[
{ label: 'Tgl Rencana Kontrol' },
{ label: 'Spesialis/Sub Spesialis' },
{ label: 'DPJP' },
{ label: 'Status SEP' },
{ label: 'Action' },
],
],
keys: ['birth_date', 'number', 'person.name', 'birth_date', 'action'],
delKeyNames: [
{ key: 'code', label: 'Kode' },
{ key: 'name', label: 'Nama' },
],
parses: {
patientId: (rec: unknown): unknown => {
const patient = rec as Patient
return patient.number
},
identity_number: (rec: unknown): unknown => {
const { person } = rec as Patient
if (person.nationality == 'WNA') {
return person.passportNumber
}
return person.residentIdentityNumber || '-'
},
birth_date: (rec: unknown): unknown => {
const { person } = rec as Patient
if (typeof person.birthDate == 'object' && person.birthDate) {
return (person.birthDate as Date).toLocaleDateString('id-ID')
} else if (typeof person.birthDate == 'string') {
return (person.birthDate as string).substring(0, 10)
}
return person.birthDate
},
patient_age: (rec: unknown): unknown => {
const { person } = rec as Patient
return calculateAge(person.birthDate)
},
gender: (rec: unknown): unknown => {
const { person } = rec as Patient
if (typeof person.gender_code == 'number' && person.gender_code >= 0) {
return person.gender_code
} else if (typeof person.gender_code === 'string' && person.gender_code) {
return genderCodes[person.gender_code] || '-'
}
return '-'
},
education: (rec: unknown): unknown => {
const { person } = rec as Patient
if (typeof person.education_code == 'number' && person.education_code >= 0) {
return person.education_code
} else if (typeof person.education_code === 'string' && person.education_code) {
return educationCodes[person.education_code] || '-'
}
return '-'
},
},
components: {
action(rec, idx) {
return {
idx,
rec: rec as object,
component: action,
}
},
},
htmls: {
patient_address(_rec) {
return '-'
},
},
}
+43
View File
@@ -0,0 +1,43 @@
<script setup lang="ts">
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
import { config } from './list.cfg'
import DetailRow from '~/components/pub/my-ui/form/view/detail-row.vue';
interface Props {
data: any
}
defineProps<Props>()
</script>
<template>
<article class="space-y-4">
<h1 class="font-medium text-[0.89rem]">Pemeriksaan Pasien</h1>
<div class="space-y-1">
<DetailRow label="Tanggal Masuk">{{ new Date().toLocaleDateString('id-ID') }}</DetailRow>
<DetailRow label="Tanggal Keluar">{{ new Date().toLocaleDateString('id-ID') }}</DetailRow>
<DetailRow label="Anamnesis">{{ `-` }}</DetailRow>
<DetailRow label="Pemeriksaan Fisik">{{ `-` }}</DetailRow>
<DetailRow label="Pemeriksaan Penunjang">{{ `-` }}</DetailRow>
</div>
<Separator class="" />
<h1 class="font-medium text-[0.89rem]">Diagnosis</h1>
<div class="space-y-1">
<DetailRow label="Diagnosis Utama">{{ `-` }}</DetailRow>
<DetailRow label="Diagnosis Sekunder">{{ `-` }}</DetailRow>
</div>
<Separator class="" />
<h1 class="font-medium text-[0.89rem]">Tindakan Operatif/Non Operatif</h1>
<div class="space-y-1">
<DetailRow label="Tindakan Operatif Utama">{{ `-` }}</DetailRow>
<DetailRow label="Tindakan /Prosedur">{{ `-` }}</DetailRow>
<DetailRow label="Medikamentosa">{{ `-` }}</DetailRow>
</div>
<Separator class="" />
<h1 class="font-medium text-[0.89rem]">Penatalaksanaan</h1>
<div class="space-y-1">
<DetailRow label="Lanjutan Penatalaksanaan">{{ `-` }}</DetailRow>
</div>
</article>
</template>
+2 -1
View File
@@ -16,6 +16,7 @@ import EarlyMedicalAssesmentList from '~/components/content/soapi/entry.vue'
import EarlyMedicalRehabList from '~/components/content/soapi/entry.vue'
import PrescriptionList from '~/components/content/prescription/list.vue'
import Consultation from '~/components/content/consultation/list.vue'
import ResumeList from '~/components/content/resume/list.vue'
const route = useRoute()
const router = useRouter()
@@ -58,7 +59,7 @@ const tabs: TabItem[] = [
{ value: 'medical-action', label: 'Order Ruang Tindakan' },
{ value: 'mcu-result', label: 'Hasil Penunjang' },
{ value: 'consultation', label: 'Konsultasi', component: Consultation, props: { encounter: data } },
{ value: 'resume', label: 'Resume' },
{ value: 'resume', label: 'Resume', component: ResumeList, props: { encounter: data } },
{ value: 'control', label: 'Surat Kontrol' },
{ value: 'screening', label: 'Skrinning MPP' },
{ value: 'supporting-document', label: 'Upload Dokumen Pendukung' },
+104
View File
@@ -0,0 +1,104 @@
<script setup lang="ts">
import type { ExposedForm } from '~/types/form';
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
import { ResumeSchema } from '~/schemas/resume.schema';
import Confirmation from '~/components/pub/my-ui/confirmation/confirmation.vue'
// #region Props & Emits
const props = defineProps<{
callbackUrl?: string
}>()
// form related state
const personPatientForm = ref<ExposedForm<any> | null>(null)
// #endregion
// #region State & Computed
const router = useRouter()
const isConfirmationOpen = ref(false)
// #endregion
// #region Lifecycle Hooks
// #endregion
// #region Functions
function goBack() {
router.go(-1)
}
async function handleConfirmAdd() {
// handleActionClick('submit')
console.log(`tersubmit wak`)
}
function handleCancelAdd() {
isConfirmationOpen.value = false
}
// #endregion region
// #region Utilities & event handlers
async function handleActionClick(eventType: string) {
if (eventType === 'submit') {
isConfirmationOpen.value = true
// const patient: Patient = await composeFormData()
// let createdPatientId = 0
// const response = await handleActionSave(
// patient,
// () => {},
// () => {},
// toast,
// )
// const data = (response?.body?.data ?? null) as PatientBase | null
// if (!data) return
// createdPatientId = data.id
// If has callback provided redirect to callback with patientData
// if (props.callbackUrl) {
// await navigateTo(props.callbackUrl + '?patient-id=' + patient.id)
// return
// }
// Navigate to patient list or show success message
// await navigateTo('/outpatient/encounter')
// return
}
if (eventType === 'back') {
if (props.callbackUrl) {
await navigateTo(props.callbackUrl)
return
}
goBack()
// handleCancelForm()
}
}
// #endregion
// #region Watchers
// #endregion
</script>
<template>
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg font-semibold xl:text-xl">Tambah Resume</div>
<AppResumeAdd
ref="personPatientForm"
:schema="ResumeSchema"
:resume-arrangement-type="personPatientForm?.values.arrangement" />
<div class="my-2 flex justify-end py-2">
<Action :enable-draft="false"
@click="handleActionClick"/>
</div>
<Confirmation
v-model:open="isConfirmationOpen"
title="Simpan Data"
message="Apakah Anda yakin ingin menyimpan data ini?"
confirm-text="Simpan"
@confirm="handleConfirmAdd"
@cancel="handleCancelAdd"
/>
</template>
+151
View File
@@ -0,0 +1,151 @@
<script setup lang="ts">
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
import type { Summary } from '~/components/pub/my-ui/summary-card/type'
// #region Imports
import { Calendar, Hospital, UserCheck, UsersRound } from 'lucide-vue-next'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
import { ActionEvents } from '~/components/pub/my-ui/data/types'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import SummaryCard from '~/components/pub/my-ui/summary-card/summary-card.vue'
import { usePaginatedList } from '~/composables/usePaginatedList'
import { getPatients, removePatient } from '~/services/patient.service'
import DetailRow from '~/components/pub/my-ui/form/view/detail-row.vue'
// #endregion
// #region State
const { data, isLoading, paginationMeta, searchInput, handlePageChange, handleSearch, fetchData } = usePaginatedList({
fetchFn: (params) => getPatients({ ...params, includes: ['person', 'person-Addresses'] }),
entityName: 'patient',
})
const refSearchNav: RefSearchNav = {
onClick: () => {
// open filter modal
},
onInput: (val: string) => {
searchInput.value = val
},
onClear: () => {
searchInput.value = ''
},
}
const formType = ref<`a` | `b`>(`a`)
const isRecordConfirmationOpen = ref(false)
const summaryLoading = ref(false)
const recId = ref<number>(0)
const recAction = ref<string>('')
const recItem = ref<any>(null)
const headerPrep: HeaderPrep = {
title: "Resume",
icon: 'i-lucide-newspaper',
addNav: {
label: "Resume",
onClick: () => navigateTo('/resume/add'),
},
}
// #endregion
// #region Lifecycle Hooks
onMounted(() => {
getPatientSummary()
})
// #endregion
// #region Functions
async function getPatientSummary() {
try {
summaryLoading.value = true
await new Promise((resolve) => setTimeout(resolve, 500))
} catch (error) {
console.error('Error fetching patient summary:', error)
} finally {
summaryLoading.value = false
}
}
function handleFormScreening(key: string) {
switch (key) {
case 'form-a':
navigateTo("/screening-mpp/add/a")
break;
case 'preview-form-a':
navigateTo('https://google.com', { external: true, open: { target: '_blank' } });
break;
case 'form-b':
navigateTo("/screening-mpp/add/b")
break;
case 'preview-form-b':
navigateTo('https://google.com', { external: true, open: { target: '_blank' } });
break;
default:
break;
}
}
// #endregion
// #region Provide
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
// #endregion
// #region Watchers
watch([recId, recAction], () => {
switch (recAction.value) {
case ActionEvents.showDetail:
navigateTo({
name: 'outpatient-encounter-id',
params: { id: recId.value },
})
break
case ActionEvents.showEdit:
// TODO: Handle edit action
// isFormEntryDialogOpen.value = true
navigateTo({
name: 'outpatient-encounter-id-edit',
params: { id: recId.value },
})
break
case ActionEvents.showConfirmDelete:
// Trigger confirmation modal open
isRecordConfirmationOpen.value = true
break
}
})
// #endregion
</script>
<template>
<Header :prep="{ ...headerPrep }" />
<AppResumeList />
<!-- Disable dulu, ayahab kalo diminta beneran -->
<!-- <div class="my-4 flex flex-1 flex-col gap-4 md:gap-8">
<div class="grid gap-4 md:grid-cols-2 md:gap-8 lg:grid-cols-4">
<template v-if="summaryLoading">
<SummaryCard
v-for="n in 4"
:key="n"
is-skeleton
/>
</template>
<template v-else>
<SummaryCard
v-for="card in summaryData"
:key="card.title"
:stat="card"
/>
</template>
</div>
</div>
-->
</template>
@@ -0,0 +1,86 @@
<script setup lang="ts">
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 { cn } from '~/lib/utils'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName: string
placeholder: string
label: string
errors?: FormErrors
class?: string
colSpan?: number
numericOnly?: boolean
maxLength?: number
isRequired?: boolean
isDisabled?: boolean
}>()
function handleInput(event: Event) {
const target = event.target as HTMLInputElement
let value = target.value
// Filter numeric only jika diperlukan
if (props.numericOnly) {
value = value.replace(/\D/g, '')
}
// Batasi panjang maksimal jika diperlukan
if (props.maxLength && value.length > props.maxLength) {
value = value.slice(0, props.maxLength)
}
// Update value jika ada perubahan
if (target.value !== value) {
target.value = value
// Trigger input event untuk update form
target.dispatchEvent(new Event('input', { bubbles: true }))
}
}
</script>
<template>
<DE.Cell :col-span="colSpan || 1">
<DE.Label
class="mb-1 font-medium"
v-if="label !== ''"
:label-for="fieldName"
:is-required="isRequired && !isDisabled"
>
{{ label }}
</DE.Label>
<DE.Field
:id="fieldName"
:errors="errors"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Textarea
:disabled="isDisabled"
v-bind="componentField"
:placeholder="placeholder"
:maxlength="maxLength"
:class="cn('focus:border-primary focus:ring-2 focus:ring-primary focus:ring-offset-0')"
autocomplete="off"
aria-autocomplete="none"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
@input="handleInput"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -1,5 +1,6 @@
<script setup lang="ts">
const props = defineProps<{
enableDraft?: boolean
smallMode?: boolean
defaultClass?: string
class?: string
@@ -28,7 +29,7 @@ function onClick(type: ClickType) {
Back
</Button>
</div>
<div>
<div v-show="props.enableDraft">
<Button variant="secondary" type="button" @click="onClick('draft')">
<Icon name="i-lucide-file" />
Draft
+41
View File
@@ -0,0 +1,41 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Resume',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => `${route.meta.title}`, // backtick to avoid the ts-plugin(2322) warning
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/rehab/encounter']
const { checkRole, hasCreateAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
// if (!hasAccess) {
// throw createError({
// statusCode: 403,
// statusMessage: 'Access denied',
// })
// }
// Define permission-based computed properties
const canCreate = true // hasCreateAccess(roleAccess)
</script>
<template>
<div v-if="canCreate">
<ContentResumeAdd />
</div>
<Error v-else :status-code="403" />
</template>
+42
View File
@@ -0,0 +1,42 @@
import { z } from 'zod'
import type { CreateDto } from '~/models/consultation'
export type ResumeArrangementType = "krs" | "mrs" | "pindahIgd" | "rujuk" | "rujukBalik" | "meninggal" | "other"
const ResumeSchema = z.object({
inDate: z.string({ required_error: 'Tanggal harus diisi' }),
outDate: z.string({ required_error: 'Tanggal harus diisi' }),
anamnesis: z.number({ required_error: 'Anamnesis harus diisi' })
.min(1, 'Anamnesis minimum 1 karakter')
.max(2048, 'Anamnesis maksimum 2048 karakter'),
physicalCheckup: z.string({ required_error: 'Uraian harus diisi' })
.min(1, 'Uraian minimum 1 karakter')
.max(2048, 'Uraian maksimum 2048 karakter'),
supplementCheckup: z.string({ required_error: 'Uraian harus diisi' })
.min(1, 'Uraian minimum 1 karakter')
.max(2048, 'Uraian maksimum 2048 karakter'),
primaryDiagnosis: z.string({ required_error: 'Diagnosis harus diisi' }),
secondaryDiagnosis: z.array(z.string()).optional().default([]),
primaryOperativeNonOperativeAct: z.string({ required_error: 'Diagnosis harus diisi' }),
secondaryOperativeNonOperativeAct: z.array(z.string()).optional().default([]),
medikamentosa: z.string({ required_error: 'Uraian harus diisi' })
.min(1, 'Uraian minimum 1 karakter')
.max(2048, 'Uraian maksimum 2048 karakter'),
arrangement: z.custom<ResumeArrangementType>().default("krs"),
faskes: z.string({ required_error: 'Faskes harus diisi' }).optional(),
clinic: z.string({ required_error: 'Klinik harus diisi' }).optional(),
deathDate: z.string({ required_error: 'Tanggal harus diisi' }).optional(),
deathCause: z.array(z.string()).optional().default([]),
keterangan: z.string({ required_error: 'Uraian harus diisi' })
.min(1, 'Uraian minimum 1 karakter')
.max(2048, 'Uraian maksimum 2048 karakter')
.optional(),
})
type ResumeFormData = z.infer<typeof ResumeSchema> & (CreateDto)
export { ResumeSchema }
export type { ResumeFormData }