Files
antrean-operasi/components/pendaftaran/BiodataPasien.vue
T

379 lines
15 KiB
Vue

<script setup lang="ts">
import api from '@/services/api';
import type { Props } from '~/types/common';
import type { PatientData } from '~/types/pendaftaran';
import { Icon } from '@iconify/vue';
const props = withDefaults(defineProps<Props>(), {
readonly: false
});
const formData = defineModel<{
noRekamMedis: string;
noKtp: string;
namaPasien: string;
jenisKelamin: string;
tanggalLahir: string;
umur: string;
alamat: string;
nomorTelepon: string[];
}>({ required: true });
const rules = {
required: (value: string) => !!value || 'Field ini wajib diisi'
};
// Autocomplete state
const patientItems = ref<PatientData[]>([]);
const loadingPatients = ref(false);
const searchTimeout = ref<NodeJS.Timeout | null>(null);
const selectedPatient = ref<PatientData | null>(null);
// Search patients by RM number or name
const searchPatients = async (search: string) => {
if (!search || search.length < 3) {
patientItems.value = [];
return;
}
loadingPatients.value = true;
try {
const response = await api.get('/reference/pasien', {
params: {
limit: 10,
search: search
}
});
if (response.data?.data) {
patientItems.value = response.data.data;
}
} catch (error) {
console.error('Error fetching patients:', error);
patientItems.value = [];
} finally {
loadingPatients.value = false;
}
};
// Handle search input with debounce
const handleSearchInput = (value: string) => {
if (value === formData.value.noRekamMedis) {
patientItems.value = selectedPatient.value ? [selectedPatient.value] : [];
return;
}
if (searchTimeout.value) {
clearTimeout(searchTimeout.value);
}
searchTimeout.value = setTimeout(() => {
searchPatients(value);
}, 500);
};
// Handle patient selection
const handlePatientSelect = (patient: PatientData | null) => {
if (patient) {
selectedPatient.value = patient;
formData.value.noRekamMedis = patient.nomr;
formData.value.noKtp = patient.nik;
formData.value.namaPasien = patient.nama;
formData.value.jenisKelamin = patient.jeniskelamin;
formData.value.tanggalLahir = patient.tgllahir;
formData.value.umur = patient.dataumur.label;
formData.value.alamat = patient.alamat;
}
};
// Fetch patient by RM number (for edit mode)
const fetchPatientByRm = async (rm: string) => {
if (!rm) return;
loadingPatients.value = true;
try {
const response = await api.get('/reference/pasien', {
params: {
limit: 1,
search: rm
}
});
if (response.data?.data && response.data.data.length > 0) {
const patient = response.data.data[0];
if (patient.nomr === rm) {
selectedPatient.value = patient;
patientItems.value = [patient];
}
}
} catch (error) {
console.error('Error fetching patient by RM:', error);
} finally {
loadingPatients.value = false;
}
};
// Watch for formData changes to load patient details in edit mode
watchEffect(async () => {
const rm = formData.value.noRekamMedis;
const nama = formData.value.namaPasien;
// Use nextTick to ensure all reactive updates are complete
await nextTick();
// If both RM and Nama are populated but selectedPatient is not, fetch patient details
if (rm && nama && !selectedPatient.value) {
await fetchPatientByRm(rm);
}
// Clear selectedPatient if RM is cleared
if (!rm && selectedPatient.value) {
selectedPatient.value = null;
patientItems.value = [];
}
});
// Phone validation rules
const phoneLength = (value: string) => {
if (!value) return true;
// Hapus karakter non-angka sebelum menghitung panjang
const length = value.replace(/[^0-9]/g, '').length;
return (length >= 9 && length <= 15) || 'Nomor telepon harus antara 9 hingga 15 digit.';
};
const indonesianPhoneFormat = (value: string) => {
if (!value) return true;
// Pola: Harus dimulai dengan '08', diikuti 7 hingga 13 digit angka
return /^08[0-9]{7,13}$/.test(value) || 'Format nomor telepon tidak valid (Contoh: 08xxxxxxxx).';
};
// Phone number input state
const newPhoneNumber = ref('');
const phoneError = ref<string | boolean>(true);
// Initialize nomorTelepon as array if not already
if (!Array.isArray(formData.value.nomorTelepon)) {
formData.value.nomorTelepon = [];
}
// Handle only numeric input and validate
const handlePhoneInput = (event: Event) => {
const input = event.target as HTMLInputElement;
const value = input.value;
// Only allow numbers
const numericValue = value.replace(/\D/g, '');
newPhoneNumber.value = numericValue;
// Validate format and length
const lengthValidation = phoneLength(numericValue);
const formatValidation = indonesianPhoneFormat(numericValue);
if (lengthValidation !== true) {
phoneError.value = lengthValidation;
} else if (formatValidation !== true) {
phoneError.value = formatValidation;
} else {
phoneError.value = true;
}
};
const addPhoneNumber = () => {
if (!newPhoneNumber.value.trim()) return;
// Validate before saving
const lengthValidation = phoneLength(newPhoneNumber.value);
const formatValidation = indonesianPhoneFormat(newPhoneNumber.value);
if (lengthValidation !== true || formatValidation !== true) {
return;
}
// Add to list
formData.value.nomorTelepon.push(newPhoneNumber.value);
// Reset input
newPhoneNumber.value = '';
phoneError.value = true;
};
const deletePhoneNumber = (index: number) => {
formData.value.nomorTelepon.splice(index, 1);
};
// Ref for no rekam medis input
const noRekamMedisInput = ref();
// Expose method to focus on no rekam medis
const focusNoRekamMedis = () => {
if (noRekamMedisInput.value) {
noRekamMedisInput.value.focus();
}
};
defineExpose({
focusNoRekamMedis
});
</script>
<template>
<v-card elevation="10">
<v-card-text class="pa-5">
<div class="d-flex align-center">
<!-- <Icon icon="solar:user-bold-duotone" height="24" class="mr-2" /> -->
<span class="text-h5">Biodata Pasien</span>
</div>
</v-card-text>
<v-divider> </v-divider>
<v-card-text>
<v-row>
<!-- No Rekam Medis -->
<v-col cols="12" md="12" class="">
<v-label class="mb-2">No Rekam Medis <span class="text-error">*</span></v-label>
<v-autocomplete ref="noRekamMedisInput" v-model="formData.noRekamMedis" :items="patientItems"
item-title="select" item-value="nomr" placeholder="Masukan Nomor RM / Nama Pasien"
variant="outlined" density="compact" :rules="[rules.required]" hide-details="auto" max="8"
:readonly="readonly" :disabled="readonly" :bg-color="readonly ? 'grey-lighten-3' : undefined"
:loading="loadingPatients" no-filter clearable @update:search="handleSearchInput"
@update:model-value="(value: string) => {
const patient = patientItems.find(p => p.nomr === value);
handlePatientSelect(patient || null);
}">
<template #no-data>
<v-list-item>
<v-list-item-title>
Cari No RM atau Nama Pasien
</v-list-item-title>
</v-list-item>
</template>
</v-autocomplete>
</v-col>
<template v-if="formData.noRekamMedis && selectedPatient">
<!-- No KTP -->
<v-col cols="12" md="6">
<v-label class="mb-2 font-weight-medium">No Ktp</v-label>
<v-text-field v-model="formData.noKtp" variant="outlined" density="compact" hide-details="auto"
readonly style="background-color: #f5f5f5; border-radius: 4px;"></v-text-field>
</v-col>
<!-- Nama Pasien -->
<v-col cols="12" md="6">
<v-label class="mb-2 font-weight-medium">Nama Pasien</v-label>
<v-text-field v-model="formData.namaPasien" variant="outlined" density="compact"
hide-details="auto" readonly
style="background-color: #f5f5f5; border-radius: 4px;"></v-text-field>
</v-col>
<!-- Jenis Kelamin -->
<v-col cols="12" md="6">
<v-label class="mb-2 font-weight-medium">Jenis Kelamin</v-label>
<v-text-field :model-value="formData.jenisKelamin === 'L' ? 'Laki-laki' : 'Perempuan'"
variant="outlined" density="compact" hide-details="auto" readonly
style="background-color: #f5f5f5; border-radius: 4px;"></v-text-field>
<!-- <v-btn-group
v-model="formData.jenisKelamin"
divided
density="compact"
class="w-100"
:disabled="readonly"
>
<v-btn
:color="formData.jenisKelamin === 'L' ? 'info' : 'default'"
:variant="formData.jenisKelamin === 'L' ? 'flat' : 'outlined'"
class="flex-1-1"
@click="formData.jenisKelamin === 'L'"
>
<v-icon icon="mdi-gender-male"></v-icon>
Laki-laki
</v-btn>
<v-btn
:color="formData.jenisKelamin === 'P' ? 'error' : 'default'"
:variant="formData.jenisKelamin === 'P' ? 'flat' : 'outlined'"
class="flex-1-1"
@click="formData.jenisKelamin === 'P'"
>
<v-icon icon="mdi-gender-female"></v-icon>
Perempuan
</v-btn>
</v-btn-group> -->
</v-col>
<!-- Tanggal Lahir -->
<v-col cols="12" md="6">
<v-label class="mb-2 font-weight-medium">Tanggal Lahir</v-label>
<v-text-field v-model="formData.tanggalLahir" type="date" variant="outlined" density="compact"
hide-details="auto" readonly
style="background-color: #f5f5f5; border-radius: 4px;"></v-text-field>
</v-col>
<!-- Umur -->
<!-- <v-col cols="12" md="6">
<v-label class="mb-2 font-weight-medium">Umur</v-label>
<v-text-field v-model="formData.umur" variant="outlined" density="compact"
hide-details="auto" readonly style="background-color: #f5f5f5; border-radius: 4px;"></v-text-field>
</v-col> -->
<!-- Alamat -->
<v-col cols="12">
<v-label class="mb-2 font-weight-medium">Alamat</v-label>
<v-textarea v-model="formData.alamat" variant="outlined" rows="3" hide-details="auto" readonly
style="background-color: #f5f5f5; border-radius: 4px;"></v-textarea>
</v-col>
<!-- Nomor Telepon -->
<v-col cols="12">
<v-label class="mb-2 font-weight-medium">Nomor Telepon</v-label>
<!-- Input field for adding phone number -->
<div v-if="!readonly" class="mb-3">
<div class="d-flex gap-2">
<v-text-field v-model="newPhoneNumber" placeholder="Contoh: 08123456789"
variant="outlined" density="compact" type="tel" hide-details="auto"
@input="handlePhoneInput" @keyup.enter="addPhoneNumber"
:error-messages="phoneError !== true && newPhoneNumber ? [phoneError as string] : []"
class="flex-1-1">
</v-text-field>
<v-btn color="primary" variant="flat" size="large" @click="addPhoneNumber"
:disabled="!newPhoneNumber.trim() || phoneError !== true">
<v-icon>mdi-check</v-icon>
</v-btn>
</div>
<div class="text-caption text-medium-emphasis mt-2">
Format: 08xxxxxxxxx (9-15 digit)
</div>
</div>
<!-- Alert when no phone numbers -->
<v-alert v-if="formData.nomorTelepon.length === 0" type="info" variant="tonal" class="mb-3"
density="compact">
<div class="d-flex align-center">
<span>Tidak ada nomor telepon</span>
</div>
</v-alert>
<!-- List of phone numbers -->
<v-list v-if="formData.nomorTelepon.length > 0" density="compact">
<v-list-item v-for="(phone, index) in formData.nomorTelepon" :key="index" class="px-4 mb-2"
border>
<template #default>
<div class="d-flex align-center justify-space-between w-100">
<span>{{ phone }}</span>
<v-btn v-if="!readonly" icon size="small" variant="text" color="error"
@click="deletePhoneNumber(index)">
<v-icon>mdi-delete</v-icon>
</v-btn>
</div>
</template>
</v-list-item>
</v-list>
</v-col>
</template>
</v-row>
</v-card-text>
</v-card>
</template>