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

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>