571 lines
23 KiB
Vue
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>
|