476 lines
18 KiB
Vue
476 lines
18 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';
|
|
|
|
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);
|
|
|
|
// Fetch dokter from API
|
|
const fetchDokter = async () => {
|
|
try {
|
|
isLoadingDokter.value = true;
|
|
const params = new URLSearchParams({
|
|
search: searchDokter.value,
|
|
limit: dokterLimit.value.toString(),
|
|
offset: dokterOffset.value.toString()
|
|
});
|
|
|
|
const response = await api.get(`/dokter?${params}`);
|
|
|
|
if (response.data.success && response.data.data) {
|
|
dokterList.value = response.data.data;
|
|
// TODO: Update when backend implements pagination
|
|
// dokterTotalCount.value = response.data.total || response.data.data.length;
|
|
dokterTotalCount.value = response.data.data.length;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching dokter:', error);
|
|
dokterList.value = [];
|
|
} finally {
|
|
isLoadingDokter.value = false;
|
|
}
|
|
};
|
|
|
|
// Watch search with debounce
|
|
watch(searchDokter, () => {
|
|
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 = '';
|
|
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);
|
|
}
|
|
};
|
|
</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>
|
|
|
|
<!-- Info Badge -->
|
|
<div class="text-caption text-medium-emphasis mb-3">
|
|
1-{{ dokterList.length }} of {{ dokterTotalCount }} Items
|
|
</div>
|
|
|
|
<!-- Data Table -->
|
|
<v-table density="compact" class="border">
|
|
<thead>
|
|
<tr>
|
|
<th class="text-left">
|
|
<v-checkbox
|
|
:model-value="isAllSelected"
|
|
@click="toggleAllDokters"
|
|
hide-details
|
|
density="compact"
|
|
:disabled="isLoadingDokter"
|
|
></v-checkbox>
|
|
</th>
|
|
<th class="text-left">NIP</th>
|
|
<th class="text-left">Nama Lengkap</th>
|
|
<th class="text-left">KSM</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr
|
|
v-for="dokter in dokterList"
|
|
:key="dokter.id"
|
|
@click="toggleDokterSelection(dokter.id)"
|
|
class="cursor-pointer"
|
|
style="cursor: pointer;"
|
|
>
|
|
<td @click.stop>
|
|
<v-checkbox
|
|
v-model="selectedDokters"
|
|
:value="dokter.id"
|
|
hide-details
|
|
density="compact"
|
|
></v-checkbox>
|
|
</td>
|
|
<td>{{ dokter.nip }}</td>
|
|
<td>{{ dokter.nama_lengkap }}</td>
|
|
<td>{{ dokter.nama_ksm }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</v-table>
|
|
|
|
<!-- Loading State -->
|
|
<div v-if="isLoadingDokter" class="text-center py-8">
|
|
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
|
<p class="text-medium-emphasis mt-2">Loading...</p>
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div v-else-if="dokterList.length === 0" class="text-center py-8">
|
|
<v-icon size="large" class="mb-2 text-medium-emphasis">mdi-information-outline</v-icon>
|
|
<p class="text-medium-emphasis">No results found</p>
|
|
</div>
|
|
</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>
|