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

571 lines
23 KiB
Vue

<script setup lang="ts">
import { onMounted } from 'vue';
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
});
const rencanaOperasiData = defineModel<{
spesialis: number | null | undefined;
subSpesialis: number | null | undefined;
tanggalDaftar: string;
kategoriOperasi: number | null | undefined;
rencanaOperasi: string;
keterangan: string;
}>('rencanaOperasiData', { required: true });
const dokterPelaksanaItems = defineModel<Dokter[]>('dokterPelaksanaItems', { required: true });
const rules = {
required: (value: any) => !!value || 'Field ini wajib diisi'
};
// Spesialis, Sub-spesialis, dan Kategori Operasi data
const spesialisList = ref<Spesialis[]>([]);
const subSpesialisList = ref<SubSpesialis[]>([]);
const kategoriOperasiList = ref<KategoriOperasi[]>([]);
const isLoadingSpesialis = ref(false);
const isLoadingSubSpesialis = ref(false);
const isLoadingKategori = ref(false);
const isInitializing = ref(false); // Flag to prevent resetting during initialization
// Autocomplete options
const spesialisOptions = computed(() => {
return spesialisList.value.map(s => ({
title: s.spesialis,
value: s.id
}));
});
const subSpesialisOptions = computed(() => {
return subSpesialisList.value.map(ss => ({
title: ss.sub_spesialis,
value: ss.id
}));
});
const kategoriOperasiOptions = computed(() => {
return kategoriOperasiList.value.map((k: KategoriOperasi) => ({
title: k.kategori,
value: k.id
}));
});
// Fetch sub-spesialis from API
const fetchSubSpesialis = async (idSpesialis: number) => {
try {
isLoadingSubSpesialis.value = true;
const response = await api.get(`/reference/sub-spesialis?id_spesialis=${idSpesialis}`);
if (response.data.success && response.data.data) {
subSpesialisList.value = response.data.data;
}
} catch (error) {
console.error('Error fetching sub-spesialis:', error);
subSpesialisList.value = [];
} finally {
isLoadingSubSpesialis.value = false;
}
};
// Watch spesialis change to fetch sub-spesialis and reset sub-spesialis selection
watch(() => rencanaOperasiData.value.spesialis, (newSpesialis, oldSpesialis) => {
// Don't reset subSpesialis during initialization
if (isInitializing.value) {
return;
}
// Only reset if spesialis actually changed
if (newSpesialis !== oldSpesialis) {
rencanaOperasiData.value.subSpesialis = null;
subSpesialisList.value = [];
if (newSpesialis) {
fetchSubSpesialis(newSpesialis);
}
}
});
// Fetch spesialis from API
const fetchSpesialis = async () => {
try {
isLoadingSpesialis.value = true;
const response = await api.get('/reference/spesialis');
if (response.data.success && response.data.data) {
spesialisList.value = response.data.data;
}
} catch (error) {
console.error('Error fetching spesialis:', error);
// You can add error notification here if needed
} finally {
isLoadingSpesialis.value = false;
}
};
// Fetch kategori operasi from API
const fetchKategoriOperasi = async () => {
try {
isLoadingKategori.value = true;
const response = await api.get('/reference/kategori');
if (response.data.success && response.data.data) {
kategoriOperasiList.value = response.data.data;
}
} catch (error) {
console.error('Error fetching kategori operasi:', error);
// You can add error notification here if needed
} finally {
isLoadingKategori.value = false;
}
};
// Load data on mount
onMounted(async () => {
isInitializing.value = true;
await fetchSpesialis();
await fetchKategoriOperasi();
// If both spesialis and subSpesialis are set, just fetch subSpesialis list
if (rencanaOperasiData.value.spesialis && rencanaOperasiData.value.subSpesialis) {
await fetchSubSpesialis(rencanaOperasiData.value.spesialis);
}
// If only subSpesialis is set, fetch the spesialis detail and load subSpesialis list
else if (rencanaOperasiData.value.subSpesialis) {
try {
const response = await api.get(`/reference/sub-spesialis/${rencanaOperasiData.value.subSpesialis}`);
if (response.data.success && response.data.data) {
const subSpesialisDetail = response.data.data;
// Set spesialis if found
if (subSpesialisDetail.id_spesialis) {
rencanaOperasiData.value.spesialis = subSpesialisDetail.id_spesialis;
// Fetch sub-spesialis list for the spesialis
await fetchSubSpesialis(subSpesialisDetail.id_spesialis);
}
}
} catch (error) {
console.error('Error fetching sub-spesialis detail:', error);
}
}
// If only spesialis is set, fetch subSpesialis list
else if (rencanaOperasiData.value.spesialis) {
await fetchSubSpesialis(rencanaOperasiData.value.spesialis);
}
isInitializing.value = false;
});
// Watch for late data loading (when data is loaded after component mount)
watch(() => rencanaOperasiData.value.spesialis, async (newSpesialis) => {
// If we're not initializing and spesialis is set and we have no subSpesialis options yet
if (!isInitializing.value && newSpesialis && subSpesialisList.value.length === 0) {
await fetchSubSpesialis(newSpesialis);
}
}, { immediate: false });
// Watch for subSpesialis being set when spesialis list is empty (late data load scenario)
watch(() => rencanaOperasiData.value.subSpesialis, async (newSubSpesialis) => {
// If subSpesialis is set but we don't have the spesialis yet
if (!isInitializing.value && newSubSpesialis && !rencanaOperasiData.value.spesialis) {
try {
const response = await api.get(`/reference/sub-spesialis/${newSubSpesialis}`);
if (response.data.success && response.data.data) {
const subSpesialisDetail = response.data.data;
if (subSpesialisDetail.id_spesialis) {
isInitializing.value = true; // Prevent reset during this operation
rencanaOperasiData.value.spesialis = subSpesialisDetail.id_spesialis;
await fetchSubSpesialis(subSpesialisDetail.id_spesialis);
isInitializing.value = false;
}
}
} catch (error) {
console.error('Error fetching sub-spesialis detail:', error);
}
}
}, { immediate: false });
// Autocomplete state for dokter
const searchDokter = ref('');
const dokterList = ref<Dokter[]>([]);
const isLoadingDokter = ref(false);
const dokterMenuOpen = ref(false);
// Initialize dokterPelaksanaItems as array if not already
if (!Array.isArray(dokterPelaksanaItems.value)) {
dokterPelaksanaItems.value = [];
}
// Fetch dokter from API for autocomplete
const fetchDokter = async (search: string = '') => {
if (!dokterMenuOpen.value && !search) return; // Only fetch when menu is open or searching
try {
isLoadingDokter.value = true;
const params = new URLSearchParams({
search: search,
limit: '50', // Load more items for better selection
offset: '0'
});
const response = await api.get(`/reference/dokter?${params}`);
if (response.data.success && response.data.data) {
dokterList.value = response.data.data;
}
} catch (error) {
console.error('Error fetching dokter:', error);
dokterList.value = [];
} finally {
isLoadingDokter.value = false;
}
};
// Watch search with debounce
let dokterSearchTimeout: NodeJS.Timeout | null = null;
watch(searchDokter, (newSearch) => {
if (dokterSearchTimeout) {
clearTimeout(dokterSearchTimeout);
}
dokterSearchTimeout = setTimeout(() => {
fetchDokter(newSearch);
}, 300);
});
// Load initial dokter list when dropdown opens
watch(dokterMenuOpen, (isOpen) => {
if (isOpen && dokterList.value.length === 0) {
fetchDokter('');
}
});
// Computed for autocomplete items (merge selected dokters dengan fetched list)
const dokterOptions = computed(() => {
// Start with already selected doctors
const selectedDokters = dokterPelaksanaItems.value;
// Merge with fetched list, avoid duplicates
const allDokters = [...selectedDokters];
dokterList.value.forEach(d => {
if (!allDokters.find(sd => sd.id === d.id)) {
allDokters.push(d);
}
});
return allDokters.map(d => ({
title: d.nama_lengkap,
value: d.id,
subtitle: `${d.nip} - ${d.nama_ksm}`,
raw: d
}));
});
// Selected dokter IDs for v-autocomplete
const selectedDokterIds = computed({
get: () => dokterPelaksanaItems.value.map(d => d.id),
set: (ids: string[]) => {
// Get all available doctors (from options)
const availableDokters = dokterOptions.value.map(opt => opt.raw);
// Update dokterPelaksanaItems based on selected IDs
dokterPelaksanaItems.value = availableDokters.filter(d => ids.includes(d.id));
}
});
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-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="Pilih Spesialis..."
variant="outlined"
density="comfortable"
:rules="[rules.required]"
hide-details="auto"
:readonly="readonly"
:disabled="readonly || isLoadingSpesialis"
:loading="isLoadingSpesialis"
:bg-color="readonly ? 'grey-lighten-3' : undefined"
></v-autocomplete>
</v-col>
<!-- Sub Spesialis -->
<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="Pilih Sub Spesialis..."
variant="outlined"
density="comfortable"
:rules="[rules.required]"
hide-details="auto"
:disabled="readonly || !rencanaOperasiData.spesialis || isLoadingSubSpesialis"
:loading="isLoadingSubSpesialis"
:readonly="readonly"
:bg-color="readonly ? 'grey-lighten-3' : undefined"
></v-autocomplete>
</v-col>
<!-- Dokter Pelaksana -->
<v-col cols="12">
<v-label class="mb-2 font-weight-medium">Dokter Pelaksana</v-label>
<v-autocomplete
v-model="selectedDokterIds"
v-model:search="searchDokter"
v-model:menu="dokterMenuOpen"
:items="dokterOptions"
item-title="title"
item-value="value"
:loading="isLoadingDokter"
:readonly="readonly"
:disabled="readonly"
placeholder="Cari dan pilih dokter..."
variant="outlined"
density="comfortable"
multiple
chips
closable-chips
hide-details="auto"
:bg-color="readonly ? 'grey-lighten-3' : undefined"
no-data-text="Tidak ada data dokter"
>
<template #chip="{ props: chipProps, item }">
<v-chip
v-bind="chipProps"
closable
@click:close="removeDokter(item.value)"
color="primary"
variant="flat"
>
<span class="font-weight-medium">{{ item.title }}</span>
</v-chip>
</template>
<template #item="{ props: itemProps, item }">
<v-list-item
v-bind="itemProps"
>
<template #prepend>
<v-icon
:color="selectedDokterIds.includes(item.value) ? 'primary' : 'grey'"
>
{{ selectedDokterIds.includes(item.value) ? 'mdi-checkbox-marked' : 'mdi-checkbox-blank-outline' }}
</v-icon>
</template>
</v-list-item>
</template>
<template #prepend-item>
<v-list-item>
<v-list-item-title class="text-caption text-medium-emphasis">
<v-icon size="small" class="mr-2">mdi-information</v-icon>
Ketik untuk mencari dokter, pilih untuk menambahkan
</v-list-item-title>
</v-list-item>
<v-divider></v-divider>
</template>
</v-autocomplete>
<!-- Display selected doctors in a nice list -->
<v-card v-if="dokterPelaksanaItems.length > 0 && !readonly" variant="outlined" class="mt-3">
<v-card-text class="pa-3">
<div class="text-caption text-medium-emphasis mb-2">
<v-icon size="small" class="mr-1">mdi-account-multiple</v-icon>
{{ dokterPelaksanaItems.length }} Dokter Terpilih
</div>
<v-list density="compact" class="pa-0">
<v-list-item
v-for="(dokter, index) in dokterPelaksanaItems"
:key="dokter.id"
class="px-0"
:class="{ 'mb-2': index < dokterPelaksanaItems.length - 1 }"
>
<template #prepend>
<v-avatar color="primary" size="32" class="mr-3">
<v-icon size="small">mdi-doctor</v-icon>
</v-avatar>
</template>
<v-list-item-title class="font-weight-medium">
{{ dokter.nama_lengkap }}
</v-list-item-title>
<v-list-item-subtitle class="text-caption">
{{ dokter.nip }} {{ dokter.nama_ksm }}
</v-list-item-subtitle>
<template #append>
<v-btn
icon
size="small"
variant="text"
color="error"
@click="removeDokter(dokter.id)"
>
<v-icon size="small">mdi-close-circle</v-icon>
</v-btn>
</template>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
<!-- Read-only display -->
<v-card v-else-if="dokterPelaksanaItems.length > 0 && readonly" variant="outlined" class="mt-3">
<v-card-text class="pa-3">
<v-list density="compact" class="pa-0">
<v-list-item
v-for="dokter in dokterPelaksanaItems"
:key="dokter.id"
class="px-0 mb-2"
>
<template #prepend>
<v-avatar color="primary" size="32" class="mr-3">
<v-icon size="small">mdi-doctor</v-icon>
</v-avatar>
</template>
<v-list-item-title class="font-weight-medium">
{{ dokter.nama_lengkap }}
</v-list-item-title>
<v-list-item-subtitle class="text-caption">
{{ dokter.nip }} {{ dokter.nama_ksm }}
</v-list-item-subtitle>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
</v-col>
<!-- Tanggal Daftar -->
<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"
density="comfortable"
:rules="[rules.required]"
hide-details="auto"
:readonly="readonly"
:bg-color="readonly ? 'grey-lighten-3' : undefined"
></v-text-field>
</v-col>
<!-- Kategori Operasi -->
<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="Pilih Kategori Operasi..."
variant="outlined"
density="comfortable"
:rules="[rules.required]"
hide-details="auto"
:readonly="readonly"
:disabled="readonly || isLoadingKategori"
:loading="isLoadingKategori"
:bg-color="readonly ? 'grey-lighten-3' : undefined"
></v-autocomplete>
</v-col>
<!-- Rencana Operasi -->
<v-col cols="12">
<v-label class="mb-2 font-weight-medium">Rencana Operasi</v-label>
<v-textarea
v-model="rencanaOperasiData.rencanaOperasi"
variant="outlined"
rows="5"
hide-details="auto"
:readonly="readonly"
:bg-color="readonly ? 'grey-lighten-3' : undefined"
></v-textarea>
</v-col>
<!-- Keterangan -->
<v-col cols="12">
<v-label class="mb-2 font-weight-medium">Keterangan</v-label>
<v-textarea
v-model="rencanaOperasiData.keterangan"
variant="outlined"
rows="3"
hide-details="auto"
:readonly="readonly"
:bg-color="readonly ? 'grey-lighten-3' : undefined"
></v-textarea>
</v-col>
</v-row>
</v-card-text>
</v-card>
</template>