fix(FE) : fix review style

This commit is contained in:
Yusron alamsyah
2026-02-23 10:09:13 +07:00
parent fa7b6d2be3
commit b3ac3a6b1a
22 changed files with 712 additions and 339 deletions

No files matched your search

+98 -120
View File
@@ -2,6 +2,7 @@
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
@@ -36,7 +37,7 @@ const searchPatients = async (search: string) => {
}
loadingPatients.value = true;
try {
const response = await api.get('/reference/pasien', {
params: {
@@ -58,7 +59,7 @@ const searchPatients = async (search: string) => {
// Handle search input with debounce
const handleSearchInput = (value: string) => {
if(value === formData.value.noRekamMedis) {
if (value === formData.value.noRekamMedis) {
patientItems.value = selectedPatient.value ? [selectedPatient.value] : [];
return;
}
@@ -88,9 +89,9 @@ const handlePatientSelect = (patient: PatientData | null) => {
// 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: {
@@ -117,15 +118,15 @@ const fetchPatientByRm = async (rm: string) => {
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;
@@ -160,15 +161,15 @@ if (!Array.isArray(formData.value.nomorTelepon)) {
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) {
@@ -180,18 +181,18 @@ const handlePhoneInput = (event: Event) => {
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;
@@ -200,73 +201,77 @@ const addPhoneNumber = () => {
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="6" class="">
<v-label class="mb-2 font-weight-medium">No Rekam Medis <span
class="text-error">*</span></v-label>
<v-autocomplete
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"
<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
Cari No RM atau Nama Pasien
</v-list-item-title>
</v-list-item>
</template>
</v-autocomplete>
</v-col>
<v-col cols="12" md="6">
</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-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>
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-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
@@ -300,8 +305,8 @@ const deletePhoneNumber = (index: number) => {
<!-- 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
<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>
@@ -315,84 +320,57 @@ const deletePhoneNumber = (index: number) => {
<!-- 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-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>
<v-label class="mb-2 font-weight-medium">Nomor Telepon</v-label>
<!-- 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>
<!-- 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>
</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>
<!-- 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>
+69 -39
View File
@@ -2,6 +2,7 @@
import api from '@/services/api';
import type { Props } from '~/types/common';
import type { DiagnosisItem, TindakanItem, DiagnosisApiData, TindakanApiData } from '~/types/pendaftaran';
import { Icon } from '@iconify/vue';
const props = withDefaults(defineProps<Props>(), {
readonly: false
@@ -239,67 +240,95 @@ const handleKodeTindakanChange = (kode: string) => {
tindakanForm.value.tindakan = found.keterangan;
}
};
// Refs for focus functionality
const diagnosisSection = ref();
const tindakanSection = ref();
const diagnosisInput = ref();
const tindakanInput = ref();
// Expose methods to focus on fields
const focusDiagnosis = () => {
if (diagnosisSection.value) {
diagnosisSection.value.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => {
if (diagnosisInput.value) {
diagnosisInput.value.focus();
}
}, 300);
}
};
const focusTindakan = () => {
if (tindakanSection.value) {
tindakanSection.value.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => {
if (tindakanInput.value) {
tindakanInput.value.focus();
}
}, 300);
}
};
defineExpose({
focusDiagnosis,
focusTindakan
});
</script>
<template>
<v-card elevation="10">
<v-divider></v-divider>
<v-card-item>
<h5 class="text-h5 font-weight-bold">Data Medis Pasien</h5>
</v-card-item>
<v-card-text class="pa-5">
<div class="d-flex align-center">
<!-- <Icon icon="solar:medical-kit-bold-duotone" height="24" class="mr-2" /> -->
<span class="text-h5">Data Medis Pasien</span>
</div>
</v-card-text>
<v-divider> </v-divider>
<v-card-text>
<!-- Diagnosa -->
<div class="mb-6">
<h6 class="text-h6 font-weight-medium mb-3">Diagnosa</h6>
<div ref="diagnosisSection" class="mb-6">
<!-- Form Input Diagnosa -->
<v-card v-if="!readonly" variant="outlined" class="mb-4 pa-4" color="blue-lighten-5">
<h6 class="text-h6 font-weight-bold mb-6">Diagnosa</h6>
<v-row>
<!-- Jenis Diagnosa -->
<v-col cols="12" md="3">
<v-col cols="12" md="6">
<v-label class="mb-2 font-weight-medium text-caption">
Jenis Diagnosa <span class="text-error">*</span>
</v-label>
<v-select
v-model="diagnosisForm.jenisDiagnosa"
:items="['utama', 'tambahan']"
variant="outlined"
density="compact"
hide-details="auto"
></v-select>
<!-- <v-btn-group
v-model="diagnosisForm.jenisDiagnosa"
variant="outlined"
density="compact"
>
<div class="d-flex ga-2">
<v-btn
:color="diagnosisForm.jenisDiagnosa === 'utama' ? 'primary' : 'light'"
variant="flat"
size="small"
:color="diagnosisForm.jenisDiagnosa === 'utama' ? 'primary' : 'default'"
:variant="diagnosisForm.jenisDiagnosa === 'utama' ? 'flat' : 'outlined'"
size="large"
class="flex-grow-1 text-none"
@click="diagnosisForm.jenisDiagnosa = 'utama'"
:value="'utama'"
:prepend-icon="diagnosisForm.jenisDiagnosa === 'utama' ? 'mdi-radiobox-marked' : 'mdi-radiobox-blank'"
>
Diagnosa Utama
Diagnosa utama
</v-btn>
<v-btn
:color="diagnosisForm.jenisDiagnosa === 'tambahan' ? 'primary' : 'light'"
variant="flat"
size="small"
:color="diagnosisForm.jenisDiagnosa === 'tambahan' ? 'primary' : 'default'"
:variant="diagnosisForm.jenisDiagnosa === 'tambahan' ? 'flat' : 'outlined'"
size="large"
class="flex-grow-1 text-none"
@click="diagnosisForm.jenisDiagnosa = 'tambahan'"
:value="'tambahan'"
:prepend-icon="diagnosisForm.jenisDiagnosa === 'tambahan' ? 'mdi-radiobox-marked' : 'mdi-radiobox-blank'"
>
Diagnosa Tambahan
Diagnosa tambahan
</v-btn>
</v-btn-group> -->
</div>
</v-col>
<!-- Kode Diagnosa -->
<v-col cols="12" md="4">
<v-col cols="12" md="6">
<v-label class="mb-2 font-weight-medium text-caption">
Kode Diagnosa <span class="text-error">*</span>
</v-label>
<v-autocomplete
ref="diagnosisInput"
v-model="diagnosisForm.kodeDiagnosa"
:items="diagnosisOptions"
placeholder="Ketik min. 3 karakter..."
@@ -324,7 +353,7 @@ const handleKodeTindakanChange = (kode: string) => {
</v-col>
<!-- Diagnosa -->
<v-col cols="12" md="5">
<v-col cols="12" md="12">
<v-label class="mb-2 font-weight-medium text-caption">
Diagnosa <span class="text-error">*</span>
</v-label>
@@ -413,7 +442,7 @@ const handleKodeTindakanChange = (kode: string) => {
<div class="d-flex ga-1">
<v-btn
icon
size="x-small"
size="small"
variant="text"
color="primary"
@click="editDiagnosis(index)"
@@ -422,7 +451,7 @@ const handleKodeTindakanChange = (kode: string) => {
</v-btn>
<v-btn
icon
size="x-small"
size="small"
variant="text"
color="error"
@click="deleteDiagnosis(index)"
@@ -438,11 +467,11 @@ const handleKodeTindakanChange = (kode: string) => {
</div>
<!-- Tindakan -->
<div>
<h6 class="text-h6 font-weight-medium mb-3">Tindakan</h6>
<div ref="tindakanSection">
<!-- Form Input Tindakan -->
<v-card v-if="!readonly" variant="outlined" class="mb-4 pa-4" color="purple-lighten-5">
<h6 class="text-h6 font-weight-bold mb-6">Tindakan</h6>
<v-row>
<!-- Kode Tindakan -->
<v-col cols="12" md="6">
@@ -450,6 +479,7 @@ const handleKodeTindakanChange = (kode: string) => {
Kode Tindakan <span class="text-error">*</span>
</v-label>
<v-autocomplete
ref="tindakanInput"
v-model="tindakanForm.kodeTindakan"
:items="tindakanOptions"
placeholder="Ketik min. 3 karakter..."
@@ -571,7 +601,7 @@ const handleKodeTindakanChange = (kode: string) => {
<div class="d-flex ga-1">
<v-btn
icon
size="x-small"
size="small"
variant="text"
color="primary"
@click="editTindakan(index)"
@@ -580,7 +610,7 @@ const handleKodeTindakanChange = (kode: string) => {
</v-btn>
<v-btn
icon
size="x-small"
size="small"
variant="text"
color="error"
@click="deleteTindakan(index)"
@@ -226,22 +226,21 @@ const closeModal = () => {
<template>
<v-dialog v-model="showModal" max-width="900px" persistent scrollable>
<v-card>
<v-card-title class="d-flex align-center justify-space-between pa-4 bg-primary text-white">
<v-card-title class="d-flex align-center justify-space-between pa-4">
<div class="d-flex align-center">
<Icon icon="solar:clipboard-text-bold-duotone" height="28" class="mr-3" />
<Icon icon="solar:clipboard-text-bold-duotone" height="28" class="text-primary mr-3" />
<span class="text-h6">Detail Pendaftaran Antrean Operasi</span>
</div>
<v-btn
icon
variant="text"
color="white"
@click="closeModal"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text class="pa-4" style="max-height: 75vh; overflow-y: auto;">
<v-card-text class="pa-5 bg-background" style="max-height: 75vh; overflow-y: auto;">
<LoadingState
:loading="loading"
:error="error"
@@ -249,7 +248,7 @@ const closeModal = () => {
error-text="Gagal memuat data detail"
@retry="fetchDetailData"
>
<!-- Status Badge -->
<!-- Status Badge
<v-row class="mb-2">
<v-col cols="12">
<v-chip
@@ -262,17 +261,17 @@ const closeModal = () => {
Status: {{ getStatusText(statusPasienData.statusOperasi) }}
</v-chip>
</v-col>
</v-row>
</v-row> -->
<!-- Informasi Pasien -->
<v-card elevation="2" class="mb-4">
<v-card-title class="bg-blue-lighten-5 pa-3">
<v-card elevation="10" class="mb-4">
<v-card-title class="bg-blue-lighten-5 pa-4">
<div class="d-flex align-center">
<Icon icon="solar:user-bold-duotone" height="24" class="mr-2 text-primary" />
<span class="text-h6">Informasi Pasien</span>
<span class="text-h6">Biodata Pasien</span>
</div>
</v-card-title>
<v-card-text class="pa-4">
<v-card-text class="pa-5">
<v-row>
<v-col cols="12" md="6">
<div class="info-item">
@@ -280,7 +279,7 @@ const closeModal = () => {
<Icon icon="mdi-card-account-details" height="20" class="mr-2" />
No Rekam Medis
</div>
<div class="info-value font-weight-bold text-primary">
<div class="info-value font-weight-bold">
{{ formData.noRekamMedis || '-' }}
</div>
</div>
@@ -385,8 +384,8 @@ const closeModal = () => {
</v-card>
<!-- Diagnosis -->
<v-card elevation="2" class="mb-4">
<v-card-title class="bg-green-lighten-5 pa-3">
<v-card elevation="10" class="mb-4">
<v-card-title class="bg-green-lighten-5 pa-4">
<div class="d-flex align-center">
<Icon icon="solar:health-bold-duotone" height="24" class="mr-2 text-success" />
<span class="text-h6">Diagnosis</span>
@@ -406,12 +405,12 @@ const closeModal = () => {
{{ index + 1 }}
</v-avatar>
<div class="flex-1-1">
<div class="text-caption text-medium-emphasis mb-1">
{{ item.jenisDiagnosa }}
</div>
<div class="font-weight-medium mb-1">
{{ item.diagnosa }}
</div>
<div class="text-caption text-medium-emphasis mb-1">
{{ item.jenisDiagnosa }}
</div>
<v-chip size="x-small" color="success" variant="outlined">
{{ item.kodeDiagnosa }}
</v-chip>
@@ -428,10 +427,10 @@ const closeModal = () => {
</v-card>
<!-- Tindakan -->
<v-card elevation="2" class="mb-4">
<v-card-title class="bg-purple-lighten-5 pa-3">
<v-card elevation="10" class="mb-4">
<v-card-title class="bg-purple-lighten-5 pa-4">
<div class="d-flex align-center">
<Icon icon="solar:medical-kit-bold-duotone" height="24" class="mr-2 text-purple" />
<Icon icon="solar:medical-kit-bold-duotone" height="24" class="mr-2 text-secondary" />
<span class="text-h6">Tindakan Operasi</span>
</div>
</v-card-title>
@@ -442,10 +441,10 @@ const closeModal = () => {
:key="index"
class="px-0 mb-3"
>
<v-card variant="tonal" color="purple">
<v-card variant="tonal" color="secondary">
<v-card-text class="pa-3">
<div class="d-flex align-start">
<v-avatar color="purple" size="32" class="mr-3">
<v-avatar color="secondary" size="32" class="mr-3">
{{ index + 1 }}
</v-avatar>
<div class="flex-1-1">
@@ -471,10 +470,10 @@ const closeModal = () => {
</v-card>
<!-- Rencana Operasi -->
<v-card elevation="2" class="mb-4">
<v-card-title class="bg-orange-lighten-5 pa-3">
<v-card elevation="10" class="mb-4">
<v-card-title class="bg-orange-lighten-5 pa-4">
<div class="d-flex align-center">
<Icon icon="solar:calendar-mark-bold-duotone" height="24" class="mr-2 text-orange" />
<Icon icon="solar:calendar-mark-bold-duotone" height="24" class="mr-2 text-warning" />
<span class="text-h6">Rencana Operasi</span>
</div>
</v-card-title>
@@ -526,10 +525,21 @@ const closeModal = () => {
</div>
</div>
</v-col>
<v-col v-if="rencanaOperasiData.keterangan" cols="12">
<v-col v-if="rencanaOperasiData.rencanaOperasi" cols="6">
<div class="info-item">
<div class="info-label">
<Icon icon="solar:notes-bold" height="20" class="mr-2" />
Rencana Operasi
</div>
<div class="info-value">
{{ rencanaOperasiData.rencanaOperasi }}
</div>
</div>
</v-col>
<v-col v-if="rencanaOperasiData.keterangan" cols="6">
<div class="info-item">
<div class="info-label">
<Icon icon="solar:notebook-bold" height="20" class="mr-2" />
Keterangan
</div>
<div class="info-value">
@@ -542,14 +552,14 @@ const closeModal = () => {
</v-card>
<!-- Dokter Pelaksana -->
<v-card elevation="2" class="mb-4">
<v-card-title class="bg-indigo-lighten-5 pa-3">
<v-card elevation="10" class="mb-4">
<v-card-title class="bg-indigo-lighten-5 pa-4">
<div class="d-flex align-center">
<Icon icon="solar:user-id-bold-duotone" height="24" class="mr-2 text-indigo" />
<Icon icon="solar:user-id-bold-duotone" height="24" class="mr-2 text-info" />
<span class="text-h6">Tim Dokter Pelaksana</span>
</div>
</v-card-title>
<v-card-text class="pa-2">
<v-card-text class="pa-4">
<v-list v-if="dokterPelaksanaItems.length > 0" class="pa-0">
<v-list-item
v-for="(dokter, index) in dokterPelaksanaItems"
@@ -583,8 +593,8 @@ const closeModal = () => {
</v-card>
<!-- Status Operasi -->
<v-card elevation="2" class="mb-2">
<v-card-title class="bg-grey-lighten-3 pa-3">
<v-card elevation="10" class="mb-2">
<v-card-title class="bg-grey-lighten-3 pa-4">
<div class="d-flex align-center">
<Icon icon="solar:clipboard-check-bold-duotone" height="24" class="mr-2 text-grey-darken-2" />
<span class="text-h6">Status Operasi</span>
+56 -16
View File
@@ -49,6 +49,11 @@ const valid = ref(true);
const loadingDetail = ref(false);
const errorDetail = ref(false);
// Refs for child components
const biodataPasienRef = ref();
const medisPasienRef = ref();
const rencanaOperasiRef = ref();
// Computed properties
const isReadonly = computed(() => props.mode === 'view');
@@ -183,6 +188,38 @@ const closeModal = () => {
showModal.value = false;
};
// Focus on error field - scroll to component and focus on first invalid field
const focusOnErrorField = (fieldName: string) => {
nextTick(() => {
// Map field names to component refs and actions
const fieldMap: Record<string, { ref: any; action: string }> = {
'noRekamMedis': { ref: biodataPasienRef, action: 'focusNoRekamMedis' },
'diagnosis': { ref: medisPasienRef, action: 'focusDiagnosis' },
'tindakan': { ref: medisPasienRef, action: 'focusTindakan' },
'spesialis': { ref: rencanaOperasiRef, action: 'focusSpesialis' },
'subSpesialis': { ref: rencanaOperasiRef, action: 'focusSubSpesialis' },
'tanggalDaftar': { ref: rencanaOperasiRef, action: 'focusTanggalDaftar' },
'kategoriOperasi': { ref: rencanaOperasiRef, action: 'focusKategoriOperasi' },
};
const fieldInfo = fieldMap[fieldName];
if (fieldInfo && fieldInfo.ref.value) {
// Scroll to component
const componentElement = fieldInfo.ref.value.$el;
if (componentElement) {
componentElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Call focus method if available
setTimeout(() => {
if (typeof fieldInfo.ref.value[fieldInfo.action] === 'function') {
fieldInfo.ref.value[fieldInfo.action]();
}
}, 300); // Wait for scroll to complete
}
}
});
};
const handleSimpan = async () => {
const { valid: isValid } = await form.value.validate();
@@ -193,6 +230,11 @@ const handleSimpan = async () => {
if (!validation.valid) {
const errorMessage = `Data berikut harus diisi:\n\n${validation.errors.join(', ')}`;
pendaftaranStore.showSnackbar(errorMessage, 'error');
// Focus on first error field
if (validation.firstErrorField) {
focusOnErrorField(validation.firstErrorField);
}
return;
}
@@ -210,6 +252,11 @@ const handleSimpan = async () => {
const validation = pendaftaranStore.validateForm();
const errorMessage = `Data berikut harus diisi:\n\n${validation.errors.join(', ')}`;
pendaftaranStore.showSnackbar(errorMessage, 'error');
// Focus on first error field
if (validation.firstErrorField) {
focusOnErrorField(validation.firstErrorField);
}
}
};
@@ -230,11 +277,11 @@ const handleReset = () => {
</script>
<template>
<v-dialog v-model="showModal" max-width="1100px" persistent scrollable>
<v-dialog v-model="showModal" max-width="900px" persistent scrollable>
<v-card>
<v-card-title class="d-flex align-center justify-space-between pa-3 bg-primary text-white">
<v-card-title class="d-flex align-center justify-space-between pa-3">
<div class="d-flex align-center">
<v-icon class="mr-3">mdi-clipboard-plus</v-icon>
<v-icon class="mr-3 text-primary">mdi-clipboard-plus</v-icon>
<span class="text-h6">{{ modalTitle }}</span>
</div>
<v-btn
@@ -246,7 +293,7 @@ const handleReset = () => {
</v-btn>
</v-card-title>
<v-card-text class="pa-4" style="max-height: 70vh; overflow-y: auto; overflow-x: hidden;">
<v-card-text class="bg-background pa-5" style="max-height: 70vh; overflow-y: auto; overflow-x: hidden;">
<LoadingState
:loading="loadingDetail"
:error="errorDetail"
@@ -257,13 +304,14 @@ const handleReset = () => {
<v-form ref="form" v-model="valid" lazy-validation>
<v-row no-gutters>
<!-- Data Biodata Pasien -->
<v-col cols="12">
<BiodataPasien v-model="formData" :readonly="isReadonly" />
<v-col cols="12" class="mb-5">
<BiodataPasien ref="biodataPasienRef" v-model="formData" :readonly="isReadonly" />
</v-col>
<!-- Data Medis Pasien -->
<v-col cols="12">
<v-col cols="12" class="mb-5">
<MedisPasien
ref="medisPasienRef"
v-model:diagnosis-items="diagnosisItems"
v-model:tindakan-items="tindakanItems"
:readonly="isReadonly"
@@ -273,6 +321,7 @@ const handleReset = () => {
<!-- Data Rencana Operasi -->
<v-col cols="12">
<RencanaOperasi
ref="rencanaOperasiRef"
v-model:rencana-operasi-data="rencanaOperasiData"
v-model:dokter-pelaksana-items="dokterPelaksanaItems"
:readonly="isReadonly"
@@ -284,15 +333,6 @@ const handleReset = () => {
</v-card-text>
<v-card-actions class="pa-4 justify-end">
<v-btn
color="default"
size="large"
variant="outlined"
prepend-icon="mdi-close"
@click="closeModal"
>
{{ isReadonly ? 'Tutup' : 'Batal' }}
</v-btn>
<v-btn
v-if="!isReadonly"
color="error"
+117 -69
View File
@@ -166,24 +166,24 @@ watch(() => props.modelValue, (isOpen) => {
</script>
<template>
<v-dialog v-model="dialog" max-width="800" persistent scrollable>
<v-dialog v-model="dialog" max-width="900px" persistent scrollable>
<v-card>
<v-card-title class="d-flex align-center justify-space-between bg-primary pa-4">
<v-card-title class="d-flex align-center justify-space-between pa-4">
<div class="d-flex align-center">
<Icon icon="solar:clipboard-check-bold" height="24" class="mr-2" />
<span class="text-h5">Update Status Antrian Operasi</span>
<Icon icon="solar:clipboard-check-bold-duotone" height="28" class="text-primary mr-3" />
<span class="text-h6">Update Status Antrian Operasi</span>
</div>
<v-btn
icon
variant="text"
size="small"
@click="dialog = false"
:disabled="loadingSubmit"
>
<Icon icon="mdi-close" height="20" />
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text class="pa-4" style="max-height: 600px;">
<v-card-text class="pa-5 bg-background" style="max-height: 75vh; overflow-y: auto;">
<!-- Loading State -->
<div v-if="loading" class="text-center py-8">
<v-progress-circular
@@ -207,62 +207,96 @@ watch(() => props.modelValue, (isOpen) => {
<!-- Content -->
<div v-else-if="pasienData">
<!-- Informasi Pasien -->
<v-card variant="outlined" class="mb-6">
<v-card-item>
<h6 class="text-h6 font-weight-bold mb-4">
<Icon icon="solar:user-bold" height="20" class="mr-2" />
Informasi Pasien
</h6>
</v-card-item>
<v-divider></v-divider>
<v-card-text >
<v-card elevation="2" class="mb-4">
<v-card-title class="bg-blue-lighten-5 pa-4">
<div class="d-flex align-center">
<Icon icon="solar:user-bold-duotone" height="24" class="mr-2 text-primary" />
<span class="text-h6">Informasi Pasien</span>
</div>
</v-card-title>
<v-card-text class="pa-5">
<v-row>
<v-col cols="12" sm="6">
<div class="mb-3">
<div class="text-caption text-medium-emphasis">No. Rekam Medis</div>
<div class="text-body-1 font-weight-medium">{{ pasienData.formData?.noRekamMedis || '-' }}</div>
<div class="info-item">
<div class="info-label">
<Icon icon="mdi-card-account-details" height="20" class="mr-2" />
No. Rekam Medis
</div>
<div class="info-value font-weight-bold">
{{ pasienData.formData?.noRekamMedis || '-' }}
</div>
</div>
</v-col>
<v-col cols="12" sm="6">
<div class="mb-3">
<div class="text-caption text-medium-emphasis">Nama Pasien</div>
<div class="text-body-1 font-weight-medium">{{ pasienData.formData?.namaPasien || '-' }}</div>
<div class="info-item">
<div class="info-label">
<Icon icon="solar:user-heart-bold" height="20" class="mr-2" />
Nama Pasien
</div>
<div class="info-value font-weight-bold">
{{ pasienData.formData?.namaPasien || '-' }}
</div>
</div>
</v-col>
<v-col cols="12" sm="6">
<div class="mb-3">
<div class="text-caption text-medium-emphasis">Jenis Kelamin</div>
<div class="text-body-1">{{ getGenderText(pasienData.formData?.jenisKelamin) }}</div>
<div class="info-item">
<div class="info-label">
<Icon icon="mdi-gender-male-female" height="20" class="mr-2" />
Jenis Kelamin
</div>
<div class="info-value">
{{ getGenderText(pasienData.formData?.jenisKelamin) }}
</div>
</div>
</v-col>
<v-col cols="12" sm="6">
<div class="mb-3">
<div class="text-caption text-medium-emphasis">Tanggal Daftar</div>
<div class="text-body-1">{{ formatDate(pasienData.rencanaOperasiData?.tanggalDaftar) }}</div>
<div class="info-item">
<div class="info-label">
<Icon icon="solar:calendar-date-bold" height="20" class="mr-2" />
Tanggal Daftar
</div>
<div class="info-value">
{{ formatDate(pasienData.rencanaOperasiData?.tanggalDaftar) }}
</div>
</div>
</v-col>
<v-col cols="12" sm="4">
<div class="mb-3">
<div class="text-caption text-medium-emphasis">Kategori</div>
<v-chip size="small" color="primary" variant="tonal">
{{ pasienData.rencanaOperasiData?.kategoriName || '-' }}
</v-chip>
<div class="info-item">
<div class="info-label">
<Icon icon="solar:clipboard-list-bold" height="20" class="mr-2" />
Kategori
</div>
<div class="info-value">
<v-chip size="small" color="primary" variant="flat">
{{ pasienData.rencanaOperasiData?.kategoriName || '-' }}
</v-chip>
</div>
</div>
</v-col>
<v-col cols="12" sm="4">
<div class="mb-3">
<div class="text-caption text-medium-emphasis">Spesialis</div>
<v-chip size="small" color="secondary" variant="tonal">
{{ pasienData.rencanaOperasiData?.SpesialisName || '-' }}
</v-chip>
<div class="info-item">
<div class="info-label">
<Icon icon="solar:stethoscope-bold" height="20" class="mr-2" />
Spesialis
</div>
<div class="info-value">
<v-chip size="small" color="secondary" variant="flat">
{{ pasienData.rencanaOperasiData?.SpesialisName || '-' }}
</v-chip>
</div>
</div>
</v-col>
<v-col cols="12" sm="4">
<div class="mb-3">
<div class="text-caption text-medium-emphasis">Sub Spesialis</div>
<v-chip size="small" color="info" variant="tonal">
{{ pasienData.rencanaOperasiData?.SubSpesialisName || '-' }}
</v-chip>
<div class="info-item">
<div class="info-label">
<Icon icon="solar:health-bold" height="20" class="mr-2" />
Sub Spesialis
</div>
<div class="info-value">
<v-chip size="small" color="info" variant="flat">
{{ pasienData.rencanaOperasiData?.SubSpesialisName || '-' }}
</v-chip>
</div>
</div>
</v-col>
</v-row>
@@ -270,15 +304,14 @@ watch(() => props.modelValue, (isOpen) => {
</v-card>
<!-- Form Update Status -->
<v-card variant="outlined">
<v-card-item>
<h6 class="text-h6 font-weight-bold mb-4">
<Icon icon="solar:settings-bold" height="20" class="mr-2" />
Update Status Operasi
</h6>
</v-card-item>
<v-divider></v-divider>
<v-card-text>
<v-card elevation="2" class="mb-2">
<v-card-title class="bg-orange-lighten-5 pa-4">
<div class="d-flex align-center">
<Icon icon="solar:settings-bold-duotone" height="24" class="mr-2 text-warning" />
<span class="text-h6">Update Status Operasi</span>
</div>
</v-card-title>
<v-card-text class="pa-4">
<v-row>
<!-- Status Operasi -->
<v-col cols="12">
@@ -299,6 +332,7 @@ watch(() => props.modelValue, (isOpen) => {
:color="formData.statusOperasi === STATUS.BELUM ? 'primary' : 'default'"
@click="formData.statusOperasi = STATUS.BELUM"
>
<Icon v-if="formData.statusOperasi === STATUS.BELUM" icon="mdi-progress-clock" height="20" class="mr-2" />
Belum
</v-btn>
<v-btn
@@ -308,6 +342,7 @@ watch(() => props.modelValue, (isOpen) => {
:color="formData.statusOperasi === STATUS.SELESAI ? 'success' : 'default'"
@click="formData.statusOperasi = STATUS.SELESAI"
>
<Icon v-if="formData.statusOperasi === STATUS.SELESAI" icon="mdi-check-circle" height="20" class="mr-2" />
Selesai
</v-btn>
<v-btn
@@ -317,6 +352,7 @@ watch(() => props.modelValue, (isOpen) => {
:color="formData.statusOperasi === STATUS.TUNDA ? 'warning' : 'default'"
@click="formData.statusOperasi = STATUS.TUNDA"
>
<Icon v-if="formData.statusOperasi === STATUS.TUNDA" icon="mdi-timer-sand" height="20" class="mr-2" />
Tunda
</v-btn>
<v-btn
@@ -326,6 +362,7 @@ watch(() => props.modelValue, (isOpen) => {
:color="formData.statusOperasi === STATUS.BATAL ? 'error' : 'default'"
@click="formData.statusOperasi = STATUS.BATAL"
>
<Icon v-if="formData.statusOperasi === STATUS.BATAL" icon="mdi-close-circle" height="20" class="mr-2" />
Batal
</v-btn>
</v-btn-group>
@@ -369,27 +406,16 @@ watch(() => props.modelValue, (isOpen) => {
</div>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn
variant="outlined"
@click="dialog = false"
:disabled="loadingSubmit"
size="large"
prepend-icon="mdi-close"
>
Batal
</v-btn>
<v-card-actions class="pa-4 justify-end">
<v-btn
color="primary"
@click="handleSubmit"
size="large"
variant="flat"
prepend-icon="mdi-content-save"
:loading="loadingSubmit"
:disabled="loading || !isFormValid"
variant="flat"
size="large"
prepend-icon="mdi-content-save"
@click="handleSubmit"
>
Simpan
</v-btn>
@@ -415,3 +441,25 @@ watch(() => props.modelValue, (isOpen) => {
</v-snackbar>
</v-dialog>
</template>
<style scoped>
.info-item {
margin-bottom: 1rem;
}
.info-label {
display: flex;
align-items: center;
font-size: 0.875rem;
color: rgb(var(--v-theme-on-surface));
opacity: 0.7;
margin-bottom: 0.5rem;
font-weight: 500;
}
.info-value {
font-size: 1rem;
color: rgb(var(--v-theme-on-surface));
padding-left: 28px;
}
</style>
+54 -8
View File
@@ -4,6 +4,7 @@ import api from '~/services/api';
import type { Props } from '~/types/common';
import type { Dokter } from '~/types/pendaftaran';
import type { KategoriOperasi, Spesialis, SubSpesialis } from '~/types/antrean';
import { Icon } from '@iconify/vue';
const props = withDefaults(defineProps<Props>(), {
readonly: false
@@ -284,23 +285,65 @@ const selectedDokterIds = computed({
const removeDokter = (dokterId: string) => {
dokterPelaksanaItems.value = dokterPelaksanaItems.value.filter(d => d.id !== dokterId);
};
// Refs for focus functionality
const spesialisInput = ref();
const subSpesialisInput = ref();
const tanggalDaftarInput = ref();
const kategoriOperasiInput = ref();
// Expose methods to focus on fields
const focusSpesialis = () => {
if (spesialisInput.value) {
spesialisInput.value.focus();
}
};
const focusSubSpesialis = () => {
if (subSpesialisInput.value) {
subSpesialisInput.value.focus();
}
};
const focusTanggalDaftar = () => {
if (tanggalDaftarInput.value) {
tanggalDaftarInput.value.focus();
}
};
const focusKategoriOperasi = () => {
if (kategoriOperasiInput.value) {
kategoriOperasiInput.value.focus();
}
};
defineExpose({
focusSpesialis,
focusSubSpesialis,
focusTanggalDaftar,
focusKategoriOperasi
});
</script>
<template>
<v-card elevation="10">
<v-divider></v-divider>
<v-card-item>
<h5 class="text-h5 font-weight-bold">Data Rencana Operasi</h5>
</v-card-item>
<v-card-text class="pa-5">
<div class="d-flex align-center">
<!-- <Icon icon="solar:calendar-mark-bold-duotone" height="24" class="mr-2" /> -->
<span class="text-h5">Data Rencana Operasi</span>
</div>
</v-card-text>
<v-divider> </v-divider>
<v-card-text>
<v-row>
<!-- Spesialis -->
<v-col cols="12" md="6">
<v-label class="mb-2 font-weight-medium">Spesialis <span class="text-error">*</span></v-label>
<v-autocomplete
ref="spesialisInput"
v-model="rencanaOperasiData.spesialis"
:items="spesialisOptions"
placeholder="Select an item..."
placeholder="Pilih Spesialis..."
variant="outlined"
density="comfortable"
:rules="[rules.required]"
@@ -316,9 +359,10 @@ const removeDokter = (dokterId: string) => {
<v-col cols="12" md="6">
<v-label class="mb-2 font-weight-medium">Sub Spesialis <span class="text-error">*</span></v-label>
<v-autocomplete
ref="subSpesialisInput"
v-model="rencanaOperasiData.subSpesialis"
:items="subSpesialisOptions"
placeholder="Select an item..."
placeholder="Pilih Sub Spesialis..."
variant="outlined"
density="comfortable"
:rules="[rules.required]"
@@ -420,7 +464,7 @@ const removeDokter = (dokterId: string) => {
<template #append>
<v-btn
icon
size="x-small"
size="small"
variant="text"
color="error"
@click="removeDokter(dokter.id)"
@@ -464,6 +508,7 @@ const removeDokter = (dokterId: string) => {
<v-col cols="12" md="6">
<v-label class="mb-2 font-weight-medium">Tanggal Daftar <span class="text-error">*</span></v-label>
<v-text-field
ref="tanggalDaftarInput"
v-model="rencanaOperasiData.tanggalDaftar"
type="datetime-local"
variant="outlined"
@@ -479,9 +524,10 @@ const removeDokter = (dokterId: string) => {
<v-col cols="12" md="6">
<v-label class="mb-2 font-weight-medium">Kategori Operasi <span class="text-error">*</span></v-label>
<v-autocomplete
ref="kategoriOperasiInput"
v-model="rencanaOperasiData.kategoriOperasi"
:items="kategoriOperasiOptions"
placeholder="Select an item..."
placeholder="Pilih Kategori Operasi..."
variant="outlined"
density="comfortable"
:rules="[rules.required]"