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

630 lines
26 KiB
Vue

<script setup lang="ts">
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
});
const diagnosisItems = defineModel<DiagnosisItem[]>('diagnosisItems', { required: true });
const tindakanItems = defineModel<TindakanItem[]>('tindakanItems', { required: true });
// Form state for diagnosis
const diagnosisForm = ref<DiagnosisItem>({
kodeDiagnosa: '',
diagnosa: '',
jenisDiagnosa: 'utama'
});
const editingDiagnosisIndex = ref<number | null>(null);
const diagnosisFormExpanded = ref(true); // Always show form
const diagnosisList = ref<DiagnosisApiData[]>([]);
const loadingDiagnosis = ref(false);
const diagnosisSearchTimeout = ref<NodeJS.Timeout | null>(null);
const diagnosisSearchPerformed = ref(false);
// Search diagnosis from API
const searchDiagnosis = async (search: string) => {
if (!search || search.length < 3) {
diagnosisList.value = [];
diagnosisSearchPerformed.value = false;
return;
}
loadingDiagnosis.value = true;
diagnosisSearchPerformed.value = false;
try {
const response = await api.get('/reference/diagnosa', {
params: {
limit: 100,
search: search
}
});
if (response.data?.data) {
diagnosisList.value = response.data.data;
diagnosisSearchPerformed.value = true;
}
} catch (error) {
console.error('Error fetching diagnosis:', error);
diagnosisList.value = [];
diagnosisSearchPerformed.value = true;
} finally {
loadingDiagnosis.value = false;
}
};
// Handle diagnosis search input with debounce
const handleDiagnosisSearchInput = (value: string) => {
if (diagnosisSearchTimeout.value) {
clearTimeout(diagnosisSearchTimeout.value);
}
diagnosisSearchTimeout.value = setTimeout(() => {
searchDiagnosis(value);
}, 500);
};
// Autocomplete options for diagnosis
const diagnosisOptions = computed(() => {
return diagnosisList.value.map(d => ({
title: d.select,
value: d.kodeicd,
diagnosa: d.keterangan
}));
});
// Form state for tindakan
const tindakanForm = ref<TindakanItem>({
kodeTindakan: '',
tindakan: '',
tindakanTambahan: ''
});
const editingTindakanIndex = ref<number | null>(null);
const tindakanFormExpanded = ref(true); // Always show form
const tindakanList = ref<TindakanApiData[]>([]);
const loadingTindakan = ref(false);
const tindakanSearchTimeout = ref<NodeJS.Timeout | null>(null);
const tindakanSearchPerformed = ref(false);
// Search tindakan from API
const searchTindakan = async (search: string) => {
if (!search || search.length < 3) {
tindakanList.value = [];
tindakanSearchPerformed.value = false;
return;
}
loadingTindakan.value = true;
tindakanSearchPerformed.value = false;
try {
const response = await api.get('/reference/tindakan', {
params: {
limit: 100,
search: search
}
});
if (response.data?.data) {
tindakanList.value = response.data.data;
tindakanSearchPerformed.value = true;
}
} catch (error) {
console.error('Error fetching tindakan:', error);
tindakanList.value = [];
tindakanSearchPerformed.value = true;
} finally {
loadingTindakan.value = false;
}
};
// Handle tindakan search input with debounce
const handleTindakanSearchInput = (value: string) => {
if (tindakanSearchTimeout.value) {
clearTimeout(tindakanSearchTimeout.value);
}
tindakanSearchTimeout.value = setTimeout(() => {
searchTindakan(value);
}, 500);
};
// Autocomplete options for tindakan
const tindakanOptions = computed(() => {
return tindakanList.value.map(t => ({
title: t.select,
value: t.kode,
tindakan: t.keterangan
}));
});
// Initialize arrays if not already
if (!Array.isArray(diagnosisItems.value)) {
diagnosisItems.value = [];
}
if (!Array.isArray(tindakanItems.value)) {
tindakanItems.value = [];
}
const resetDiagnosisForm = () => {
diagnosisForm.value = {
kodeDiagnosa: '',
diagnosa: '',
jenisDiagnosa: 'utama'
};
editingDiagnosisIndex.value = null;
};
const openDiagnosisModal = () => {
resetDiagnosisForm();
};
const editDiagnosis = (index: number) => {
diagnosisForm.value = { ...diagnosisItems.value[index] };
editingDiagnosisIndex.value = index;
};
const saveDiagnosis = () => {
if (!diagnosisForm.value.kodeDiagnosa.trim() || !diagnosisForm.value.diagnosa.trim()) return;
if (editingDiagnosisIndex.value !== null) {
diagnosisItems.value[editingDiagnosisIndex.value] = { ...diagnosisForm.value };
} else {
diagnosisItems.value.push({ ...diagnosisForm.value });
}
resetDiagnosisForm();
};
const cancelEditDiagnosis = () => {
resetDiagnosisForm();
};
const deleteDiagnosis = (index: number) => {
diagnosisItems.value.splice(index, 1);
};
// Auto-fill diagnosis when code is selected
const handleKodeDiagnosaChange = (kode: string) => {
const found = diagnosisList.value.find(d => d.kodeicd === kode);
if (found) {
diagnosisForm.value.diagnosa = found.keterangan;
}
};
const resetTindakanForm = () => {
tindakanForm.value = {
kodeTindakan: '',
tindakan: '',
tindakanTambahan: ''
};
editingTindakanIndex.value = null;
};
const openTindakanModal = () => {
resetTindakanForm();
};
const editTindakan = (index: number) => {
tindakanForm.value = { ...tindakanItems.value[index] };
editingTindakanIndex.value = index;
};
const saveTindakan = () => {
if (!tindakanForm.value.kodeTindakan.trim() || !tindakanForm.value.tindakan.trim()) return;
if (editingTindakanIndex.value !== null) {
tindakanItems.value[editingTindakanIndex.value] = { ...tindakanForm.value };
} else {
tindakanItems.value.push({ ...tindakanForm.value });
}
resetTindakanForm();
};
const cancelEditTindakan = () => {
resetTindakanForm();
};
const deleteTindakan = (index: number) => {
tindakanItems.value.splice(index, 1);
};
// Auto-fill tindakan when code is selected
const handleKodeTindakanChange = (kode: string) => {
const found = tindakanList.value.find(t => t.kode === kode);
if (found) {
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-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 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="6">
<v-label class="mb-2 font-weight-medium text-caption">
Jenis Diagnosa <span class="text-error">*</span>
</v-label>
<div class="d-flex ga-2">
<v-btn
:color="diagnosisForm.jenisDiagnosa === 'utama' ? 'primary' : 'default'"
:variant="diagnosisForm.jenisDiagnosa === 'utama' ? 'flat' : 'outlined'"
size="large"
class="flex-grow-1 text-none"
@click="diagnosisForm.jenisDiagnosa = 'utama'"
:prepend-icon="diagnosisForm.jenisDiagnosa === 'utama' ? 'mdi-radiobox-marked' : 'mdi-radiobox-blank'"
>
Diagnosa utama
</v-btn>
<v-btn
:color="diagnosisForm.jenisDiagnosa === 'tambahan' ? 'primary' : 'default'"
:variant="diagnosisForm.jenisDiagnosa === 'tambahan' ? 'flat' : 'outlined'"
size="large"
class="flex-grow-1 text-none"
@click="diagnosisForm.jenisDiagnosa = 'tambahan'"
:prepend-icon="diagnosisForm.jenisDiagnosa === 'tambahan' ? 'mdi-radiobox-marked' : 'mdi-radiobox-blank'"
>
Diagnosa tambahan
</v-btn>
</div>
</v-col>
<!-- Kode Diagnosa -->
<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..."
variant="outlined"
density="compact"
hide-details="auto"
:loading="loadingDiagnosis"
no-filter
clearable
@update:search="handleDiagnosisSearchInput"
@update:model-value="handleKodeDiagnosaChange"
>
<template #no-data>
<v-list-item>
<v-list-item-title class="text-caption">
{{ diagnosisSearchPerformed && diagnosisList.length === 0
? 'Data tidak ditemukan' : 'Ketik untuk mencari...' }}
</v-list-item-title>
</v-list-item>
</template>
</v-autocomplete>
</v-col>
<!-- Diagnosa -->
<v-col cols="12" md="12">
<v-label class="mb-2 font-weight-medium text-caption">
Diagnosa <span class="text-error">*</span>
</v-label>
<v-text-field
v-model="diagnosisForm.diagnosa"
variant="outlined"
density="compact"
readonly
bg-color="grey-lighten-3"
hide-details="auto"
></v-text-field>
</v-col>
<!-- Action Buttons -->
<v-col cols="12">
<div class="d-flex ga-2 justify-end">
<v-btn
v-if="editingDiagnosisIndex !== null"
color="default"
variant="outlined"
size="small"
@click="cancelEditDiagnosis"
>
<v-icon start size="small">mdi-close</v-icon>
Batal
</v-btn>
<v-btn
color="primary"
variant="flat"
size="small"
@click="saveDiagnosis"
:disabled="!diagnosisForm.kodeDiagnosa || !diagnosisForm.diagnosa"
>
<v-icon start size="small">
{{ editingDiagnosisIndex !== null ? 'mdi-check' : 'mdi-plus' }}
</v-icon>
{{ editingDiagnosisIndex !== null ? 'Update' : 'Tambah' }}
</v-btn>
</div>
</v-col>
</v-row>
</v-card>
<!-- List Diagnosa -->
<v-alert
v-if="diagnosisItems.length === 0"
type="info"
variant="tonal"
density="compact"
class="ma-0"
>
<div class="d-flex align-center">
<span>Tidak ada data diagnosa</span>
</div>
</v-alert>
<v-card v-else variant="outlined">
<v-card-text class="pa-1">
<v-list density="compact" class="pa-0">
<v-list-item
v-for="(item, index) in diagnosisItems"
:key="index"
class="px-4"
:class="{ 'bg-blue-lighten-5': editingDiagnosisIndex === index }"
>
<template #prepend>
<v-avatar
:color="item.jenisDiagnosa === 'utama' ? 'primary' : 'secondary'"
size="32"
class="mr-3"
>
<v-icon size="small">
{{ item.jenisDiagnosa === 'utama' ? 'mdi-stethoscope' : 'mdi-stethoscope' }}
</v-icon>
</v-avatar>
</template>
<v-list-item-title class="font-weight-medium">
{{ item.kodeDiagnosa }} - {{ item.diagnosa }}
</v-list-item-title>
<v-list-item-subtitle class="text-caption">
{{ item.jenisDiagnosa === 'utama' ? 'Diagnosa Utama' : 'Diagnosa Tambahan' }}
</v-list-item-subtitle>
<template v-if="!readonly" #append>
<div class="d-flex ga-1">
<v-btn
icon
size="small"
variant="text"
color="primary"
@click="editDiagnosis(index)"
>
<v-icon size="small">mdi-pencil</v-icon>
</v-btn>
<v-btn
icon
size="small"
variant="text"
color="error"
@click="deleteDiagnosis(index)"
>
<v-icon size="small">mdi-delete</v-icon>
</v-btn>
</div>
</template>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
</div>
<!-- Tindakan -->
<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">
<v-label class="mb-2 font-weight-medium text-caption">
Kode Tindakan <span class="text-error">*</span>
</v-label>
<v-autocomplete
ref="tindakanInput"
v-model="tindakanForm.kodeTindakan"
:items="tindakanOptions"
placeholder="Ketik min. 3 karakter..."
variant="outlined"
density="compact"
hide-details="auto"
:loading="loadingTindakan"
no-filter
clearable
@update:search="handleTindakanSearchInput"
@update:model-value="handleKodeTindakanChange"
>
<template #no-data>
<v-list-item>
<v-list-item-title class="text-caption">
{{ tindakanSearchPerformed && tindakanList.length === 0
? 'Data tidak ditemukan' : 'Ketik untuk mencari...' }}
</v-list-item-title>
</v-list-item>
</template>
</v-autocomplete>
</v-col>
<!-- Tindakan -->
<v-col cols="12" md="6">
<v-label class="mb-2 font-weight-medium text-caption">
Tindakan <span class="text-error">*</span>
</v-label>
<v-text-field
v-model="tindakanForm.tindakan"
variant="outlined"
density="compact"
readonly
bg-color="grey-lighten-3"
hide-details="auto"
></v-text-field>
</v-col>
<!-- Tindakan Tambahan -->
<v-col cols="12" md="12">
<v-label class="mb-2 font-weight-medium text-caption">
Tindakan Tambahan
</v-label>
<v-textarea
v-model="tindakanForm.tindakanTambahan"
placeholder="Prosedur tambahan..."
variant="outlined"
density="compact"
rows="2"
hide-details="auto"
></v-textarea>
</v-col>
<!-- Action Buttons -->
<v-col cols="12">
<div class="d-flex ga-2 justify-end">
<v-btn
v-if="editingTindakanIndex !== null"
color="default"
variant="outlined"
size="small"
@click="cancelEditTindakan"
>
<v-icon start size="small">mdi-close</v-icon>
Batal
</v-btn>
<v-btn
color="primary"
variant="flat"
size="small"
@click="saveTindakan"
:disabled="!tindakanForm.kodeTindakan || !tindakanForm.tindakan"
>
<v-icon start size="small">
{{ editingTindakanIndex !== null ? 'mdi-check' : 'mdi-plus' }}
</v-icon>
{{ editingTindakanIndex !== null ? 'Update' : 'Tambah' }}
</v-btn>
</div>
</v-col>
</v-row>
</v-card>
<!-- List Tindakan -->
<v-alert
v-if="tindakanItems.length === 0"
type="info"
variant="tonal"
density="compact"
class="ma-0"
>
<div class="d-flex align-center">
<span>Tidak ada data tindakan</span>
</div>
</v-alert>
<v-card v-else variant="outlined">
<v-card-text class="pa-1">
<v-list density="compact" class="pa-0">
<v-list-item
v-for="(item, index) in tindakanItems"
:key="index"
class="px-4 mb-1"
:class="{ 'bg-purple-lighten-5': editingTindakanIndex === index }"
>
<template #prepend>
<v-avatar color="primary" size="32" class="mr-3">
<v-icon size="small">mdi-medical-bag</v-icon>
</v-avatar>
</template>
<v-list-item-title class="font-weight-medium">
{{ item.kodeTindakan }} - {{ item.tindakan }}
</v-list-item-title>
<v-list-item-subtitle v-if="item.tindakanTambahan" class="text-caption">
{{ item.tindakanTambahan }}
</v-list-item-subtitle>
<template v-if="!readonly" #append>
<div class="d-flex ga-1">
<v-btn
icon
size="small"
variant="text"
color="primary"
@click="editTindakan(index)"
>
<v-icon size="small">mdi-pencil</v-icon>
</v-btn>
<v-btn
icon
size="small"
variant="text"
color="error"
@click="deleteTindakan(index)"
>
<v-icon size="small">mdi-delete</v-icon>
</v-btn>
</div>
</template>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
</div>
</v-card-text>
</v-card>
</template>