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