Merge branch 'dev' of github.com:dikstub-rssa/simrs-fe into dev

This commit is contained in:
2025-12-15 15:21:30 +07:00
26 changed files with 10135 additions and 7230 deletions

No files matched your search

+6 -22
View File
@@ -1,33 +1,17 @@
# Build Stage # Build stage
FROM node:20-alpine AS build-stage FROM node:20-alpine AS build-stage
# Set the working directory inside the container
WORKDIR /app WORKDIR /app
# Enable pnpm using corepack
RUN corepack enable RUN corepack enable
# Copy pnpm related files and package.json to leverage Docker layer caching
COPY package.json pnpm-lock.yaml ./ 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 . . COPY . .
RUN pnpm generate
# Build the Vue.js application for production # Production stage
RUN pnpm build 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 EXPOSE 80
# Command to run Nginx in the foreground
CMD ["nginx", "-g", "daemon off;"] CMD ["nginx", "-g", "daemon off;"]
File diff suppressed because it is too large Load diff
+119 -16
View File
@@ -11,6 +11,7 @@ import { calculateAge } from '~/models/person'
// components // components
import * as DE from '~/components/pub/my-ui/doc-entry' import * as DE from '~/components/pub/my-ui/doc-entry'
import { InputBase, FileField as FileUpload } from '~/components/pub/my-ui/form' 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 { SelectBirthPlace } from '~/components/app/person/fields'
import { import {
InputName, InputName,
@@ -60,10 +61,20 @@ interface PatientFormInput {
interface Props { interface Props {
isReadonly: boolean isReadonly: boolean
languageOptions?: { value: string; label: string }[]
ethnicOptions?: { value: string; label: string }[]
initialValues?: PatientFormInput initialValues?: PatientFormInput
mode?: 'add' | 'edit'
identityFileUrl?: string
familyCardFileUrl?: string
} }
const props = defineProps<Props>() const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'preview', url: string): void
}>()
const formSchema = toTypedSchema(PatientSchema) const formSchema = toTypedSchema(PatientSchema)
const { values, resetForm, setValues, setFieldValue, validate, setFieldError } = useForm<FormData>({ const { values, resetForm, setValues, setFieldValue, validate, setFieldError } = useForm<FormData>({
@@ -208,6 +219,7 @@ watch(
label="Suku" label="Suku"
placeholder="Pilih suku bangsa" placeholder="Pilih suku bangsa"
:is-disabled="isReadonly || values.nationality !== 'WNI'" :is-disabled="isReadonly || values.nationality !== 'WNI'"
:items="ethnicOptions || []"
/> />
<SelectLanguage <SelectLanguage
field-name="language" field-name="language"
@@ -215,6 +227,7 @@ watch(
placeholder="Pilih preferensi bahasa" placeholder="Pilih preferensi bahasa"
is-required is-required
:is-disabled="isReadonly" :is-disabled="isReadonly"
:items="languageOptions || []"
/> />
<SelectReligion <SelectReligion
field-name="religion" field-name="religion"
@@ -256,22 +269,112 @@ watch(
:col-count="2" :col-count="2"
:cell-flex="false" :cell-flex="false"
> >
<FileUpload <div class="flex flex-col gap-3">
field-name="identityCardFile" <FileUpload
label="Dokumen KTP" field-name="residentIdentityFile"
placeholder="Unggah scan dokumen KTP" label="Dokumen KTP"
:accept="['pdf', 'jpg', 'png']" placeholder="Unggah scan dokumen KTP"
:max-size-mb="1" :accept="['pdf', 'jpg', 'png']"
:is-disabled="isReadonly" :max-size-mb="1"
/> :is-disabled="isReadonly"
<FileUpload />
field-name="familyCardFile" <!-- Embed Preview Dokumen KTP (mode edit) -->
label="Dokumen KK" <div
placeholder="Unggah scan dokumen KK" v-if="mode === 'edit'"
:accept="['pdf', 'jpg', 'png']" class="flex flex-col gap-1"
:max-size-mb="1" >
:is-disabled="isReadonly" <span class="text-xs text-muted-foreground">Dokumen Tersimpan</span>
/> <div
class="relative h-28 w-48 cursor-pointer overflow-hidden rounded-md border bg-muted"
:class="{ 'cursor-not-allowed opacity-60': !identityFileUrl }"
@click="identityFileUrl && emit('preview', identityFileUrl)"
>
<img
v-if="identityFileUrl"
:src="identityFileUrl"
alt="Dokumen KTP"
class="h-full w-full object-cover"
/>
<Skeleton
v-else
class="h-full w-full"
/>
<!-- Overlay -->
<div
class="absolute inset-0 flex items-center justify-center bg-black/50 transition-opacity"
:class="identityFileUrl ? 'opacity-0 hover:opacity-100' : 'opacity-100'"
>
<div
v-if="identityFileUrl"
class="flex flex-col items-center gap-1 text-white"
>
<span class="i-lucide-eye h-5 w-5" />
<span class="text-xs font-medium">Lihat Dokumen</span>
</div>
<div
v-else
class="flex flex-col items-center gap-1 text-white/70"
>
<span class="i-lucide-info h-5 w-5" />
<span class="text-xs font-medium">No Data</span>
</div>
</div>
</div>
</div>
</div>
<div class="flex flex-col gap-3">
<FileUpload
field-name="familyIdentityFile"
label="Dokumen KK"
placeholder="Unggah scan dokumen KK"
:accept="['pdf', 'jpg', 'png']"
:max-size-mb="1"
:is-disabled="isReadonly"
/>
<!-- Embed Preview Dokumen KK (mode edit) -->
<div
v-if="mode === 'edit'"
class="flex flex-col gap-1"
>
<span class="text-xs text-muted-foreground">Dokumen Tersimpan</span>
<div
class="relative h-28 w-48 cursor-pointer overflow-hidden rounded-md border bg-muted"
:class="{ 'cursor-not-allowed opacity-60': !familyCardFileUrl }"
@click="familyCardFileUrl && emit('preview', familyCardFileUrl)"
>
<img
v-if="familyCardFileUrl"
:src="familyCardFileUrl"
alt="Dokumen KK"
class="h-full w-full object-cover"
/>
<Skeleton
v-else
class="h-full w-full"
/>
<!-- Overlay -->
<div
class="absolute inset-0 flex items-center justify-center bg-black/50 transition-opacity"
:class="familyCardFileUrl ? 'opacity-0 hover:opacity-100' : 'opacity-100'"
>
<div
v-if="familyCardFileUrl"
class="flex flex-col items-center gap-1 text-white"
>
<span class="i-lucide-eye h-5 w-5" />
<span class="text-xs font-medium">Lihat Dokumen</span>
</div>
<div
v-else
class="flex flex-col items-center gap-1 text-white/70"
>
<span class="i-lucide-info h-5 w-5" />
<span class="text-xs font-medium">No Data</span>
</div>
</div>
</div>
</div>
</div>
</DE.Block> </DE.Block>
</form> </form>
</template> </template>
@@ -6,6 +6,7 @@ import { cn } from '~/lib/utils'
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<{
items: { value: string; label: string }[]
fieldName?: string fieldName?: string
label?: string label?: string
placeholder?: string placeholder?: string
@@ -28,23 +29,23 @@ const {
labelClass, labelClass,
} = props } = props
const ethnicOptions = [ // const ethnicOptions = [
{ label: 'Tidak diketahui', value: 'unknown', priority: 1 }, // { label: 'Tidak diketahui', value: 'unknown', priority: 1 },
{ label: 'Jawa', value: 'jawa' }, // { label: 'Jawa', value: 'jawa' },
{ label: 'Sunda', value: 'sunda' }, // { label: 'Sunda', value: 'sunda' },
{ label: 'Batak', value: 'batak' }, // { label: 'Batak', value: 'batak' },
{ label: 'Betawi', value: 'betawi' }, // { label: 'Betawi', value: 'betawi' },
{ label: 'Minangkabau', value: 'minangkabau' }, // { label: 'Minangkabau', value: 'minangkabau' },
{ label: 'Bugis', value: 'bugis' }, // { label: 'Bugis', value: 'bugis' },
{ label: 'Madura', value: 'madura' }, // { label: 'Madura', value: 'madura' },
{ label: 'Banjar', value: 'banjar' }, // { label: 'Banjar', value: 'banjar' },
{ label: 'Bali', value: 'bali' }, // { label: 'Bali', value: 'bali' },
{ label: 'Dayak', value: 'dayak' }, // { label: 'Dayak', value: 'dayak' },
{ label: 'Aceh', value: 'aceh' }, // { label: 'Aceh', value: 'aceh' },
{ label: 'Sasak', value: 'sasak' }, // { label: 'Sasak', value: 'sasak' },
{ label: 'Papua', value: 'papua' }, // { label: 'Papua', value: 'papua' },
{ label: 'Lainnya', value: 'lainnya', priority: -100 }, // { label: 'Lainnya', value: 'lainnya', priority: -100 },
] // ]
</script> </script>
<template> <template>
@@ -70,7 +71,7 @@ const ethnicOptions = [
<Combobox <Combobox
:id="fieldName" :id="fieldName"
v-bind="componentField" v-bind="componentField"
:items="ethnicOptions" :items="items"
:placeholder="placeholder" :placeholder="placeholder"
:is-disabled="isDisabled" :is-disabled="isDisabled"
search-placeholder="Cari..." search-placeholder="Cari..."
@@ -6,6 +6,7 @@ import { cn } from '~/lib/utils'
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<{
items: { value: string; label: string }[]
fieldName?: string fieldName?: string
label?: string label?: string
placeholder?: string placeholder?: string
@@ -29,15 +30,15 @@ const {
labelClass, labelClass,
} = props } = props
const langOptions = [ // const langOptions = [
{ label: 'Bahasa Indonesia', value: 'id', priority: 1 }, // { label: 'Bahasa Indonesia', value: 'id', priority: 1 },
{ label: 'Bahasa Jawa', value: 'jawa' }, // { label: 'Bahasa Jawa', value: 'jawa' },
{ label: 'Bahasa Sunda', value: 'sunda' }, // { label: 'Bahasa Sunda', value: 'sunda' },
{ label: 'Bahasa Bali', value: 'bali' }, // { label: 'Bahasa Bali', value: 'bali' },
{ label: 'Bahasa Jaksel', value: 'jaksel' }, // { label: 'Bahasa Jaksel', value: 'jaksel' },
{ label: 'Bahasa Inggris', value: 'en' }, // { label: 'Bahasa Inggris', value: 'en' },
{ label: 'Tidak Diketahui', value: 'unknown', priority: 100 }, // { label: 'Tidak Diketahui', value: 'unknown', priority: 100 },
] // ]
</script> </script>
<template> <template>
@@ -64,7 +65,7 @@ const langOptions = [
:is-disabled="isDisabled" :is-disabled="isDisabled"
:id="fieldName" :id="fieldName"
v-bind="componentField" v-bind="componentField"
:items="langOptions" :items="items"
:placeholder="placeholder" :placeholder="placeholder"
:preserve-order="false" :preserve-order="false"
:class=" :class="
+83 -2
View File
@@ -38,6 +38,7 @@ const props = defineProps<{
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'back'): void (e: 'back'): void
(e: 'edit'): void (e: 'edit'): void
(e: 'preview', url: string): void
}>() }>()
// #endregion // #endregion
@@ -70,6 +71,14 @@ const patientAge = computed(() => {
} }
return calculateAge(props.patient.person.birthDate) return calculateAge(props.patient.person.birthDate)
}) })
const familiyCardFile = computed(() => {
return props.patient.person.familyIdentityFileUrl || ''
})
const identityFile = computed(() => {
return props.patient.person.residentIdentityFileUrl || ''
})
// #endregion // #endregion
// #region Lifecycle Hooks // #region Lifecycle Hooks
@@ -166,13 +175,85 @@ function onNavigate(type: ClickType) {
<AccordionTrigger>{{ section }}</AccordionTrigger> <AccordionTrigger>{{ section }}</AccordionTrigger>
<AccordionContent> <AccordionContent>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<!-- Dokumen KTP -->
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<span class="text-sm text-muted-foreground">Dokumen KTP</span> <span class="text-sm text-muted-foreground">Dokumen KTP</span>
<Skeleton class="h-32 w-64 rounded-md" /> <div
class="relative h-32 w-64 cursor-pointer overflow-hidden rounded-md border bg-muted"
:class="{ 'cursor-not-allowed opacity-60': !identityFile }"
@click="identityFile && emit('preview', identityFile)"
>
<img
v-if="identityFile"
:src="identityFile"
alt="Dokumen KTP"
class="h-full w-full object-cover"
/>
<Skeleton
v-else
class="h-full w-full"
/>
<!-- Overlay -->
<div
class="absolute inset-0 flex items-center justify-center bg-black/50 transition-opacity"
:class="identityFile ? 'opacity-0 hover:opacity-100' : 'opacity-100'"
>
<div
v-if="identityFile"
class="flex flex-col items-center gap-1 text-white"
>
<span class="i-lucide-eye h-6 w-6" />
<span class="text-sm font-medium">Lihat Dokumen</span>
</div>
<div
v-else
class="flex flex-col items-center gap-1 text-white/70"
>
<span class="i-lucide-info h-6 w-6" />
<span class="text-sm font-medium">No Data</span>
</div>
</div>
</div>
</div> </div>
<!-- Dokumen KK -->
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<span class="text-sm text-muted-foreground">Dokumen KK</span> <span class="text-sm text-muted-foreground">Dokumen KK</span>
<Skeleton class="h-32 w-64 rounded-md" /> <div
class="relative h-32 w-64 cursor-pointer overflow-hidden rounded-md border bg-muted"
:class="{ 'cursor-not-allowed opacity-60': !familiyCardFile }"
@click="familiyCardFile && emit('preview', familiyCardFile)"
>
<img
v-if="familiyCardFile"
:src="familiyCardFile"
alt="Dokumen KK"
class="h-full w-full object-cover"
/>
<Skeleton
v-else
class="h-full w-full"
/>
<!-- Overlay -->
<div
class="absolute inset-0 flex items-center justify-center bg-black/50 transition-opacity"
:class="familiyCardFile ? 'opacity-0 hover:opacity-100' : 'opacity-100'"
>
<div
v-if="familiyCardFile"
class="flex flex-col items-center gap-1 text-white"
>
<span class="i-lucide-eye h-6 w-6" />
<span class="text-sm font-medium">Lihat Dokumen</span>
</div>
<div
v-else
class="flex flex-col items-center gap-1 text-white/70"
>
<span class="i-lucide-info h-6 w-6" />
<span class="text-sm font-medium">No Data</span>
</div>
</div>
</div>
</div> </div>
</div> </div>
</AccordionContent> </AccordionContent>
@@ -0,0 +1,476 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import { Form } from '~/components/pub/ui/form'
import { toTypedSchema } from '@vee-validate/zod'
import Block from '~/components/pub/my-ui/form/block.vue'
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
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 { educationCodes, genderCodes, occupationCodes, religionCodes } from '~/lib/constants'
import { mapToComboboxOptList } from '~/lib/utils'
interface DivisionFormData {
name: string
code: string
parentId: string
}
const props = defineProps<{
division: {
msg: {
placeholder: string
search: string
empty: string
}
}
items: {
value: string
label: string
code: string
}[]
schema: any
initialValues?: Partial<DivisionFormData>
errors?: FormErrors
}>()
const emit = defineEmits<{
submit: [values: DivisionFormData, resetForm: () => void]
cancel: [resetForm: () => void]
}>()
const educationOpts = mapToComboboxOptList(educationCodes)
const occupationOpts = mapToComboboxOptList(occupationCodes)
const religionOpts = mapToComboboxOptList(religionCodes)
const genderOpts = mapToComboboxOptList(genderCodes)
const formSchema = toTypedSchema(props.schema)
// Form submission handler
function onSubmitForm(values: any, { resetForm }: { resetForm: () => void }) {
const formData: DivisionFormData = {
name: values.name || '',
code: values.code || '',
parentId: values.parentId || '',
}
emit('submit', formData, resetForm)
}
</script>
<template>
<Form
v-slot="{ handleSubmit, resetForm }"
as=""
keep-values
:validation-schema="formSchema"
:initial-values="initialValues"
>
<form
id="entry-form"
@submit="handleSubmit($event, (values) => onSubmitForm(values, { resetForm }))"
>
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
<div class="flex flex-col justify-between">
<Block>
<FieldGroup>
<Label label-for="residentIdentityNumber">KTP</Label>
<Field
id="residentIdentityNumber"
:errors="errors"
>
<FormField
v-slot="{ componentField }"
name="residentIdentityNumber"
>
<FormItem>
<FormControl>
<Input
id="residentIdentityNumber"
type="text"
maxlength="16"
placeholder="Nomor KTP"
autocomplete="off"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<!-- FrontTitle -->
<FieldGroup :column="3">
<Label label-for="frontTitle">Gelar Depan</Label>
<Field
id="frontTitle"
:errors="errors"
>
<FormField
v-slot="{ componentField }"
name="frontTitle"
>
<FormItem>
<FormControl>
<Input
id="frontTitle"
type="text"
placeholder="Dr., Ir., dll"
autocomplete="off"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<FieldGroup :column="3">
<Label
label-for="name"
position="dynamic"
>
Nama
</Label>
<Field
id="name"
:errors="errors"
>
<FormField
v-slot="{ componentField }"
name="name"
>
<FormItem>
<FormControl>
<Input
id="name"
type="text"
placeholder="Nama lengkap"
autocomplete="off"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<!-- EndTitle -->
<FieldGroup :column="3">
<Label
label-for="endTitle"
position="dynamic"
>
Gelar Belakang
</Label>
<Field
id="endTitle"
:errors="errors"
>
<FormField
v-slot="{ componentField }"
name="endTitle"
>
<FormItem>
<FormControl>
<Input
id="endTitle"
type="text"
placeholder="S.Kom, M.Kes, dll"
autocomplete="off"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<!-- BirthDate -->
<FieldGroup :column="2">
<Label label-for="birthDate">Tanggal Lahir</Label>
<Field
id="birthDate"
:errors="errors"
>
<FormField
v-slot="{ componentField }"
name="birthDate"
>
<FormItem>
<FormControl>
<Input
id="birthDate"
type="date"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<!-- BirthRegency_Code -->
<FieldGroup :column="2">
<Label label-for="birthRegencyCode">Tempat Lahir</Label>
<Field
id="birthRegencyCode"
:errors="errors"
>
<FormField
v-slot="{ componentField }"
name="birthRegencyCode"
>
<FormItem>
<FormControl>
<Combobox
id="parentId"
v-bind="componentField"
:items="educationOpts"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<!-- Gender_Code -->
<FieldGroup>
<Label label-for="genderCode">Jenis Kelamin</Label>
<Field
id="genderCode"
:errors="errors"
>
<FormField
v-slot="{ componentField }"
name="genderCode"
>
<FormItem>
<FormControl>
<Combobox
id="genderCode"
v-bind="componentField"
:items="genderOpts"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<!-- PassportNumber -->
<FieldGroup :column="2">
<Label label-for="passportNumber">Paspor</Label>
<Field
id="passportNumber"
:errors="errors"
>
<FormField
v-slot="{ componentField }"
name="passportNumber"
>
<FormItem>
<FormControl>
<Input
id="passportNumber"
type="text"
placeholder="Nomor Paspor"
autocomplete="off"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<!-- DrivingLicenseNumber -->
<FieldGroup :column="2">
<Label label-for="drivingLicenseNumber">SIM</Label>
<Field
id="drivingLicenseNumber"
:errors="errors"
>
<FormField
v-slot="{ componentField }"
name="drivingLicenseNumber"
>
<FormItem>
<FormControl>
<Input
id="drivingLicenseNumber"
type="text"
placeholder="Nomor SIM"
autocomplete="off"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<!-- Religion_Code -->
<FieldGroup :column="2">
<Label label-for="religionCode">Agama</Label>
<Field
id="religionCode"
:errors="errors"
>
<FormField
v-slot="{ componentField }"
name="religionCode"
>
<FormItem>
<FormControl>
<Combobox
id="religionCode"
v-bind="componentField"
:items="religionOpts"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<FieldGroup :column="2">
<Label label-for="ethnicCode">Suku</Label>
<Field
id="ethnicCode"
:errors="errors"
>
<FormField
v-slot="{ componentField }"
name="ethnicCode"
>
<FormItem>
<FormControl>
<Combobox
id="ethnicCode"
v-bind="componentField"
:items="occupationOpts"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<!-- Language_Code -->
<FieldGroup :column="2">
<Label label-for="languageCode">Bahasa</Label>
<Field
id="languageCode"
:errors="errors"
>
<FormField
v-slot="{ componentField }"
name="languageCode"
>
<FormItem>
<FormControl>
<Combobox
id="parentId"
v-bind="componentField"
:items="educationOpts"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<!-- Education_Code -->
<FieldGroup :column="2">
<Label label-for="educationCode">Pendidikan</Label>
<Field
id="educationCode"
:errors="errors"
>
<FormField
v-slot="{ componentField }"
name="educationCode"
>
<FormItem>
<FormControl>
<Combobox
id="educationCode"
v-bind="componentField"
:items="educationOpts"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<!-- Occupation_Code -->
<FieldGroup :column="2">
<Label label-for="occupationCode">Pekerjaan</Label>
<Field
id="occupationCode"
:errors="errors"
>
<FormField
v-slot="{ componentField }"
name="occupationCode"
>
<FormItem>
<FormControl>
<Combobox
id="occupationCode"
v-bind="componentField"
:items="occupationOpts"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<!-- Occupation_Name -->
<FieldGroup :column="2">
<Label label-for="occupationName">Detail Pekerjaan</Label>
<Field
id="occupationName"
:errors="errors"
>
<FormField
v-slot="{ componentField }"
name="occupationName"
>
<FormItem>
<FormControl>
<Input
id="occupationName"
type="text"
placeholder="Contoh: Guru SMP, Petani"
autocomplete="off"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
</Block>
</div>
</div>
</form>
</Form>
</template>
+8 -5
View File
@@ -15,13 +15,14 @@ import { useIntegrationSepEntry } from '~/handlers/integration-sep-entry.handler
// Props // Props
const props = defineProps<{ const props = defineProps<{
id: number id: number
classCode?: 'ambulatory' | 'emergency' | 'inpatient' | 'outpatient' classCode?: 'ambulatory' | 'emergency' | 'inpatient'
subclassCode?: 'reg' | 'rehab' | 'chemo' | 'emg' | 'eon' | 'op' | 'icu' | 'hcu' | 'vk' subclassCode?: 'regular' | 'rehab' | 'chemo' | 'emg' | 'eon' | 'op' | 'icu' | 'hcu' | 'vk'
formType: string formType: string
}>() }>()
const route = useRoute() const route = useRoute()
const formRef = ref<InstanceType<typeof AppEncounterEntryForm> | null>(null) const formRef = ref<InstanceType<typeof AppEncounterEntryForm> | null>(null)
// const patientMode = ref<'new' | 'exists'>('exists')
const { const {
paymentsList, paymentsList,
@@ -56,7 +57,6 @@ const {
getDoctorInfo, getDoctorInfo,
getValidateMember, getValidateMember,
getValidateSepNumber, getValidateSepNumber,
handleFetchDoctors,
} = useEncounterEntry(props) } = useEncounterEntry(props)
const { recSepId, openHistory, histories, getMonitoringHistoryMappers } = useIntegrationSepEntry() const { recSepId, openHistory, histories, getMonitoringHistoryMappers } = useIntegrationSepEntry()
@@ -82,11 +82,13 @@ function handleFetch(value?: any) {
async function handleEvent(menu: string, value?: any) { async function handleEvent(menu: string, value?: any) {
if (menu === 'search') { if (menu === 'search') {
// patientMode.value = 'exists'
getPatientsList({ 'page-size': 10, includes: 'person' }).then(() => { getPatientsList({ 'page-size': 10, includes: 'person' }).then(() => {
openPatient.value = true openPatient.value = true
}) })
} else if (menu === 'add') { } else if (menu === 'add') {
navigateTo('/client/patient/add') // navigateTo('/client/patient/add')
// patientMode.value = 'new'
} else if (menu === 'add-sep') { } else if (menu === 'add-sep') {
if (isSepValid.value) { if (isSepValid.value) {
return return
@@ -167,6 +169,7 @@ onMounted(async () => {
Kunjungan Kunjungan
</div> </div>
<!-- :patientMode="patientMode" -->
<AppEncounterEntryForm <AppEncounterEntryForm
ref="formRef" ref="formRef"
:mode="props.formType" :mode="props.formType"
@@ -226,10 +229,10 @@ onMounted(async () => {
/> />
Batal Batal
</Button> </Button>
<!-- :disabled="isSaveDisabled" -->
<Button <Button
type="button" type="button"
class="h-[40px] min-w-[120px] text-white" class="h-[40px] min-w-[120px] text-white"
:disabled="isSaveDisabled"
@click="handleSaveClick" @click="handleSaveClick"
> >
<Icon <Icon
+7 -4
View File
@@ -35,7 +35,7 @@ import FilterForm from '~/components/app/encounter/filter-form.vue'
// Props // Props
const props = defineProps<{ const props = defineProps<{
classCode?: 'ambulatory' | 'emergency' | 'inpatient' classCode?: 'ambulatory' | 'emergency' | 'inpatient'
subclassCode?: 'reg' | 'rehab' | 'chemo' | 'emg' | 'eon' | 'op' | 'icu' | 'hcu' | 'vk' subclassCode?: 'regular' | 'rehab' | 'chemo' | 'emg' | 'eon' | 'op' | 'icu' | 'hcu' | 'vk' | undefined
canCreate?: boolean canCreate?: boolean
canUpdate?: boolean canUpdate?: boolean
canDelete?: boolean canDelete?: boolean
@@ -162,7 +162,6 @@ async function getPatientList() {
'Responsible_Doctor-employee', 'Responsible_Doctor-employee',
'Responsible_Doctor-employee-person', 'Responsible_Doctor-employee-person',
'EncounterDocuments', 'EncounterDocuments',
'unit',
'vclaimReference', // vclaimReference | vclaimSep 'vclaimReference', // vclaimReference | vclaimSep
] ]
const includesParams = includesParamsArrays.join(',') const includesParams = includesParamsArrays.join(',')
@@ -172,8 +171,11 @@ async function getPatientList() {
if (props.classCode) { if (props.classCode) {
params['class-code'] = props.classCode params['class-code'] = props.classCode
} }
if (props.subclassCode) { if (props.subclassCode == 'regular') {
params['subclass-code'] = props.subclassCode params['specialist-code'] = 'rehab'
params['specialist-code-opt'] = 'ne'
} else {
params['specialist-code'] = 'rehab'
} }
const result = await getEncounterList(params) const result = await getEncounterList(params)
if (result.success) { if (result.success) {
@@ -365,6 +367,7 @@ function handleRemoveConfirmation() {
</script> </script>
<template> <template>
{{ subclassCode }}
<CH.ContentHeader v-bind="hreaderPrep"> <CH.ContentHeader v-bind="hreaderPrep">
<FilterNav <FilterNav
:active-positon="activeServicePosition" :active-positon="activeServicePosition"
+18
View File
@@ -6,6 +6,8 @@ import type { Person } from '~/models/person'
// Components // Components
import Header from '~/components/pub/my-ui/nav-header/prep.vue' import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import DocPreviewDialog from '~/components/pub/my-ui/modal/doc-preview-dialog.vue'
import { getPatientDetail } from '~/services/patient.service' import { getPatientDetail } from '~/services/patient.service'
@@ -17,6 +19,9 @@ const props = defineProps<{
// #endregion // #endregion
// #region State & Computed // #region State & Computed
const isDocPreviewDialogOpen = ref<boolean>(false)
const docPreviewUrl = ref<string>('')
const patient = ref( const patient = ref(
withBase<PatientEntity>({ withBase<PatientEntity>({
person: {} as Person, person: {} as Person,
@@ -80,6 +85,19 @@ async function onEdit() {
:patient="patient" :patient="patient"
@back="onBack" @back="onBack"
@edit="onEdit" @edit="onEdit"
@preview="
(url: string) => {
docPreviewUrl = url
isDocPreviewDialogOpen = true
}
"
/> />
<Dialog
v-model:open="isDocPreviewDialogOpen"
title="Preview Dokumen"
size="2xl"
>
<DocPreviewDialog :link="docPreviewUrl" />
</Dialog>
</div> </div>
</template> </template>
+53 -3
View File
@@ -17,6 +17,8 @@ import AppPersonAddressEntryFormRelative from '~/components/app/person-address/e
import AppPersonFamilyParentsForm from '~/components/app/person/family-parents-form.vue' import AppPersonFamilyParentsForm from '~/components/app/person/family-parents-form.vue'
import AppPersonContactEntryForm from '~/components/app/person-contact/entry-form.vue' import AppPersonContactEntryForm from '~/components/app/person-contact/entry-form.vue'
import AppPersonRelativeEntryForm from '~/components/app/person-relative/entry-form.vue' import AppPersonRelativeEntryForm from '~/components/app/person-relative/entry-form.vue'
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import DocPreviewDialog from '~/components/pub/my-ui/modal/doc-preview-dialog.vue'
// helper // helper
import { format, parseISO } from 'date-fns' import { format, parseISO } from 'date-fns'
@@ -24,6 +26,8 @@ import { id as localeID } from 'date-fns/locale'
// services // services
import { getPatientDetail, uploadAttachment } from '~/services/patient.service' import { getPatientDetail, uploadAttachment } from '~/services/patient.service'
import { getValueLabelList as getEthnicOpts } from '~/services/ethnic.service'
import { getValueLabelList as getLanguageOpts } from '~/services/language.service'
import { isReadonly, isProcessing, handleActionSave, handleActionEdit } from '~/handlers/patient.handler' import { isReadonly, isProcessing, handleActionSave, handleActionEdit } from '~/handlers/patient.handler'
@@ -47,6 +51,23 @@ const props = defineProps<{
const residentIdentityFile = ref<File>() const residentIdentityFile = ref<File>()
const familyCardFile = ref<File>() const familyCardFile = ref<File>()
// Dialog preview dokumen
const isDocPreviewDialogOpen = ref<boolean>(false)
const docPreviewUrl = ref<string>('')
// Computed untuk URL dokumen tersimpan
const identityFileUrl = computed(() => {
return patientDetail.value.person?.residentIdentityFileUrl || ''
})
const familyCardFileUrl = computed(() => {
return patientDetail.value.person?.familyIdentityFileUrl || ''
})
function handlePreviewDoc(url: string) {
docPreviewUrl.value = url
isDocPreviewDialogOpen.value = true
}
// form related state // form related state
const personAddressForm = ref<ExposedForm<any> | null>(null) const personAddressForm = ref<ExposedForm<any> | null>(null)
const personAddressRelativeForm = ref<ExposedForm<any> | null>(null) const personAddressRelativeForm = ref<ExposedForm<any> | null>(null)
@@ -58,6 +79,9 @@ const personPatientForm = ref<ExposedForm<any> | null>(null)
// #endregion // #endregion
// #region State & Computed // #region State & Computed
const ethnicOptions = ref<{ value: string; label: string }[]>([])
const languageOptions = ref<{ value: string; label: string }[]>([])
const patientDetail = ref( const patientDetail = ref(
withBase<PatientEntity>({ withBase<PatientEntity>({
person: {} as Person, person: {} as Person,
@@ -223,6 +247,13 @@ const responsibleFormInitialValues = computed(() => {
// #region Lifecycle Hooks // #region Lifecycle Hooks
onMounted(async () => { onMounted(async () => {
const optsReq = {
'page-no-limit': true,
}
ethnicOptions.value = await getEthnicOpts(optsReq, true)
languageOptions.value = await getLanguageOpts(optsReq, true)
// if edit mode, fetch patient detail // if edit mode, fetch patient detail
if (props.mode === 'edit' && props.patientId) { if (props.mode === 'edit' && props.patientId) {
await loadInitData(props.patientId) await loadInitData(props.patientId)
@@ -309,7 +340,9 @@ async function handleActionClick(eventType: string) {
const patient: Patient = await composeFormData() const patient: Patient = await composeFormData()
let createdPatientId = 0 let createdPatientId = 0
let createdPersonId = 0
let response: any let response: any
// return // return
if (props.mode === 'edit' && props.patientId) { if (props.mode === 'edit' && props.patientId) {
response = await handleActionEdit( response = await handleActionEdit(
@@ -330,13 +363,15 @@ async function handleActionClick(eventType: string) {
const data = (response?.body?.data ?? null) as PatientBase | null const data = (response?.body?.data ?? null) as PatientBase | null
if (!data) return if (!data) return
createdPatientId = data.id createdPatientId = data.id
createdPersonId = data.person_id!
if (residentIdentityFile.value) { if (residentIdentityFile.value) {
void uploadAttachment(residentIdentityFile.value, createdPatientId, 'ktp') void uploadAttachment(residentIdentityFile.value, createdPersonId, 'ktp')
} }
if (familyCardFile.value) { if (familyCardFile.value) {
void uploadAttachment(familyCardFile.value, createdPatientId, 'kk') void uploadAttachment(familyCardFile.value, createdPersonId, 'kk')
} }
// If has callback provided redirect to callback with patientData // If has callback provided redirect to callback with patientData
@@ -435,10 +470,16 @@ watch(
{{ mode === 'edit' ? 'Edit Pasien' : 'Tambah Pasien' }} {{ mode === 'edit' ? 'Edit Pasien' : 'Tambah Pasien' }}
</div> </div>
<AppPatientEntryForm <AppPatientEntryForm
:key="`patient-${formKey}`"
ref="personPatientForm" ref="personPatientForm"
:key="`patient-${formKey}`"
:is-readonly="isProcessing || isReadonly" :is-readonly="isProcessing || isReadonly"
:initial-values="patientFormInitialValues" :initial-values="patientFormInitialValues"
:language-options="languageOptions"
:ethnic-options="ethnicOptions"
:mode="mode"
:identity-file-url="identityFileUrl"
:family-card-file-url="familyCardFileUrl"
@preview="handlePreviewDoc"
/> />
<div class="h-6"></div> <div class="h-6"></div>
<AppPersonAddressEntryForm <AppPersonAddressEntryForm
@@ -483,6 +524,15 @@ watch(
<div class="my-2 flex justify-end py-2"> <div class="my-2 flex justify-end py-2">
<Action @click="handleActionClick" /> <Action @click="handleActionClick" />
</div> </div>
<!-- Dialog Preview Dokumen -->
<Dialog
v-model:open="isDocPreviewDialogOpen"
title="Preview Dokumen"
size="2xl"
>
<DocPreviewDialog :link="docPreviewUrl" />
</Dialog>
</template> </template>
<style scoped> <style scoped>
+64 -44
View File
@@ -24,6 +24,7 @@ import {
import { getDetail as getDoctorDetail, getValueLabelList as getDoctorValueLabelList } from '~/services/doctor.service' import { getDetail as getDoctorDetail, getValueLabelList as getDoctorValueLabelList } from '~/services/doctor.service'
import { import {
create as createEncounter, create as createEncounter,
createWithPatient,
getDetail as getEncounterDetail, getDetail as getEncounterDetail,
update as updateEncounter, update as updateEncounter,
} from '~/services/encounter.service' } from '~/services/encounter.service'
@@ -44,7 +45,8 @@ import {
export function useEncounterEntry(props: { export function useEncounterEntry(props: {
id: number id: number
classCode?: 'ambulatory' | 'emergency' | 'inpatient' | 'outpatient' classCode?: 'ambulatory' | 'emergency' | 'inpatient' | 'outpatient'
subclassCode?: 'reg' | 'rehab' | 'chemo' | 'emg' | 'eon' | 'op' | 'icu' | 'hcu' | 'vk' subclassCode?: 'regular' | 'rehab' | 'chemo' | 'emg' | 'eon' | 'op' | 'icu' | 'hcu' | 'vk'
specialist_Code?: 'regular' | 'rehab' | 'chemo' | 'emg' | 'eon' | 'op' | 'icu' | 'hcu' | 'vk'
}) { }) {
const route = useRoute() const route = useRoute()
const userStore = useUserStore() const userStore = useUserStore()
@@ -207,7 +209,7 @@ export function useEncounterEntry(props: {
} }
async function getDoctorInfo(value: string) { async function getDoctorInfo(value: string) {
const resp = await getDoctorDetail(value, { includes: 'unit,specialist,subspecialist' }) const resp = await getDoctorDetail(value, { includes: 'specialist,subspecialist' })
if (resp.success) { if (resp.success) {
selectedDoctor.value = resp.body.data selectedDoctor.value = resp.body.data
} }
@@ -294,22 +296,22 @@ export function useEncounterEntry(props: {
} }
} }
async function handleFetchDoctors(unit?: string, specialist_code?: string, subSpecialist_code?: string) { async function handleFetchDoctors(specialist_code?: string, subSpecialist_code?: string) {
try { try {
const filterParams: Record<string, any> = { const filterParams: Record<string, any> = {
'page-size': 100, 'page-size': 100,
includes: 'employee-Person,unit,specialist,subspecialist' includes: 'employee-Person,specialist,subspecialist'
} }
// rehab is special // rehab is special
if (unit) { // if (unit) {
if (unit == 'rehab') { // if (unit == 'rehab') {
filterParams['unit-code'] = 'rehab' // filterParams['unit-code'] = 'rehab'
} else { // } else {
filterParams['unit-code'] = 'rehab' // filterParams['unit-code'] = 'rehab'
filterParams['unit-code-opt'] = 'ne' // filterParams['unit-code-opt'] = 'ne'
} // }
} // }
if (specialist_code) { if (specialist_code) {
if (specialist_code == 'rehab') { if (specialist_code == 'rehab') {
filterParams['specialist-code'] = 'rehab' filterParams['specialist-code'] = 'rehab'
@@ -318,7 +320,9 @@ export function useEncounterEntry(props: {
filterParams['specialist-code-opt'] = 'ne' filterParams['specialist-code-opt'] = 'ne'
} }
} }
if (subSpecialist_code) filterParams['subspecialist-code'] = subSpecialist_code if (subSpecialist_code) {
filterParams['subspecialist-code'] = subSpecialist_code
}
doctorsList.value = await getDoctorValueLabelList(filterParams, true) doctorsList.value = await getDoctorValueLabelList(filterParams, true)
// const isSub = getIsSubspecialist(subSpecialistId, specialistsTree.value) // const isSub = getIsSubspecialist(subSpecialistId, specialistsTree.value)
@@ -507,13 +511,18 @@ export function useEncounterEntry(props: {
} }
async function handleSaveEncounter(formValues: any) { async function handleSaveEncounter(formValues: any) {
if (!selectedPatient.value || !selectedPatientObject.value) { let enccounterRef = formValues
toast({ if (!formValues.encounter) {
title: 'Gagal', if (!selectedPatient.value || !selectedPatientObject.value) {
description: 'Pasien harus dipilih terlebih dahulu', toast({
variant: 'destructive', title: 'Gagal',
}) description: 'Pasien harus dipilih terlebih dahulu',
return variant: 'destructive',
})
return
}
} else {
enccounterRef = {...formValues.encounter}
} }
try { try {
@@ -527,24 +536,24 @@ export function useEncounterEntry(props: {
return date.toISOString() return date.toISOString()
} }
const { specialist_id, subspecialist_id } = getSpecialistIdsFromCode(formValues.subSpecialistId || '') const { specialist_id, subspecialist_id } = getSpecialistIdsFromCode(enccounterRef.subSpecialistId || '')
const patientId = formValues.patient_id || selectedPatientObject.value?.id || Number(selectedPatient.value) const patientId = enccounterRef.patient_id || selectedPatientObject.value?.id || Number(selectedPatient.value)
const registeredAtValue = formValues.registeredAt || formValues.registerDate || '' const registeredAtValue = enccounterRef.registeredAt || enccounterRef.registerDate || ''
const visitDateValue = formValues.visitDate || formValues.registeredAt || formValues.registerDate || '' const visitDateValue = enccounterRef.visitDate || enccounterRef.registeredAt || enccounterRef.registerDate || ''
const memberNumber = formValues.member_number ?? formValues.cardNumber ?? formValues.memberNumber ?? null const memberNumber = enccounterRef.member_number ?? enccounterRef.cardNumber ?? enccounterRef.memberNumber ?? null
const refNumber = formValues.ref_number ?? formValues.sepNumber ?? formValues.refNumber ?? null const refNumber = enccounterRef.ref_number ?? enccounterRef.sepNumber ?? enccounterRef.refNumber ?? null
sepFile.value = formValues.sepFile || null sepFile.value = enccounterRef.sepFile || null
sippFile.value = formValues.sippFile || null sippFile.value = enccounterRef.sippFile || null
let paymentMethodCode = formValues.paymentMethod_code ?? null let paymentMethodCode = enccounterRef.paymentMethod_code ?? null
if (!isUsePaymentNew && !paymentMethodCode) { if (!isUsePaymentNew && !paymentMethodCode) {
if (formValues.paymentType === 'jkn' || formValues.paymentType === 'jkmm') { if (enccounterRef.paymentType === 'jkn' || enccounterRef.paymentType === 'jkmm') {
paymentMethodCode = 'insurance' paymentMethodCode = 'insurance'
} else if (formValues.paymentType === 'spm') { } else if (enccounterRef.paymentType === 'spm') {
paymentMethodCode = 'cash' paymentMethodCode = 'cash'
} else if (formValues.paymentType === 'pks') { } else if (enccounterRef.paymentType === 'pks') {
paymentMethodCode = 'membership' paymentMethodCode = 'membership'
} else { } else {
paymentMethodCode = 'cash' paymentMethodCode = 'cash'
@@ -553,19 +562,20 @@ export function useEncounterEntry(props: {
const payload: any = { const payload: any = {
patient_id: patientId, patient_id: patientId,
appointment_doctor_code: formValues.doctor_code || null, appointment_doctor_code: enccounterRef.doctor_code || null,
class_code: props.classCode || '', class_code: props.classCode || '',
subClass_code: props.subclassCode || '', subClass_code: props.subclassCode || '',
infra_id: formValues.infra_id ?? null, specialist_code: selectedDoctor.value?.specialist_code || '',
unit_code: formValues.unitCode ?? userStore?.user?.unit_code ?? null, infra_id: enccounterRef.infra_id ?? null,
refSource_name: formValues.refSource_name ?? 'RSSA', unit_code: enccounterRef.unitCode ?? userStore?.user?.unit_code ?? null,
refTypeCode: formValues.paymentType === 'jkn' ? 'bpjs' : '', refSource_name: enccounterRef.refSource_name ?? 'RSSA',
refTypeCode: enccounterRef.paymentType === 'jkn' ? 'bpjs' : '',
vclaimReference: vclaimReference.value ?? null, vclaimReference: vclaimReference.value ?? null,
paymentType: formValues.paymentType, paymentType: enccounterRef.paymentType,
registeredAt: formatDate(registeredAtValue), registeredAt: formatDate(registeredAtValue),
visitDate: formatDate(visitDateValue), visitDate: formatDate(visitDateValue),
} }
if (props.classCode !== 'inpatient') { if (props.classCode !== 'inpatient') {
delete payload.infra_id delete payload.infra_id
} }
@@ -588,10 +598,10 @@ export function useEncounterEntry(props: {
} }
if (paymentMethodCode === 'insurance') { if (paymentMethodCode === 'insurance') {
payload.insuranceCompany_id = formValues.insuranceCompany_id ?? null payload.insuranceCompany_id = enccounterRef.insuranceCompany_id ?? null
if (memberNumber) payload.member_number = memberNumber if (memberNumber) payload.member_number = memberNumber
if (formValues.refTypeCode) payload.refTypeCode = formValues.refTypeCode if (enccounterRef.refTypeCode) payload.refTypeCode = enccounterRef.refTypeCode
if (formValues.vclaimReference) payload.vclaimReference = formValues.vclaimReference if (enccounterRef.vclaimReference) payload.vclaimReference = enccounterRef.vclaimReference
} else { } else {
if (paymentMethodCode === 'membership' && memberNumber) { if (paymentMethodCode === 'membership' && memberNumber) {
payload.member_number = memberNumber payload.member_number = memberNumber
@@ -607,8 +617,18 @@ export function useEncounterEntry(props: {
if (isEditMode.value) { if (isEditMode.value) {
result = await updateEncounter(props.id, payload) result = await updateEncounter(props.id, payload)
} else { } else {
console.log('💾 [ADD MODE] Sending POST request:', { payload }) // console.log('💾 [ADD MODE] Sending POST request:', { payload })
result = await createEncounter(payload) if (!formValues.encounter) {
result = await createEncounter(payload)
} else {
if (formValues.patient?.person?.birthDate) {
formValues.patient.person.birthDate += 'T00:00:00.000Z'
}
result = await createWithPatient({
encounter: payload,
patient: formValues.patient,
})
}
} }
if (result.success) { if (result.success) {
+15
View File
@@ -5,6 +5,10 @@ import type { EncounterDocument } from "./encounter-document"
import type { InternalReference } from "./internal-reference" import type { InternalReference } from "./internal-reference"
import type { Nurse } from "./nurse" import type { Nurse } from "./nurse"
import { type Patient, genPatient } from "./patient" import { type Patient, genPatient } from "./patient"
import type { Person } from "./person"
import type { PersonAddress } from "./person-address"
import type { PersonContact } from "./person-contact"
import type { PersonRelative } from "./person-relative"
import type { Specialist } from "./specialist" import type { Specialist } from "./specialist"
import type { Subspecialist } from "./subspecialist" import type { Subspecialist } from "./subspecialist"
import { genUnit, type Unit } from "./unit" import { genUnit, type Unit } from "./unit"
@@ -45,6 +49,16 @@ export interface Encounter {
encounterDocuments: EncounterDocument[] encounterDocuments: EncounterDocument[]
} }
export interface CreateDtoWithPatient {
encounter: Encounter
patient: {
person: Person
personAddresses: PersonAddress[]
personContacts: PersonContact[]
personRelatives: PersonRelative[]
}
}
export function genEncounter(): Encounter { export function genEncounter(): Encounter {
return { return {
id: 0, id: 0,
@@ -62,6 +76,7 @@ export function genEncounter(): Encounter {
medicalDischargeEducation: '', medicalDischargeEducation: '',
status_code: '', status_code: '',
encounterDocuments: [], encounterDocuments: [],
} }
} }
+1 -1
View File
@@ -1,4 +1,4 @@
import { type Base, genBase } from "./_base" import { type Base, genBase } from './_base'
export interface Ethnic extends Base { export interface Ethnic extends Base {
code: string code: string
+2 -2
View File
@@ -1,11 +1,11 @@
import { type Base, genBase } from "./_base" import { type Base, genBase } from './_base'
export interface Language extends Base { export interface Language extends Base {
code: string code: string
name: string name: string
} }
export function genMcuSrc(): Language { export function genLanguage(): Language {
return { return {
...genBase(), ...genBase(),
code: '', code: '',
@@ -32,29 +32,26 @@ const hasAccess = getPageAccess(roleAccess, 'create')
// TODO: Make a function for this // TODO: Make a function for this
const { user } = useUserStore() const { user } = useUserStore()
const servicePosition = user.user_contractPosition_code == 'emp' ? getServicePosition('emp|'+user.employee_position_code) : null const servicePosition = user.user_contractPosition_code == 'emp' ? getServicePosition(user.activeRole) : null
const subClassCode = servicePosition == 'med' ? const subClassCode = servicePosition == 'med' ?
// medic // medic
(user.unit_code == 'rehab' ? 'rehab' : 'reg') : (user.specialist_code == 'rehab' ? 'rehab' : 'regular') :
// non medic // non medic
( (
user.employee_position_code == 'reg' ? servicePosition == 'reg' ?
(user.installation_code == 'rehab' ? 'rehab' : 'reg') : (user.installation_code == 'rehab' ? 'rehab' : 'regular') :
undefined undefined
) )
</script> </script>
<template> <template>
<div v-if="hasAccess"> <Content v-if="hasAccess"
<Content :id="0"
:id="0" class-code="ambulatory"
class-code="ambulatory" :subclass-code="subClassCode"
:subclass-code="subClassCode" form-type="add"
form-type="add" />
/> <Error v-else
</div>
<Error
v-else
:status-code="403" :status-code="403"
/> />
</template> </template>
@@ -39,24 +39,23 @@ const canRemove = getPageAccess(roleAccess, 'delete')
// User info // User info
const { user } = useUserStore() const { user } = useUserStore()
// TODO: Make a function for this // TODO: Make a function for this
const servicePosition = user.user_contractPosition_code == 'emp' ? getServicePosition('emp|'+user.employee_position_code) : null const servicePosition = user.user_contractPosition_code == 'emp' ? getServicePosition(user.activeRole) : null
const subClassCode = servicePosition == 'med' ? const subClassCode = servicePosition == 'med' ?
// medic // medic
(user.unit_code == 'rehab' ? 'rehab' : 'reg') : (user.specialist_code == 'rehab' ? 'rehab' : 'regular') :
// non medic // non medic
( (
user.employee_position_code == 'reg' ? servicePosition == 'reg' ?
(user.installation_code == 'rehab' ? 'rehab' : 'reg') : (user.installation_code == 'rehab' ? 'rehab' : 'regular') :
undefined undefined
) )
</script> </script>
<template> <template>
<div> <div>
{{ user.employee_position_code }}---<br />
{{ servicePosition }}---<br />
{{ subClassCode }}---
<div v-if="hasAccess"> <div v-if="hasAccess">
{{ servicePosition }}--
{{ subClassCode }}--
<Content <Content
class-code="ambulatory" class-code="ambulatory"
:subclass-code="subClassCode" :subclass-code="subClassCode"
+23 -3
View File
@@ -1,5 +1,9 @@
import { isValid } from "date-fns" import { isValid } from "date-fns"
import { z } from 'zod' import { z } from 'zod'
import { PatientSchema } from "./patient.schema"
import { PersonAddressSchema } from "./person-address.schema"
import { PersonContactBaseSchema } from "./person-contact.schema"
import { PersonAddressRelativeSchema } from "./person-address-relative.schema"
const ERROR_MESSAGES = { const ERROR_MESSAGES = {
required: { required: {
@@ -18,12 +22,15 @@ const ERROR_MESSAGES = {
const ACCEPTED_UPLOAD_TYPES = ['image/jpeg', 'image/png', 'application/pdf'] const ACCEPTED_UPLOAD_TYPES = ['image/jpeg', 'image/png', 'application/pdf']
const isValidationSep = false const isValidationSep = false
// const encounterPatientShcema = z.object({
// })
const IntegrationEncounterSchema = z const IntegrationEncounterSchema = z
.object({ .object({
// Patient data (readonly, populated from selected patient) // Patient data (readonly, populated from selected patient)
patientSource: z.string().optional(),
patientName: z.string().optional(), patientName: z.string().optional(),
nationalIdentity: z.string().optional(), residentIdentiyNumber: z.string().optional(),
medicalRecordNumber: z.string().optional(), medicalRecordNumber: z.string().optional(),
// Visit data // Visit data
@@ -129,8 +136,21 @@ const IntegrationEncounterSchema = z
}, },
) )
const IntegrationEncounterWPSchema = z
.object({
encounter: IntegrationEncounterSchema,
patient: z.object({
person: PatientSchema,
personAddresses: z.array(PersonAddressSchema),
personContacts: z.array(PersonContactBaseSchema),
personRelatives: z.array(PersonAddressRelativeSchema),
}),
})
type IntegrationEncounterFormData = z.infer<typeof IntegrationEncounterSchema> type IntegrationEncounterFormData = z.infer<typeof IntegrationEncounterSchema>
type IntegrationEncounterWPFormData = z.infer<typeof IntegrationEncounterWPSchema>
export { IntegrationEncounterSchema } export { IntegrationEncounterSchema, IntegrationEncounterWPSchema }
export type { IntegrationEncounterFormData } export type { IntegrationEncounterFormData, IntegrationEncounterWPFormData }
+2 -1
View File
@@ -102,7 +102,8 @@ const PatientSchema = z
(data) => { (data) => {
if (data.nationality === 'WNI') { if (data.nationality === 'WNI') {
const nik = data.identityNumber?.trim() const nik = data.identityNumber?.trim()
return !!nik && nik.length === 16 && /^\d+$/.test(nik) if (!nik) return true
return nik && nik.length === 16 && /^\d+$/.test(nik)
} }
return true return true
}, },
+14 -1
View File
@@ -64,4 +64,17 @@ export async function checkIn(id: number, data: CheckInFormData) {
console.error(`Error putting ${name}:`, error) console.error(`Error putting ${name}:`, error)
throw new Error(`Failed to put ${name}`) throw new Error(`Failed to put ${name}`)
} }
} }
export async function createWithPatient(data: any) {
try {
const resp = await xfetch(path + '/create-with-patient', 'POST', data)
const result: any = {}
result.success = resp.success
result.body = (resp.body as Record<string, any>) || {}
return result
} catch (error) {
console.error(`Error putting ${name}:`, error)
throw new Error(`Failed to put ${name}`)
}
}
+44
View File
@@ -0,0 +1,44 @@
// Base
import * as base from './_crud-base'
// Types
import type { Ethnic } from '~/models/ethnic'
const path = '/api/v1/ethnic'
const name = 'ethnic'
export function create(data: any) {
return base.create(path, data, name)
}
export function getList(params: any = null) {
return base.getList(path, params, name)
}
export function getDetail(id: number | string) {
return base.getDetail(path, id, name)
}
export function update(id: number | string, data: any) {
return base.update(path, id, data, name)
}
export function remove(id: number | string) {
return base.remove(path, id, name)
}
export async function getValueLabelList(
params: any = null,
useCodeAsValue = false,
): Promise<{ value: string; label: string }[]> {
let data: { value: string; label: string }[] = []
const result = await getList(params)
if (result.success) {
const resultData = result.body?.data || []
data = resultData.map((item: Ethnic) => ({
value: useCodeAsValue ? item.code : item.id ? Number(item.id) : item.code,
label: item.name,
}))
}
return data
}
+44
View File
@@ -0,0 +1,44 @@
// Base
import * as base from './_crud-base'
// Types
import type { Language } from '~/models/language'
const path = '/api/v1/language'
const name = 'language'
export function create(data: any) {
return base.create(path, data, name)
}
export function getList(params: any = null) {
return base.getList(path, params, name)
}
export function getDetail(id: number | string) {
return base.getDetail(path, id, name)
}
export function update(id: number | string, data: any) {
return base.update(path, id, data, name)
}
export function remove(id: number | string) {
return base.remove(path, id, name)
}
export async function getValueLabelList(
params: any = null,
useCodeAsValue = false,
): Promise<{ value: string; label: string }[]> {
let data: { value: string; label: string }[] = []
const result = await getList(params)
if (result.success) {
const resultData = result.body?.data || []
data = resultData.map((item: Language) => ({
value: useCodeAsValue ? item.code : item.id ? Number(item.id) : item.code,
label: item.name,
}))
}
return data
}
+6 -3
View File
@@ -101,7 +101,7 @@ export async function removePatient(id: number) {
} }
} }
export async function uploadAttachment(file: File, userId: number, key: UploadCodeKey) { export async function uploadAttachment(file: File, personId: number, key: UploadCodeKey) {
try { try {
const resolvedKey = uploadCode[key] const resolvedKey = uploadCode[key]
if (!resolvedKey) { if (!resolvedKey) {
@@ -110,11 +110,14 @@ export async function uploadAttachment(file: File, userId: number, key: UploadCo
// siapkan form-data body // siapkan form-data body
const formData = new FormData() const formData = new FormData()
formData.append('code', resolvedKey)
formData.append('content', file) formData.append('content', file)
formData.append('entityType_code', 'person')
formData.append('ref_id', String(personId))
// formData.append('upload_employee_id', String(userId))
formData.append('type_code', resolvedKey)
// kirim via xfetch // kirim via xfetch
const resp = await xfetch(`${mainUrl}/${userId}/upload`, 'POST', formData) const resp = await xfetch(`/api/v1/upload-file`, 'POST', formData)
// struktur hasil sama seperti patchPatient // struktur hasil sama seperti patchPatient
const result: any = {} const result: any = {}
+8544 -6636
View File
File diff suppressed because it is too large Load diff
-5
View File
@@ -21,11 +21,6 @@
} }
] ]
}, },
{
"title": "IGD",
"icon": "i-lucide-zap",
"link": "/emergency/encounter"
},
{ {
"title": "Rawat Inap", "title": "Rawat Inap",
"icon": "i-lucide-building-2", "icon": "i-lucide-building-2",