diff --git a/Dockerfile b/Dockerfile index d02bc726..e930a32a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,33 +1,17 @@ -# Build Stage +# Build stage FROM node:20-alpine AS build-stage - -# Set the working directory inside the container WORKDIR /app -# Enable pnpm using corepack RUN corepack enable - -# Copy pnpm related files and package.json to leverage Docker layer caching COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile -# Install dependencies using pnpm -# Using --frozen-lockfile ensures consistent installations based on pnpm-lock.yaml -RUN --mount=type=cache,id=pnpm-store,target=/root/.pnpm-store pnpm install --frozen-lockfile - -# Copy the rest of the application files COPY . . +RUN pnpm generate -# Build the Vue.js application for production -RUN pnpm build +# Production stage +FROM nginx:stable-alpine +COPY --from=build-stage /app/.output/public /usr/share/nginx/html -# Production Stage -FROM nginx:stable-alpine AS production-stage - -# Copy the built Vue.js application from the build stage to Nginx's web root -COPY --from=build-stage /app/dist /usr/share/nginx/html - -# Expose port 80 for Nginx EXPOSE 80 - -# Command to run Nginx in the foreground CMD ["nginx", "-g", "daemon off;"] diff --git a/app/components/app/encounter/entry-form.vue b/app/components/app/encounter/entry-form.vue index 56e0383b..8fd21ea6 100644 --- a/app/components/app/encounter/entry-form.vue +++ b/app/components/app/encounter/entry-form.vue @@ -9,7 +9,9 @@ import DatepickerSingle from '~/components/pub/my-ui/datepicker/datepicker-singl import FileUpload from '~/components/pub/my-ui/form/file-field.vue' // Types -import { IntegrationEncounterSchema, type IntegrationEncounterFormData } from '~/schemas/integration-encounter.schema' +import { IntegrationEncounterSchema, type IntegrationEncounterFormData} from '~/schemas/integration-encounter.schema' +// import { IntegrationEncounterSchema, IntegrationEncounterSchemaWP} from '~/schemas/integration-encounter.schema' +// import type { IntegrationEncounterFormData, IntegrationEncounterWPFormData } from '~/schemas/integration-encounter.schema' import type { PatientEntity } from '~/models/patient' // Helpers @@ -20,9 +22,21 @@ import type { Doctor } from '~/models/doctor' // References import { paymentMethodCodes } from '~/const/key-val/common' +import { getValueLabelList as getEthnicOpts } from '~/services/ethnic.service' +import { getValueLabelList as getLanguageOpts } from '~/services/language.service' // App things import { genEncounter, type Encounter } from '~/models/encounter' +import PatientForm from '~/components/app/patient/entry-form.vue' +import PersonAddressEntryForm from '~/components/app/person-address/entry-form.vue' +import PersonAddressEntryFormRelative from '~/components/app/person-address/entry-form-relative.vue' +import PersonFamilyParentsForm from '~/components/app/person/family-parents-form.vue' +import type { PersonAddress } from '~/models/person-address' +import type { PersonRelative } from '~/models/person-relative' +import type { PersonContact } from '~/models/person-contact' +import { withBase } from '~/models/_base' +import type { Person } from '~/models/person' +import type { ExposedForm } from '~/types/form' // Props const props = defineProps<{ @@ -46,9 +60,12 @@ const props = defineProps<{ // Model const model = defineModel() model.value = genEncounter() +// const patientSource = ref<'new' | 'exists'>('exists') // Common preparation const defaultCBItems = [{ label: 'Pilih', value: '' }] +const formKey = ref(0) +// const withPatientStatus = ref(false) // Emit preparation const emit = defineEmits<{ @@ -61,10 +78,13 @@ const emit = defineEmits<{ const { handleSubmit, errors, defineField, meta } = useForm({ validationSchema: toTypedSchema(IntegrationEncounterSchema), }) +// const { handleSubmit: handleSubmitWP, errors: errorsWP, defineField: defineFieldWP, meta: metaWP } = useForm({ +// validationSchema: toTypedSchema(IntegrationEncounterSchemaWP), +// }) // Bind fields and extract attrs +const [patientSource] = defineField('patientSource') const [doctorCode, doctorCodeAttrs] = defineField('doctor_code') -const [unitCode, unitCodeAttrs] = defineField('unit_code') const [registerDate, registerDateAttrs] = defineField('registerDate') const [paymentMethodCode, paymentMethodCodeAttrs] = defineField('paymentMethod_code') const [patientCategory, patientCategoryAttrs] = defineField('patientCategory') @@ -72,10 +92,11 @@ const [cardNumber, cardNumberAttrs] = defineField('cardNumber') const [sepType, sepTypeAttrs] = defineField('sepType') const [sepNumber, sepNumberAttrs] = defineField('sepNumber') const [patientName, patientNameAttrs] = defineField('patientName') -const [nationalIdentity, nationalIdentityAttrs] = defineField('nationalIdentity') +const [residentIdentiyNumber, residentIdentiyNumberAttrs] = defineField('residentIdentiyNumber') const [medicalRecordNumber, medicalRecordNumberAttrs] = defineField('medicalRecordNumber') const [sepFile, sepFileAttrs] = defineField('sepFile') const [sippFile, sippFileAttrs] = defineField('sippFile') + const patientId = ref('') const sepReference = ref('') const sepControlDate = ref('') @@ -94,10 +115,14 @@ const debouncedSepNumber = refDebounced(sepNumber, 500) const debouncedCardNumber = refDebounced(cardNumber, 500) const sepFileReview = ref(null) const sippFileReview = ref(null) -const unitFullName = ref('') // Unit, specialist, subspecialist +const specialistFullName = ref('') // Unit, specialist, subspecialist const formRef = ref(null) // Expose submit method for parent component const paymentMethodItems = CB.recStrToItem(paymentMethodCodes) +if (!patientSource.value) { + patientSource.value = 'exists' +} + if (mode === 'add') { // Set default sepDate to current date in YYYY-MM-DD format const today = new Date() @@ -107,11 +132,32 @@ if (mode === 'add') { registerDate.value = `${year}-${month}-${day}` } + +// NEW PATIENT THINGS +const ethnicOptions = ref<{ value: string; label: string }[]>([]) +const languageOptions = ref<{ value: string; label: string }[]>([]) + +const personPatientForm = ref | null>(null) +const personAddressForm = ref | null>(null) +const personAddressRelativeForm = ref | null>(null) +const personContactForm = ref | null>(null) +const personEmergencyContactRelative = ref | null>(null) +const personFamilyForm = ref | null>(null) + +// const patientDetail = ref( +// withBase({ +// person: {} as Person, +// personAddresses: [], +// personContacts: [], +// personRelatives: [], +// }), +// ) + watch( () => props.selectedDoctor, (doctor) => { - unitFullName.value = doctor.subspecialist?.name ?? doctor.specialist?.name ?? doctor.unit?.name ?? 'tidak diketahui' - model.value!.unit_code = doctor.unit_code || '' + specialistFullName.value = doctor.subspecialist?.name ?? doctor.specialist?.name ?? doctor.unit?.name ?? 'tidak diketahui' + // model.value!.unit_code = doctor.unit_code || '' model.value!.specialist_code = doctor.specialist_code || '' model.value!.subspecialist_code = doctor.subspecialist_code || '' }, @@ -123,7 +169,7 @@ watch( (objects) => { if (objects && Object.keys(objects).length > 0) { patientName.value = objects?.patientName || '' - nationalIdentity.value = objects?.nationalIdentity || '' + residentIdentiyNumber.value = objects?.residentIdentiyNumber || '' medicalRecordNumber.value = objects?.medicalRecordNumber || '' doctorCode.value = objects?.doctorCode || '' patientCategory.value = objects?.patientCategory || '' @@ -151,7 +197,7 @@ watch( if (patient && Object.keys(patient).length > 0) { patientId.value = patient?.id ? String(patient.id) : '' patientName.value = patient?.person?.name || '' - nationalIdentity.value = patient?.person?.residentIdentityNumber || '' + residentIdentiyNumber.value = patient?.person?.residentIdentityNumber || '' medicalRecordNumber.value = patient?.number || '' } }, @@ -180,6 +226,14 @@ watch(debouncedCardNumber, (newValue) => { emit('event', 'member-changed', newValue) }) +onMounted(async () => { + const optsReq = { + 'page-no-limit': true, + } + ethnicOptions.value = await getEthnicOpts(optsReq, true) + languageOptions.value = await getLanguageOpts(optsReq, true) +}) + function onAddSep() { const formValues = { patientId: patientId.value || '', @@ -200,7 +254,7 @@ function onSearchSep() { } // Submit handler -const onSubmit = handleSubmit((values) => { +const onSubmit = handleSubmit(async(values) => { let payload: any = values if (props.mode === 'edit') { payload = { @@ -209,7 +263,32 @@ const onSubmit = handleSubmit((values) => { sippFileReview: sippFileReview.value, } } - emit('event', 'save', payload) + if (patientSource.value === 'exists') { + emit('event', 'save', payload) + } else { + const [patient, address, addressRelative, families, contacts, emergencyContact] = await Promise.all([ + personPatientForm.value?.validate(), + personAddressForm.value?.validate(), + personAddressRelativeForm.value?.validate(), + personFamilyForm.value?.validate(), + personContactForm.value?.validate(), + personEmergencyContactRelative.value?.validate(), + ]) + + const results = [patient, address, addressRelative, families, contacts, emergencyContact] + const allValid = results.every((r) => r?.valid) + if (!allValid) return Promise.reject('Form validation failed') + + emit('event', 'save', { + encounter: payload, + patient: { + person: patient?.values, + personAddresses: [address?.values, addressRelative?.values], + personContacts: contacts?.values.contacts, + personRelatives: families?.values.families, + }, + }) + } }) function openFile(path: string) { @@ -234,99 +313,229 @@ function submitForm() { } } +function requestPatient() { + patientSource.value = 'exists' + emit('event', 'search') +} + +function newPatient() { + patientSource.value = 'new' + emit('event', 'add') +} + defineExpose({ submitForm, }) diff --git a/app/components/app/patient/entry-form.vue b/app/components/app/patient/entry-form.vue index c2e187a5..34dd2a34 100644 --- a/app/components/app/patient/entry-form.vue +++ b/app/components/app/patient/entry-form.vue @@ -11,6 +11,7 @@ import { calculateAge } from '~/models/person' // components import * as DE from '~/components/pub/my-ui/doc-entry' import { InputBase, FileField as FileUpload } from '~/components/pub/my-ui/form' +import { Skeleton } from '~/components/pub/ui/skeleton' import { SelectBirthPlace } from '~/components/app/person/fields' import { InputName, @@ -60,10 +61,20 @@ interface PatientFormInput { interface Props { isReadonly: boolean + languageOptions?: { value: string; label: string }[] + ethnicOptions?: { value: string; label: string }[] initialValues?: PatientFormInput + mode?: 'add' | 'edit' + identityFileUrl?: string + familyCardFileUrl?: string } const props = defineProps() + +const emit = defineEmits<{ + (e: 'preview', url: string): void +}>() + const formSchema = toTypedSchema(PatientSchema) const { values, resetForm, setValues, setFieldValue, validate, setFieldError } = useForm({ @@ -208,6 +219,7 @@ watch( label="Suku" placeholder="Pilih suku bangsa" :is-disabled="isReadonly || values.nationality !== 'WNI'" + :items="ethnicOptions || []" /> - - +
+ + +
+ Dokumen Tersimpan +
+ Dokumen KTP + + +
+
+ + Lihat Dokumen +
+
+ + No Data +
+
+
+
+
+
+ + +
+ Dokumen Tersimpan +
+ Dokumen KK + + +
+
+ + Lihat Dokumen +
+
+ + No Data +
+
+
+
+
diff --git a/app/components/app/patient/fields/select-ethnicity.vue b/app/components/app/patient/fields/select-ethnicity.vue index 43ef6cba..73645441 100644 --- a/app/components/app/patient/fields/select-ethnicity.vue +++ b/app/components/app/patient/fields/select-ethnicity.vue @@ -6,6 +6,7 @@ import { cn } from '~/lib/utils' import * as DE from '~/components/pub/my-ui/doc-entry' const props = defineProps<{ + items: { value: string; label: string }[] fieldName?: string label?: string placeholder?: string @@ -28,23 +29,23 @@ const { labelClass, } = props -const ethnicOptions = [ - { label: 'Tidak diketahui', value: 'unknown', priority: 1 }, - { label: 'Jawa', value: 'jawa' }, - { label: 'Sunda', value: 'sunda' }, - { label: 'Batak', value: 'batak' }, - { label: 'Betawi', value: 'betawi' }, - { label: 'Minangkabau', value: 'minangkabau' }, - { label: 'Bugis', value: 'bugis' }, - { label: 'Madura', value: 'madura' }, - { label: 'Banjar', value: 'banjar' }, - { label: 'Bali', value: 'bali' }, - { label: 'Dayak', value: 'dayak' }, - { label: 'Aceh', value: 'aceh' }, - { label: 'Sasak', value: 'sasak' }, - { label: 'Papua', value: 'papua' }, - { label: 'Lainnya', value: 'lainnya', priority: -100 }, -] +// const ethnicOptions = [ +// { label: 'Tidak diketahui', value: 'unknown', priority: 1 }, +// { label: 'Jawa', value: 'jawa' }, +// { label: 'Sunda', value: 'sunda' }, +// { label: 'Batak', value: 'batak' }, +// { label: 'Betawi', value: 'betawi' }, +// { label: 'Minangkabau', value: 'minangkabau' }, +// { label: 'Bugis', value: 'bugis' }, +// { label: 'Madura', value: 'madura' }, +// { label: 'Banjar', value: 'banjar' }, +// { label: 'Bali', value: 'bali' }, +// { label: 'Dayak', value: 'dayak' }, +// { label: 'Aceh', value: 'aceh' }, +// { label: 'Sasak', value: 'sasak' }, +// { label: 'Papua', value: 'papua' }, +// { label: 'Lainnya', value: 'lainnya', priority: -100 }, +// ]