496 lines
18 KiB
Vue
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>
|