Files
2026-02-03 13:53:54 +07:00

496 lines
18 KiB
Vue

<script setup lang="ts">
import { onMounted } from 'vue';
import api from '~/services/api';
import TableAntrian from './TableAntrian.vue';
import type { Props } from '~/types/common';
import type { Dokter } from '~/types/pendaftaran';
import type { KategoriOperasi, Spesialis, SubSpesialis } from '~/types/antrean';
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);
// 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(`/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) => {
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('/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('/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(() => {
fetchSpesialis();
fetchKategoriOperasi();
});
// Modal state for dokter
const showDokterModal = ref(false);
const searchDokter = ref('');
const selectedDokters = ref<string[]>([]);
const dokterList = ref<Dokter[]>([]);
const isLoadingDokter = ref(false);
const dokterLimit = ref(10);
const dokterOffset = ref(0);
const dokterTotalCount = ref(0);
const dokterCurrentPage = ref(1);
const dokterItemsPerPage = ref(10);
// Fetch dokter from API
const fetchDokter = async () => {
try {
isLoadingDokter.value = true;
const offset = (dokterCurrentPage.value - 1) * dokterItemsPerPage.value;
const params = new URLSearchParams({
search: searchDokter.value,
limit: dokterItemsPerPage.value.toString(),
offset: offset.toString()
});
const response = await api.get(`/dokter?${params}`);
if (response.data.success && response.data.data) {
dokterList.value = response.data.data;
dokterTotalCount.value = response.data.Paginate.Total;
}
} 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, () => {
if (dokterSearchTimeout) {
clearTimeout(dokterSearchTimeout);
}
dokterSearchTimeout = setTimeout(() => {
dokterCurrentPage.value = 1;
fetchDokter();
}, 500);
});
// Watch for page changes
watch(dokterCurrentPage, () => {
fetchDokter();
});
watch(dokterItemsPerPage, () => {
dokterCurrentPage.value = 1;
fetchDokter();
});
// Initialize dokterPelaksanaItems as array if not already
if (!Array.isArray(dokterPelaksanaItems.value)) {
dokterPelaksanaItems.value = [];
}
const openDokterModal = () => {
// Pre-select already added doctors
selectedDokters.value = dokterPelaksanaItems.value.map(d => d.id);
searchDokter.value = '';
dokterCurrentPage.value = 1;
showDokterModal.value = true;
fetchDokter();
};
const saveDokterSelection = () => {
// Get selected doctor objects
const selected = dokterList.value.filter(d => selectedDokters.value.includes(d.id));
dokterPelaksanaItems.value = selected;
showDokterModal.value = false;
};
const deleteDokter = (index: number) => {
dokterPelaksanaItems.value.splice(index, 1);
};
// Toggle all selection
const toggleAllDokters = () => {
if (selectedDokters.value.length === dokterList.value.length) {
selectedDokters.value = [];
} else {
selectedDokters.value = dokterList.value.map(d => d.id);
}
};
const isAllSelected = computed(() => {
return dokterList.value.length > 0 &&
selectedDokters.value.length === dokterList.value.length;
});
const toggleDokterSelection = (dokterId: string) => {
const index = selectedDokters.value.indexOf(dokterId);
if (index > -1) {
selectedDokters.value.splice(index, 1);
} else {
selectedDokters.value.push(dokterId);
}
};
// Headers for dokter table
const dokterHeaders = [
{ title: 'Select', key: 'select', sortable: false, width: '80px' },
{ title: 'NIP', key: 'nip', sortable: false },
{ title: 'Nama Lengkap', key: 'nama_lengkap', sortable: false },
{ title: 'KSM', key: 'nama_ksm', sortable: false }
];
const handleDokterPageUpdate = (page: unknown) => {
dokterCurrentPage.value = page as number;
};
const handleDokterItemsPerPageUpdate = (items: unknown) => {
dokterItemsPerPage.value = items as number;
};
</script>
<template>
<v-card elevation="10">
<v-card-item>
<h5 class="text-h5 font-weight-bold">Data Rencana Operasi</h5>
</v-card-item>
<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
v-model="rencanaOperasiData.spesialis"
:items="spesialisOptions"
placeholder="Select an item..."
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
v-model="rencanaOperasiData.subSpesialis"
:items="subSpesialisOptions"
placeholder="Select an item..."
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-card variant="outlined" class="mb-3">
<v-alert
v-if="dokterPelaksanaItems.length === 0"
type="info"
variant="tonal"
density="compact"
>
<div class="d-flex align-center">
<span>No items</span>
</div>
</v-alert>
<v-list v-else density="compact">
<v-list-item
v-for="(item, index) in dokterPelaksanaItems"
:key="index"
@click="!readonly && openDokterModal()"
class="px-4 mb-2"
:style="readonly ? 'cursor: default;' : 'cursor: pointer;'"
>
<template #default>
<div class="d-flex align-center justify-space-between w-100">
<div>
<div class="font-weight-medium">{{ item.nama_lengkap }}</div>
<div class="text-caption text-medium-emphasis">
{{ item.nip }} - {{ item.nama_ksm }}
</div>
</div>
<v-btn
v-if="!readonly"
icon
size="small"
variant="text"
color="error"
@click="deleteDokter(index)"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</div>
</template>
</v-list-item>
</v-list>
</v-card>
<v-btn v-if="!readonly" color="primary" prepend-icon="mdi-plus" @click="openDokterModal">
Tambah Dokter Pelaksana
</v-btn>
</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
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
v-model="rencanaOperasiData.kategoriOperasi"
:items="kategoriOperasiOptions"
placeholder="Select an item..."
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>
<!-- Modal for selecting dokter -->
<v-dialog v-model="showDokterModal" max-width="900px">
<v-card>
<v-card-title class="d-flex align-center justify-space-between pa-4">
<div class="d-flex align-center">
<v-icon class="mr-3">mdi-database</v-icon>
<span class="text-h6">Select Item</span>
</div>
<v-btn
icon
variant="text"
@click="showDokterModal = false"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-divider></v-divider>
<v-card-text class="pa-4">
<!-- Search Bar -->
<v-text-field
v-model="searchDokter"
placeholder="Search..."
variant="outlined"
density="comfortable"
hide-details
prepend-inner-icon="mdi-magnify"
:loading="isLoadingDokter"
class="mb-4"
></v-text-field>
<!-- Data Table -->
<TableAntrian
:headers="dokterHeaders"
:items="dokterList"
:server-side="true"
:total-items="dokterTotalCount"
:current-page="dokterCurrentPage"
:items-per-page="dokterItemsPerPage"
:loading="isLoadingDokter"
min-width="700px"
@update:page="handleDokterPageUpdate"
@update:itemsPerPage="handleDokterItemsPerPageUpdate"
>
<template #item.select="{ item }">
<v-checkbox
v-model="selectedDokters"
:value="item.id"
hide-details
density="compact"
@click.stop
></v-checkbox>
</template>
<template #item.nip="{ item }">
<div @click="toggleDokterSelection(item.id)" style="cursor: pointer;">
{{ item.nip }}
</div>
</template>
<template #item.nama_lengkap="{ item }">
<div @click="toggleDokterSelection(item.id)" style="cursor: pointer;">
{{ item.nama_lengkap }}
</div>
</template>
<template #item.nama_ksm="{ item }">
<div @click="toggleDokterSelection(item.id)" style="cursor: pointer;">
{{ item.nama_ksm }}
</div>
</template>
</TableAntrian>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="pa-4 justify-end">
<v-btn
color="primary"
variant="flat"
@click="saveDokterSelection"
:disabled="selectedDokters.length === 0"
>
<v-icon start>mdi-check</v-icon>
Simpan ({{ selectedDokters.length }})
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>