517 lines
15 KiB
Vue
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>
|