Files
antrean-operasi/components/apps/patient/form/DataDiriSection.vue
2026-01-22 09:11:15 +07:00

517 lines
15 KiB
Vue

<template>
<div>
<!-- Section Header -->
<v-card-subtitle
class="bg-grey-lighten-4 text-subtitle-2 font-weight-medium py-2"
>
DATA DIRI
</v-card-subtitle>
<v-card-text>
<!-- Checkbox Bayi Baru Lahir -->
<v-row>
<v-col cols="12" class="text-right py-0">
<v-checkbox
v-model="isBayiBaru"
label="Bayi baru lahir"
density="compact"
hide-details
/>
</v-col>
</v-row>
<v-row>
<!-- Photo Upload -->
<v-col cols="12" md="2" class="text-center">
<v-avatar size="100" color="grey-lighten-3">
<v-icon size="50" color="grey">mdi-account</v-icon>
</v-avatar>
<v-btn
variant="text"
color="primary"
size="small"
class="mt-2"
@click="uploadPhoto"
>
Upload Foto
</v-btn>
</v-col>
<!-- Form Fields -->
<v-col cols="12" md="10">
<v-row>
<!-- Nama Lengkap -->
<v-col cols="12">
<v-text-field
v-model="localData.namaLengkap"
label="Nama Lengkap"
placeholder="Nama Lengkap"
variant="outlined"
density="compact"
:rules="[rules.required]"
required
@input="debouncedParseNameToFhir"
@paste="handlePaste"
/>
<!-- FHIR Name Preview - Auto shows when typing -->
<v-expand-transition>
<v-alert
v-if="fhirName && localData.namaLengkap && isTypingComplete"
density="compact"
type="info"
variant="tonal"
class="mt-2"
closable
@click:close="hideFhirPreview"
>
<template v-slot:title>
<span class="text-body-2">FHIR Name Structure</span>
</template>
<div class="text-caption mt-2">
<v-row dense>
<v-col
v-if="fhirName.prefix && fhirName.prefix.length > 0"
cols="12"
>
<v-chip
size="x-small"
color="primary"
variant="outlined"
class="mr-1"
>
Prefix
</v-chip>
<span class="font-weight-medium">{{
fhirName.prefix.join(", ")
}}</span>
</v-col>
<v-col
v-if="fhirName.given && fhirName.given.length > 0"
cols="12"
>
<v-chip
size="x-small"
color="success"
variant="outlined"
class="mr-1"
>
Given
</v-chip>
<span class="font-weight-medium">{{
fhirName.given.join(" ")
}}</span>
</v-col>
<v-col v-if="fhirName.family" cols="12">
<v-chip
size="x-small"
color="warning"
variant="outlined"
class="mr-1"
>
Family
</v-chip>
<span class="font-weight-medium">{{
fhirName.family
}}</span>
</v-col>
<v-col
v-if="fhirName.suffix && fhirName.suffix.length > 0"
cols="12"
>
<v-chip
size="x-small"
color="info"
variant="outlined"
class="mr-1"
>
Suffix
</v-chip>
<span class="font-weight-medium">{{
fhirName.suffix.join(", ")
}}</span>
</v-col>
</v-row>
</div>
</v-alert>
</v-expand-transition>
<!-- Optional: Inline chips preview -->
<div
v-if="
fhirName &&
localData.namaLengkap &&
isTypingComplete &&
!showDetailedPreview
"
class="mt-2"
>
<v-chip-group>
<v-chip
v-for="(prefix, index) in fhirName.prefix || []"
:key="`prefix-${index}`"
size="x-small"
color="primary"
>
{{ prefix }}
</v-chip>
<v-chip
v-for="(given, index) in fhirName.given || []"
:key="`given-${index}`"
size="x-small"
color="success"
>
{{ given }}
</v-chip>
<v-chip v-if="fhirName.family" size="x-small" color="warning">
{{ fhirName.family }}
</v-chip>
<v-chip
v-for="(suffix, index) in fhirName.suffix || []"
:key="`suffix-${index}`"
size="x-small"
color="info"
>
{{ suffix }}
</v-chip>
</v-chip-group>
</div>
</v-col>
<!-- Nomor Telepon Selular -->
<v-col cols="12" md="6">
<v-text-field
v-model="localData.nomorTeleponSelular"
label="Nomor Telepon Selular"
placeholder="Nomor Telepon Selular"
variant="outlined"
density="compact"
:rules="[rules.required, rules.phone]"
required
>
<template v-slot:prepend>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn
variant="outlined"
density="compact"
v-bind="props"
class="mr-2"
>
+62
<v-icon end>mdi-menu-down</v-icon>
</v-btn>
</template>
<v-list density="compact">
<v-list-item value="+62">+62</v-list-item>
</v-list>
</v-menu>
</template>
</v-text-field>
</v-col>
<!-- Nomor Telepon Rumah -->
<v-col cols="12" md="6">
<v-text-field
v-model="localData.nomorTeleponRumah"
label="Nomor Telepon Rumah"
placeholder="Nomor Telepon Rumah"
variant="outlined"
density="compact"
>
<template v-slot:prepend>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn
variant="outlined"
density="compact"
v-bind="props"
class="mr-2"
>
+62
<v-icon end>mdi-menu-down</v-icon>
</v-btn>
</template>
<v-list density="compact">
<v-list-item value="+62">+62</v-list-item>
</v-list>
</v-menu>
</template>
</v-text-field>
</v-col>
<!-- Email -->
<v-col cols="12" md="6">
<v-text-field
v-model="localData.email"
label="Email"
placeholder="Email"
type="email"
variant="outlined"
density="compact"
:rules="[rules.email]"
/>
</v-col>
<!-- Jenis Kelamin -->
<v-col cols="12" md="6">
<v-select
v-model="localData.jenisKelamin"
label="Jenis Kelamin"
:items="jenisKelaminOptions"
variant="outlined"
density="compact"
:rules="[rules.required]"
required
/>
</v-col>
<!-- Tempat Lahir -->
<v-col cols="12" md="6">
<v-text-field
v-model="localData.tempatLahir"
label="Tempat Lahir"
placeholder="Tempat Lahir"
variant="outlined"
density="compact"
:rules="[rules.required]"
required
/>
</v-col>
<!-- Tanggal Lahir -->
<v-col cols="12" md="6">
<v-text-field
v-model="localData.tanggalLahir"
label="Tanggal Lahir"
placeholder="Tanggal Lahir (DD/MM/YYYY)"
variant="outlined"
density="compact"
:rules="[rules.required]"
required
>
<template v-slot:append-inner>
<v-icon>mdi-calendar</v-icon>
</template>
</v-text-field>
</v-col>
<!-- Jenis Identitas -->
<v-col cols="12" md="6">
<v-select
v-model="localData.jenisIdentitas"
label="Jenis Identitas"
:items="jenisIdentitasOptions"
variant="outlined"
density="compact"
:rules="[rules.required]"
required
/>
</v-col>
<!-- Nomor Identitas -->
<v-col cols="12" md="6">
<v-text-field
v-model="localData.nomorIdentitas"
label="Nomor Identitas"
placeholder="Nomor Identitas"
variant="outlined"
density="compact"
:rules="[rules.required]"
required
/>
</v-col>
<!-- Nama Ibu Kandung -->
<v-col cols="12" md="6">
<v-text-field
v-model="localData.namaIbuKandung"
label="Nama Ibu Kandung"
placeholder="Nama Lengkap Ibu Kandung"
variant="outlined"
density="compact"
/>
</v-col>
<!-- No. Rekam Medis -->
<v-col cols="12" md="6">
<v-text-field
v-model="localData.nomorRekamMedis"
label="No. Rekam Medis Lama / Manual"
placeholder="No. Rekam medis"
variant="outlined"
density="compact"
/>
</v-col>
</v-row>
</v-col>
</v-row>
</v-card-text>
</div>
</template>
<script setup lang="ts">
import { ref, watch, computed } from "vue";
import { debounce } from "lodash-es"; // or use VueUse's useDebounceFn
import {
parseFhirHumanName,
validateFhirHumanName
} from "~/utils/module/fhirNameParser";
import type { FhirHumanName } from "~/types/fhir/humanName";
// Types
interface DataDiriForm {
photo: File | null;
namaLengkap: string;
nomorTeleponSelular: string;
nomorTeleponRumah: string;
email: string;
jenisKelamin: string;
tempatLahir: string;
tanggalLahir: string;
jenisIdentitas: string;
nomorIdentitas: string;
namaIbuKandung: string;
nomorRekamMedis: string;
fhirName?: FhirHumanName | null;
}
interface Props {
formData: DataDiriForm;
isBayiBaru: boolean;
}
// Props & Emits
const props = defineProps<Props>();
const emit = defineEmits<{
"update:formData": [value: DataDiriForm];
"update:isBayiBaru": [value: boolean];
}>();
// Reactive data
const localData = ref<DataDiriForm>({ ...props.formData });
const isBayiBaru = ref(props.isBayiBaru);
const fhirName = ref<FhirHumanName | null>(null);
const isTypingComplete = ref(false);
const showDetailedPreview = ref(true); // Toggle between detailed and chip view
// Rules
const rules = {
required: (v: string) => !!v || "Field ini wajib diisi",
email: (v: string) => !v || /.+@.+\..+/.test(v) || "Format email tidak valid",
phone: (v: string) =>
!v || /^[0-9+\-() ]+$/.test(v) || "Format nomor telepon tidak valid"
};
// Options
interface SelectOption {
title: string;
value: string;
}
const jenisKelaminOptions: SelectOption[] = [
{ title: "Laki-laki", value: "L" },
{ title: "Perempuan", value: "P" }
];
const jenisIdentitasOptions: SelectOption[] = [
{ title: "KTP", value: "KTP" },
{ title: "SIM", value: "SIM" },
{ title: "Paspor", value: "PASSPORT" }
];
// Methods
import { formatFhirName } from "~/utils/module/fhirNameParser";
const parseNameToFhir = (): void => {
if (localData.value.namaLengkap && localData.value.namaLengkap.trim()) {
fhirName.value = parseFhirHumanName(localData.value.namaLengkap);
if (fhirName.value) {
const errors = validateFhirHumanName(fhirName.value);
if (errors.length > 0) {
console.warn("FHIR Name validation errors:", errors);
}
// Store FHIR name in form data
localData.value.fhirName = fhirName.value;
// Format the FHIR name back to a string and update namaLengkap
const formattedName = formatFhirName(fhirName.value);
if (formattedName !== localData.value.namaLengkap) {
localData.value.namaLengkap = formattedName;
}
// Show preview after parsing
isTypingComplete.value = true;
}
} else {
// Clear FHIR name if input is empty
fhirName.value = null;
localData.value.fhirName = null;
isTypingComplete.value = false;
}
};
// Debounced version of parseNameToFhir (wait 500ms after user stops typing)
const debouncedParseNameToFhir = debounce(() => {
parseNameToFhir();
}, 500);
// Handle paste event
const handlePaste = (event: ClipboardEvent): void => {
// Reset typing complete flag
isTypingComplete.value = false;
// Parse after a short delay to ensure v-model is updated
setTimeout(() => {
parseNameToFhir();
}, 100);
};
// Hide FHIR preview
const hideFhirPreview = (): void => {
isTypingComplete.value = false;
};
const uploadPhoto = (): void => {
// Implementation for photo upload
console.log("Upload photo clicked");
};
// Watchers
watch(
localData,
(newVal) => {
emit("update:formData", newVal);
},
{ deep: true }
);
watch(isBayiBaru, (newVal) => {
emit("update:isBayiBaru", newVal);
});
// Reset typing complete when name is cleared
watch(
() => localData.value.namaLengkap,
(newVal) => {
if (!newVal) {
isTypingComplete.value = false;
}
}
);
</script>
<style scoped>
/* Optional: Add smooth transitions */
.v-chip-group {
gap: 4px;
}
</style>