Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8745b22f07 |
No files matched your search
@@ -0,0 +1,114 @@
|
|||||||
|
<script setup>
|
||||||
|
/**
|
||||||
|
* DialogDetailEvent — Dialog untuk menampilkan detail jadwal dokter
|
||||||
|
* yang diklik di kalender. Mendukung tampilan Rutin dan Adhoc.
|
||||||
|
* Menggunakan v-model pattern untuk kontrol buka/tutup dialog dari parent.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
/** Kontrol buka/tutup dialog (v-model) */
|
||||||
|
modelValue: { type: Boolean, default: false },
|
||||||
|
/** Data event yang sedang ditampilkan */
|
||||||
|
event: { type: Object, default: null },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'delete']);
|
||||||
|
|
||||||
|
/** Computed v-model proxy untuk dialog visibility */
|
||||||
|
const dialogOpen = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val) => emit('update:modelValue', val),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit event delete ke parent untuk menghapus jadwal.
|
||||||
|
*/
|
||||||
|
const handleDelete = () => {
|
||||||
|
emit('delete', props.event?.id);
|
||||||
|
dialogOpen.value = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-dialog v-model="dialogOpen" max-width="420px">
|
||||||
|
<v-card rounded="lg" class="pa-0">
|
||||||
|
<v-card-title class="pa-5" :class="event?.tipe === 'Adhoc' ? 'bg-secondary' : 'bg-primary'"
|
||||||
|
style="color:white;">
|
||||||
|
<div class="d-flex justify-space-between align-center w-100">
|
||||||
|
<div class="d-flex align-center gap-2">
|
||||||
|
<v-icon>{{ event?.tipe === 'Adhoc' ? 'mdi-calendar-clock' : 'mdi-calendar-sync'
|
||||||
|
}}</v-icon>
|
||||||
|
<span class="ml-2 text-h6 font-weight-bold">Jadwal {{ event?.tipe }}</span>
|
||||||
|
</div>
|
||||||
|
<v-btn icon="mdi-close" variant="text" size="small" style="color:white;"
|
||||||
|
@click="dialogOpen = false" />
|
||||||
|
</div>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="pa-6" v-if="event">
|
||||||
|
<v-list density="compact" class="pa-0">
|
||||||
|
<v-list-item class="px-0">
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon color="primary" size="small" class="mr-3">mdi-hospital-building</v-icon>
|
||||||
|
</template>
|
||||||
|
<div class="text-caption text-grey-darken-1">Poli / Klinik</div>
|
||||||
|
<div class="text-body-2 font-weight-bold">{{ event.poli }}</div>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-divider class="my-2" />
|
||||||
|
|
||||||
|
<v-list-item class="px-0">
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon color="primary" size="small" class="mr-3">mdi-doctor</v-icon>
|
||||||
|
</template>
|
||||||
|
<div class="text-caption text-grey-darken-1">Dokter</div>
|
||||||
|
<div class="text-body-2 font-weight-bold">{{ event.dokter }}</div>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-divider class="my-2" />
|
||||||
|
|
||||||
|
<v-list-item class="px-0">
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon color="primary" size="small" class="mr-3">mdi-calendar</v-icon>
|
||||||
|
</template>
|
||||||
|
<div class="text-caption text-grey-darken-1">Tanggal</div>
|
||||||
|
<div class="text-body-2 font-weight-bold">{{ event.tanggal }}</div>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<!-- Rutin: tampilkan hari -->
|
||||||
|
<template v-if="event.tipe === 'Rutin' && event.hari">
|
||||||
|
<v-divider class="my-2" />
|
||||||
|
<v-list-item class="px-0">
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon color="primary" size="small" class="mr-3">mdi-calendar-week</v-icon>
|
||||||
|
</template>
|
||||||
|
<div class="text-caption text-grey-darken-1">Hari Praktek</div>
|
||||||
|
<div class="text-body-2 font-weight-bold">{{ event.hari }}</div>
|
||||||
|
</v-list-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Adhoc: tampilkan shift -->
|
||||||
|
<template v-if="event.tipe === 'Adhoc' && event.shift">
|
||||||
|
<v-divider class="my-2" />
|
||||||
|
<v-list-item class="px-0">
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon color="secondary" size="small" class="mr-3">mdi-clock-outline</v-icon>
|
||||||
|
</template>
|
||||||
|
<div class="text-caption text-grey-darken-1">Waktu</div>
|
||||||
|
<div class="text-body-2 font-weight-bold">{{ event.shift }}</div>
|
||||||
|
</v-list-item>
|
||||||
|
</template>
|
||||||
|
</v-list>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<v-card-actions class="pa-5 justify-space-between">
|
||||||
|
<v-btn color="error" variant="text" prepend-icon="mdi-trash-can-outline" @click="handleDelete">
|
||||||
|
Hapus
|
||||||
|
</v-btn>
|
||||||
|
<v-btn color="primary" variant="flat" @click="dialogOpen = false">Tutup</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,336 @@
|
|||||||
|
<script setup>
|
||||||
|
/**
|
||||||
|
* DialogTambahJadwal — Dialog form untuk menambahkan jadwal dokter baru.
|
||||||
|
* Mendukung dua tipe jadwal: Rutin (mingguan berulang) dan Adhoc (satu kali).
|
||||||
|
* Menggunakan v-model pattern untuk kontrol buka/tutup dialog dari parent.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const DAYS_OF_WEEK = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu'];
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
/** Kontrol buka/tutup dialog (v-model) */
|
||||||
|
modelValue: { type: Boolean, default: false },
|
||||||
|
/** Daftar klinik untuk referensi nama poli */
|
||||||
|
clinicList: { type: Array, default: () => [] },
|
||||||
|
/** ID poli yang sedang dipilih */
|
||||||
|
selectedPoli: { type: [String, Number, null], default: null },
|
||||||
|
/** Nama dokter yang sedang dipilih */
|
||||||
|
selectedDoctor: { type: [String, null], default: null },
|
||||||
|
/** Tanggal yang diklik di kalender (opsional, sebagai default adhoc) */
|
||||||
|
clickedDate: { type: [Date, null], default: null },
|
||||||
|
/** Mode tampilan saat ini ('list' atau 'calendar') */
|
||||||
|
viewMode: { type: String, default: 'list' },
|
||||||
|
/** Daftar jadwal saat ini (untuk preview kalender) */
|
||||||
|
jadwalList: { type: Array, default: () => [] },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'save']);
|
||||||
|
|
||||||
|
/** Computed v-model proxy untuk dialog visibility */
|
||||||
|
const dialogOpen = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val) => emit('update:modelValue', val),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** State form tambah jadwal */
|
||||||
|
const form = reactive({
|
||||||
|
poliId: null,
|
||||||
|
dokter: null,
|
||||||
|
tipe: 'Rutin',
|
||||||
|
hari: '',
|
||||||
|
shifts: [{ jamMulai: '', jamSelesai: '' }],
|
||||||
|
adhocTanggal: '',
|
||||||
|
adhocShifts: [{ jamMulai: '', jamSelesai: '' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Computed daftar dokter berdasarkan poli yang dipilih */
|
||||||
|
const availableDoctors = computed(() => {
|
||||||
|
if (!form.poliId) return [];
|
||||||
|
const clinic = props.clinicList.find(c => c.id === form.poliId);
|
||||||
|
return clinic?.doctors || [];
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Error validasi form */
|
||||||
|
const formErrors = ref({});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format tanggal ke string YYYY-MM-DD.
|
||||||
|
* @param {Date} date
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const toDateStr = (date) => {
|
||||||
|
const d = new Date(date);
|
||||||
|
const tzOffset = d.getTimezoneOffset() * 60000;
|
||||||
|
return new Date(d.getTime() - tzOffset).toISOString().slice(0, 10);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Reset form saat dialog dibuka */
|
||||||
|
watch(() => props.modelValue, (open) => {
|
||||||
|
if (open) {
|
||||||
|
if (props.viewMode === 'calendar') {
|
||||||
|
form.poliId = props.selectedPoli || null;
|
||||||
|
form.dokter = props.selectedDoctor || null;
|
||||||
|
} else {
|
||||||
|
form.poliId = null;
|
||||||
|
form.dokter = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.tipe = 'Rutin';
|
||||||
|
form.hari = '';
|
||||||
|
form.shifts = [{ jamMulai: '', jamSelesai: '' }];
|
||||||
|
formErrors.value = {};
|
||||||
|
|
||||||
|
if (props.clickedDate) {
|
||||||
|
const dateStr = toDateStr(props.clickedDate);
|
||||||
|
form.adhocTanggal = dateStr;
|
||||||
|
form.adhocShifts = [{ jamMulai: '08:00', jamSelesai: '12:00' }];
|
||||||
|
} else {
|
||||||
|
const dateStr = toDateStr(new Date());
|
||||||
|
form.adhocTanggal = dateStr;
|
||||||
|
form.adhocShifts = [{ jamMulai: '08:00', jamSelesai: '12:00' }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validasi form sebelum simpan.
|
||||||
|
* @returns {boolean} true jika valid
|
||||||
|
*/
|
||||||
|
const validateForm = () => {
|
||||||
|
const errors = {};
|
||||||
|
if (!form.poliId) errors.poli = 'Pilih poli terlebih dahulu';
|
||||||
|
if (!form.dokter) errors.dokter = 'Pilih dokter terlebih dahulu';
|
||||||
|
|
||||||
|
if (form.tipe === 'Rutin') {
|
||||||
|
if (!form.hari) errors.hari = 'Pilih hari kerja';
|
||||||
|
form.shifts.forEach((shift, idx) => {
|
||||||
|
if (!shift.jamMulai) errors[`shift_${idx}_jamMulai`] = 'Pilih jam mulai';
|
||||||
|
if (!shift.jamSelesai) errors[`shift_${idx}_jamSelesai`] = 'Pilih jam selesai';
|
||||||
|
if (shift.jamMulai && shift.jamSelesai && shift.jamMulai >= shift.jamSelesai) {
|
||||||
|
errors[`shift_${idx}_jamSelesai`] = 'Harus setelah jam mulai';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (!form.adhocTanggal) errors.adhocTanggal = 'Pilih tanggal';
|
||||||
|
form.adhocShifts.forEach((shift, idx) => {
|
||||||
|
if (!shift.jamMulai) errors[`adhocShift_${idx}_jamMulai`] = 'Pilih jam mulai';
|
||||||
|
if (!shift.jamSelesai) errors[`adhocShift_${idx}_jamSelesai`] = 'Pilih jam selesai';
|
||||||
|
if (shift.jamMulai && shift.jamSelesai && shift.jamMulai >= shift.jamSelesai) {
|
||||||
|
errors[`adhocShift_${idx}_jamSelesai`] = 'Harus setelah jam mulai';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
formErrors.value = errors;
|
||||||
|
return Object.keys(errors).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menangani submit form — validasi lalu emit data ke parent.
|
||||||
|
*/
|
||||||
|
const handleSave = () => {
|
||||||
|
if (!validateForm()) return;
|
||||||
|
|
||||||
|
const poliName = props.clinicList.find(c => c.id === form.poliId)?.title ?? '';
|
||||||
|
const emittedData = [];
|
||||||
|
|
||||||
|
if (form.tipe === 'Rutin') {
|
||||||
|
form.shifts.forEach((shift, index) => {
|
||||||
|
emittedData.push({
|
||||||
|
id: Date.now() + index,
|
||||||
|
poliId: form.poliId,
|
||||||
|
poliName,
|
||||||
|
dokter: form.dokter,
|
||||||
|
tipe: 'Rutin',
|
||||||
|
hari: [form.hari],
|
||||||
|
jamMulai: shift.jamMulai || null,
|
||||||
|
jamSelesai: shift.jamSelesai || null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
form.adhocShifts.forEach((shift, index) => {
|
||||||
|
emittedData.push({
|
||||||
|
id: Date.now() + index,
|
||||||
|
poliId: form.poliId,
|
||||||
|
poliName,
|
||||||
|
dokter: form.dokter,
|
||||||
|
tipe: 'Adhoc',
|
||||||
|
mulai: `${form.adhocTanggal}T${shift.jamMulai}:00`,
|
||||||
|
selesai: `${form.adhocTanggal}T${shift.jamSelesai}:00`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('save', emittedData);
|
||||||
|
dialogOpen.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
import MiniCalendarPreview from './MiniCalendarPreview.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-dialog v-model="dialogOpen" max-width="520px" persistent>
|
||||||
|
<v-card rounded="lg" class="pa-0">
|
||||||
|
<!-- Header -->
|
||||||
|
<v-card-title class="bg-primary text-white pa-5">
|
||||||
|
<div class="d-flex justify-space-between align-center w-100">
|
||||||
|
<div class="d-flex align-center gap-2">
|
||||||
|
<v-icon>mdi-calendar-plus</v-icon>
|
||||||
|
<span class="text-h6 font-weight-bold">Tambah Jadwal Dokter</span>
|
||||||
|
</div>
|
||||||
|
<v-btn icon="mdi-close" variant="text" size="small" class="text-white"
|
||||||
|
@click="dialogOpen = false" />
|
||||||
|
</div>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="pa-6">
|
||||||
|
|
||||||
|
<!-- Tipe Jadwal -->
|
||||||
|
<div class="text-caption text-grey-darken-1 mb-2 font-weight-medium">Tipe Jadwal</div>
|
||||||
|
<v-btn-toggle v-model="form.tipe" mandatory color="primary" variant="outlined" class="mb-5 w-100"
|
||||||
|
divided>
|
||||||
|
<v-btn value="Rutin" class="flex-grow-1">
|
||||||
|
<v-icon start>mdi-calendar-sync</v-icon>
|
||||||
|
Rutin
|
||||||
|
</v-btn>
|
||||||
|
<v-btn value="Adhoc" class="flex-grow-1">
|
||||||
|
<v-icon start>mdi-calendar-clock</v-icon>
|
||||||
|
Adhoc
|
||||||
|
</v-btn>
|
||||||
|
</v-btn-toggle>
|
||||||
|
|
||||||
|
<!-- Info poli & dokter -->
|
||||||
|
<v-row class="mb-4">
|
||||||
|
<v-col cols="12" sm="12">
|
||||||
|
<div class="text-caption text-grey mb-1">Poli / Klinik <span class="text-error">*</span></div>
|
||||||
|
<v-select v-model="form.poliId" :items="clinicList" item-title="title" item-value="id"
|
||||||
|
density="compact" variant="outlined" hide-details="auto" placeholder="Pilih Poli"
|
||||||
|
prepend-inner-icon="mdi-hospital-building" @update:model-value="form.dokter = null"
|
||||||
|
:error-messages="formErrors.poli" />
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="12">
|
||||||
|
<div class="text-caption text-grey mb-1">Dokter <span class="text-error">*</span></div>
|
||||||
|
<v-select v-model="form.dokter" :items="availableDoctors" density="compact" variant="outlined"
|
||||||
|
hide-details="auto" placeholder="Pilih Dokter" prepend-inner-icon="mdi-doctor"
|
||||||
|
:disabled="!form.poliId" :error-messages="formErrors.dokter" />
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- ── Form Rutin ──────────────────────────────── -->
|
||||||
|
<template v-if="form.tipe === 'Rutin'">
|
||||||
|
<div class="text-caption text-grey-darken-1 mb-3 font-weight-medium">Pilih Hari Kerja</div>
|
||||||
|
<div class="d-flex flex-wrap gap-2 mb-1">
|
||||||
|
<v-chip class="mr-1" v-for="day in DAYS_OF_WEEK" :key="day"
|
||||||
|
:color="form.hari === day ? 'primary' : 'primary'"
|
||||||
|
:variant="form.hari === day ? 'flat' : 'outlined'" size="small" style="cursor:pointer;"
|
||||||
|
@click="form.hari = day">
|
||||||
|
{{ day }}
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
<div v-if="formErrors.hari" class="text-caption text-error mb-4">{{ formErrors.hari }}</div>
|
||||||
|
|
||||||
|
<MiniCalendarPreview v-if="form.poliId && form.dokter" :tipe="form.tipe" :hari="form.hari"
|
||||||
|
:adhoc-tanggal="form.adhocTanggal" :jadwal-list="jadwalList" :poli-id="form.poliId"
|
||||||
|
:dokter="form.dokter" />
|
||||||
|
|
||||||
|
<!-- Jam required -->
|
||||||
|
<div v-for="(shift, index) in form.shifts" :key="index" class="d-flex align-center gap-4 mt-4">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="6">
|
||||||
|
<div class="text-caption text-grey mb-1">Jam Mulai</div>
|
||||||
|
<v-text-field v-model="shift.jamMulai" type="time" variant="outlined"
|
||||||
|
density="compact" hide-details required
|
||||||
|
:error-messages="formErrors[`shift_${index}_jamMulai`]" />
|
||||||
|
<div v-if="formErrors[`shift_${index}_jamMulai`]"
|
||||||
|
class="text-caption text-error mt-1">{{ formErrors[`shift_${index}_jamMulai`] }}
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6">
|
||||||
|
<div class="text-caption text-grey mb-1">Jam Selesai</div>
|
||||||
|
<v-text-field v-model="shift.jamSelesai" type="time" variant="outlined"
|
||||||
|
density="compact" hide-details required
|
||||||
|
:error-messages="formErrors[`shift_${index}_jamSelesai`]" />
|
||||||
|
<div v-if="formErrors[`shift_${index}_jamSelesai`]"
|
||||||
|
class="text-caption text-error mt-1">{{ formErrors[`shift_${index}_jamSelesai`]
|
||||||
|
}}</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5" v-if="form.shifts.length > 1">
|
||||||
|
<v-btn icon="mdi-trash-can-outline" variant="text" color="error" size="small"
|
||||||
|
@click="form.shifts.splice(index, 1)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<v-btn variant="outlined" color="primary" size="small" class="text-none"
|
||||||
|
@click="form.shifts.push({ jamMulai: '', jamSelesai: '' })">
|
||||||
|
+ Tambah Jadwal {{ form.hari ? `Hari ${form.hari}` : '' }}
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ── Form Adhoc ──────────────────────────────── -->
|
||||||
|
<template v-else>
|
||||||
|
<v-row class="mb-2">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-text-field v-model="form.adhocTanggal" label="Tanggal" type="date" variant="outlined"
|
||||||
|
density="compact" prepend-inner-icon="mdi-calendar" hide-details="auto"
|
||||||
|
:error-messages="formErrors.adhocTanggal" />
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
<MiniCalendarPreview v-if="form.poliId && form.dokter" :tipe="form.tipe" :hari="form.hari"
|
||||||
|
:adhoc-tanggal="form.adhocTanggal" :jadwal-list="jadwalList" :poli-id="form.poliId"
|
||||||
|
:dokter="form.dokter" />
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<div v-for="(shift, index) in form.adhocShifts" :key="index" class="d-flex align-center gap-4 mt-2">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="6">
|
||||||
|
<div class="text-caption text-grey mb-1">Jam Mulai</div>
|
||||||
|
<v-text-field v-model="shift.jamMulai" type="time" variant="outlined"
|
||||||
|
density="compact" hide-details required
|
||||||
|
:error-messages="formErrors[`adhocShift_${index}_jamMulai`]" />
|
||||||
|
<div v-if="formErrors[`adhocShift_${index}_jamMulai`]"
|
||||||
|
class="text-caption text-error mt-1">{{
|
||||||
|
formErrors[`adhocShift_${index}_jamMulai`] }}</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6">
|
||||||
|
<div class="text-caption text-grey mb-1">Jam Selesai</div>
|
||||||
|
<v-text-field v-model="shift.jamSelesai" type="time" variant="outlined"
|
||||||
|
density="compact" hide-details required
|
||||||
|
:error-messages="formErrors[`adhocShift_${index}_jamSelesai`]" />
|
||||||
|
<div v-if="formErrors[`adhocShift_${index}_jamSelesai`]"
|
||||||
|
class="text-caption text-error mt-1">{{
|
||||||
|
formErrors[`adhocShift_${index}_jamSelesai`] }}</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5" v-if="form.adhocShifts.length > 1">
|
||||||
|
<v-btn icon="mdi-trash-can-outline" variant="text" color="error" size="small"
|
||||||
|
@click="form.adhocShifts.splice(index, 1)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<v-btn variant="outlined" color="primary" size="small" class="text-none"
|
||||||
|
@click="form.adhocShifts.push({ jamMulai: '', jamSelesai: '' })">
|
||||||
|
+ Tambah Shift Adhoc
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<v-card-actions class="pa-5 justify-end gap-2">
|
||||||
|
<v-btn variant="text" color="grey" @click="dialogOpen = false">Batal</v-btn>
|
||||||
|
<v-btn color="primary" variant="flat" prepend-icon="mdi-content-save" @click="handleSave">
|
||||||
|
Simpan Jadwal
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,344 @@
|
|||||||
|
<script setup>
|
||||||
|
/**
|
||||||
|
* JadwalDokterCalendar — Mode tampilan kalender untuk jadwal dokter.
|
||||||
|
* Menampilkan filter poli/dokter dan v-calendar Vuetify dengan event
|
||||||
|
* jadwal rutin (primary) dan adhoc (secondary).
|
||||||
|
* Menerima data dari parent dan emit interaksi kembali ke parent.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const MONTH_NAMES = [
|
||||||
|
'Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni',
|
||||||
|
'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'
|
||||||
|
];
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
/** Daftar klinik/poli untuk filter */
|
||||||
|
clinicList: { type: Array, default: () => [] },
|
||||||
|
/** Daftar event kalender yang sudah di-generate oleh parent */
|
||||||
|
calendarEvents: { type: Array, default: () => [] },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits([
|
||||||
|
'update:selectedPoli',
|
||||||
|
'update:selectedDoctor',
|
||||||
|
'date-click',
|
||||||
|
'event-click',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ── Filter State (local, synced ke parent via emit) ──────────────────────
|
||||||
|
|
||||||
|
/** Poli yang dipilih di mode calendar */
|
||||||
|
const selectedPoli = defineModel('selectedPoli', { type: [String, Number, null], default: null });
|
||||||
|
|
||||||
|
/** Dokter yang dipilih di mode calendar */
|
||||||
|
const selectedDoctor = defineModel('selectedDoctor', { type: [String, null], default: null });
|
||||||
|
|
||||||
|
|
||||||
|
const selectedElement = ref(null)
|
||||||
|
const selectedOpen = ref(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Daftar dokter berdasarkan poli yang dipilih.
|
||||||
|
* @returns {string[]} Kosong jika poli belum dipilih
|
||||||
|
*/
|
||||||
|
const doctorList = computed(() => {
|
||||||
|
if (!selectedPoli.value) return [];
|
||||||
|
const clinic = props.clinicList.find(c => c.id === selectedPoli.value);
|
||||||
|
return clinic?.doctors ?? [];
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Reset dokter saat poli berubah */
|
||||||
|
watch(selectedPoli, () => { selectedDoctor.value = null; });
|
||||||
|
|
||||||
|
// ── Navigasi Kalender ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const isDailyEventsOpen = ref(false);
|
||||||
|
const dailyEvents = ref([]);
|
||||||
|
const selectedDateFormatted = ref('');
|
||||||
|
|
||||||
|
|
||||||
|
/** Tanggal referensi untuk navigasi bulan */
|
||||||
|
const viewDate = ref(new Date());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key dinamis untuk v-calendar. Vuetify v-calendar punya internal navigation
|
||||||
|
* state sendiri — mengubah :model-value saja tidak cukup. Dengan mengganti :key,
|
||||||
|
* kita paksa komponen re-mount sehingga bulan yang tampil ikut berubah.
|
||||||
|
*/
|
||||||
|
const calendarKey = computed(() =>
|
||||||
|
`cal-${viewDate.value.getFullYear()}-${viewDate.value.getMonth()}`
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Judul bulan & tahun aktif */
|
||||||
|
const calendarTitle = computed(() =>
|
||||||
|
`${MONTH_NAMES[viewDate.value.getMonth()]} ${viewDate.value.getFullYear()}`
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Navigasi ke bulan sebelumnya */
|
||||||
|
const prevMonth = () => {
|
||||||
|
const d = new Date(viewDate.value);
|
||||||
|
d.setMonth(d.getMonth() - 1);
|
||||||
|
viewDate.value = new Date(d);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Navigasi ke bulan berikutnya */
|
||||||
|
const nextMonth = () => {
|
||||||
|
const d = new Date(viewDate.value);
|
||||||
|
d.setMonth(d.getMonth() + 1);
|
||||||
|
viewDate.value = new Date(d);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menangani klik pada tanggal di kalender.
|
||||||
|
* @param {Date|string|object} info - Payload dari @click:date
|
||||||
|
*/
|
||||||
|
const handleDateClick = (info, { date }) => {
|
||||||
|
emit('date-click', date);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menangani klik pada event di kalender.
|
||||||
|
* @param {object} payload - Payload dari @click:event v-calendar
|
||||||
|
*/
|
||||||
|
const handleEventClick = (nativeEvent, { event }) => {
|
||||||
|
const ev = event;
|
||||||
|
if (!ev?.extendedProps) return;
|
||||||
|
const d = ev.start instanceof Date ? ev.start : new Date(ev.start);
|
||||||
|
emit('event-click', {
|
||||||
|
...ev.extendedProps,
|
||||||
|
tanggal: d.toLocaleDateString('id-ID', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Menangani klik "+X lainnya" pada kalender.
|
||||||
|
* @param {Date|string|object} info - Payload dari @click:more
|
||||||
|
*/
|
||||||
|
const handleMoreClick = (nativeEvent, { date }) => {
|
||||||
|
selectedDateFormatted.value = date;
|
||||||
|
console.log("date", date)
|
||||||
|
console.log("calendarEvents.value", props.calendarEvents)
|
||||||
|
dailyEvents.value = props.calendarEvents.filter(e =>
|
||||||
|
e.start.toISOString().split("T")[0] === selectedDateFormatted.value
|
||||||
|
);
|
||||||
|
|
||||||
|
const open = () => {
|
||||||
|
// selectedEvent.value = event
|
||||||
|
selectedElement.value = nativeEvent.target
|
||||||
|
requestAnimationFrame(() => requestAnimationFrame(() => selectedOpen.value = true))
|
||||||
|
}
|
||||||
|
if (selectedOpen.value) {
|
||||||
|
selectedOpen.value = false
|
||||||
|
requestAnimationFrame(() => requestAnimationFrame(() => open()))
|
||||||
|
} else {
|
||||||
|
open()
|
||||||
|
}
|
||||||
|
nativeEvent.stopPropagation()
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menangani klik pada event di dalam dialog harian.
|
||||||
|
*/
|
||||||
|
const handleDailyEventClick = (ev) => {
|
||||||
|
isDailyEventsOpen.value = false;
|
||||||
|
const payload = { event: ev };
|
||||||
|
handleEventClick(payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- ── Filter Card ─────────────────────────────────────── -->
|
||||||
|
<v-row class="mb-2">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card class="pa-6" color="white">
|
||||||
|
<div class="d-flex align-center mb-4">
|
||||||
|
<v-icon color="primary" class="mr-2">mdi-filter-variant</v-icon>
|
||||||
|
<h3 class="text-subtitle-1 font-weight-bold text-primary">FILTER JADWAL</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-select v-model="selectedPoli" :items="clinicList" item-title="title" item-value="id"
|
||||||
|
label="Pilih Poli / Klinik" variant="outlined" density="compact" clearable
|
||||||
|
prepend-inner-icon="mdi-hospital-building" hide-details />
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-select v-model="selectedDoctor" :items="doctorList" label="Pilih Dokter"
|
||||||
|
variant="outlined" density="compact" clearable prepend-inner-icon="mdi-doctor"
|
||||||
|
:disabled="!selectedPoli" hide-details
|
||||||
|
:placeholder="selectedPoli ? 'Pilih dokter...' : 'Pilih poli terlebih dahulu'" />
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<div v-if="selectedPoli || selectedDoctor" class="mt-4 d-flex align-center gap-2 flex-wrap">
|
||||||
|
<span class="text-caption text-grey-darken-1">Filter aktif:</span>
|
||||||
|
<v-chip v-if="selectedPoli" size="small" color="primary" variant="tonal" closable
|
||||||
|
@click:close="selectedPoli = null">
|
||||||
|
<v-icon start size="x-small">mdi-hospital-building</v-icon>
|
||||||
|
{{clinicList.find(c => c.id === selectedPoli)?.title}}
|
||||||
|
</v-chip>
|
||||||
|
<v-chip v-if="selectedDoctor" size="small" color="secondary" variant="tonal" closable
|
||||||
|
@click:close="selectedDoctor = null">
|
||||||
|
<v-icon start size="x-small">mdi-doctor</v-icon>
|
||||||
|
{{ selectedDoctor }}
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- ── Calendar Card ───────────────────────────────────── -->
|
||||||
|
<v-row v-if="selectedDoctor && selectedPoli">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card elevation="2" rounded="lg" color="white">
|
||||||
|
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<div class="d-flex align-center justify-space-between px-6 pt-5 pb-4">
|
||||||
|
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-btn icon="mdi-chevron-left" variant="text" size="small" @click="prevMonth" />
|
||||||
|
<v-btn icon="mdi-chevron-right" variant="text" size="small" @click="nextMonth" />
|
||||||
|
<span class="text-body-1 font-weight-bold" style="min-width:150px;text-align:center;">
|
||||||
|
{{ calendarTitle }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- Legend -->
|
||||||
|
<div class="d-flex align-center gap-3">
|
||||||
|
<div class="d-flex align-center gap-1">
|
||||||
|
<span class="legend-dot bg-primary"></span>
|
||||||
|
<span class="text-caption ml-1">Rutin</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-center gap-1 ml-1">
|
||||||
|
<span class="legend-dot bg-secondary"></span>
|
||||||
|
<span class="text-caption ml-1">Adhoc</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<!-- v-calendar Vuetify -->
|
||||||
|
<v-calendar :interval-height="100" locale="id" :key="calendarKey" :model-value="viewDate"
|
||||||
|
:events="calendarEvents" view-mode="month" event-more-text="{0} lainnya" class="jadwal-calendar"
|
||||||
|
@click:date="handleDateClick" @click:event="handleEventClick" @click:more="handleMoreClick" />
|
||||||
|
<v-menu v-model="selectedOpen" :activator="selectedElement" :close-on-content-click="false"
|
||||||
|
location="end">
|
||||||
|
<v-card color="grey-lighten-4" min-width="350px" flat>
|
||||||
|
<v-toolbar color="primary" dark>
|
||||||
|
<v-btn icon>
|
||||||
|
<v-icon>mdi-calendar</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-toolbar-title>Jadwal Lainnya</v-toolbar-title>
|
||||||
|
</v-toolbar>
|
||||||
|
<v-card-text style="max-height: 300px; overflow-y: auto;">
|
||||||
|
<v-list v-if="dailyEvents.length > 0" lines="two" bg-color="transparent">
|
||||||
|
<v-list-item v-for="(ev, idx) in dailyEvents" :key="idx" class="mb-3 rounded border"
|
||||||
|
:class="ev.color === 'primary' ? 'border-primary' : 'border-secondary'"
|
||||||
|
@click="handleDailyEventClick(ev)" hover>
|
||||||
|
<v-list-item-title class="font-weight-bold">
|
||||||
|
<v-chip :color="ev.color" size="small" class="mr-2">{{
|
||||||
|
ev.extendedProps?.tipe }}</v-chip>
|
||||||
|
{{ ev.name }}
|
||||||
|
</v-list-item-title>
|
||||||
|
<v-list-item-subtitle class="mt-1">
|
||||||
|
<v-icon size="x-small" class="mr-1">mdi-doctor</v-icon>{{
|
||||||
|
ev.extendedProps?.dokter }}
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
<v-icon size="x-small" class="mr-1">mdi-hospital-building</v-icon>{{
|
||||||
|
ev.extendedProps?.poli }}
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
<div v-else class="text-center pa-6 text-grey">
|
||||||
|
Tidak ada jadwal tambahan.
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-menu>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<v-card min-height="400px">
|
||||||
|
<v-card-text>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<div class="text-center pa-12 d-flex align-center justify-center" style="height: 400px;">
|
||||||
|
<div>
|
||||||
|
<v-icon size="48" color="grey-lighten-1"
|
||||||
|
class="mb-2">mdi-calendar-blank-outline</v-icon>
|
||||||
|
<div class="text-body-2 text-grey-lighten-1">Silahkan Pilih Poli dan Dokter terlebih
|
||||||
|
dahulu</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.v-calendar-weekly__day) {
|
||||||
|
height: 125px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jadwal-calendar {
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-dot {
|
||||||
|
display: inline-block;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header hari */
|
||||||
|
:deep(.v-calendar-month__weekday) {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: #283593;
|
||||||
|
background-color: #e8eaf6;
|
||||||
|
padding: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nomor tanggal */
|
||||||
|
:deep(.v-calendar-month__day-label) {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #424242;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-calendar-month__day-label:hover) {
|
||||||
|
background-color: #e8eaf6;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hari ini */
|
||||||
|
:deep(.v-calendar-month__day--today .v-calendar-month__day-label) {
|
||||||
|
background-color: #1a237e;
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Event */
|
||||||
|
:deep(.v-calendar-month__event) {
|
||||||
|
font-size: 0.68rem !important;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-calendar-month__event:hover) {
|
||||||
|
opacity: 0.82;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
<script setup>
|
||||||
|
/**
|
||||||
|
* JadwalDokterList — Mode tampilan daftar (tabel) untuk jadwal dokter.
|
||||||
|
* Menampilkan filter (poli klinik, tanggal, nama dokter) dan tabel
|
||||||
|
* dengan kolom NO, POLI, NAMA DOKTER, JADWAL RUTIN, AKSI.
|
||||||
|
* Mode ini menjadi tampilan default halaman Jadwal Dokter.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
/** Daftar klinik/poli dari store */
|
||||||
|
clinicList: { type: Array, default: () => [] },
|
||||||
|
/** Daftar jadwal yang sudah ditambahkan */
|
||||||
|
jadwalList: { type: Array, default: () => [] },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['edit', 'add']);
|
||||||
|
|
||||||
|
// ── Filter State ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Poli yang dipilih untuk filter */
|
||||||
|
const filterPoli = ref(null);
|
||||||
|
|
||||||
|
/** Tanggal yang dipilih untuk filter */
|
||||||
|
const filterDate = ref(null);
|
||||||
|
|
||||||
|
/** Keyword pencarian nama dokter */
|
||||||
|
const filterDokter = ref('');
|
||||||
|
|
||||||
|
// ── Pagination ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Halaman aktif saat ini */
|
||||||
|
const currentPage = ref(1);
|
||||||
|
|
||||||
|
/** Jumlah baris per halaman */
|
||||||
|
const itemsPerPage = ref(10);
|
||||||
|
|
||||||
|
/** Opsi jumlah baris per halaman */
|
||||||
|
const perPageOptions = [5, 10, 25, 50];
|
||||||
|
|
||||||
|
// ── Mapping Hari ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const DAY_MAP = {
|
||||||
|
0: 'Minggu', 1: 'Senin', 2: 'Selasa', 3: 'Rabu',
|
||||||
|
4: 'Kamis', 5: 'Jumat', 6: 'Sabtu'
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Computed: Data Tabel ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Membangun data tabel dari jadwalList, dikelompokkan per dokter + poli.
|
||||||
|
* Setiap baris menampilkan 1 dokter dengan ringkasan jadwal rutinnya.
|
||||||
|
* @returns {Array<{ id, poliName, poliId, dokter, jadwalRutin, raw }>}
|
||||||
|
*/
|
||||||
|
const tableData = computed(() => {
|
||||||
|
/** Gabungkan jadwal berdasarkan key unik dokter+poli */
|
||||||
|
const grouped = {};
|
||||||
|
|
||||||
|
props.jadwalList.forEach(j => {
|
||||||
|
const key = `${j.poliId}-${j.dokter}`;
|
||||||
|
if (!grouped[key]) {
|
||||||
|
grouped[key] = {
|
||||||
|
id: j.id,
|
||||||
|
poliName: j.poliName,
|
||||||
|
poliId: j.poliId,
|
||||||
|
dokter: j.dokter,
|
||||||
|
hariSet: new Set(),
|
||||||
|
jamMulai: null,
|
||||||
|
jamSelesai: null,
|
||||||
|
raw: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
grouped[key].raw.push(j);
|
||||||
|
|
||||||
|
if (j.tipe === 'Rutin' && j.hari) {
|
||||||
|
j.hari.forEach(h => grouped[key].hariSet.add(h));
|
||||||
|
if (j.jamMulai) grouped[key].jamMulai = j.jamMulai;
|
||||||
|
if (j.jamSelesai) grouped[key].jamSelesai = j.jamSelesai;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.values(grouped).map(g => {
|
||||||
|
const hariArr = Array.from(g.hariSet);
|
||||||
|
/** Format jadwal rutin: "Senin - Kamis\n08:00 - 14:00 WIB" */
|
||||||
|
let jadwalRutin = '';
|
||||||
|
if (hariArr.length > 0) {
|
||||||
|
jadwalRutin = hariArr.join(', ');
|
||||||
|
if (g.jamMulai && g.jamSelesai) {
|
||||||
|
jadwalRutin += `\n${g.jamMulai} - ${g.jamSelesai} WIB`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
jadwalRutin = '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: g.id,
|
||||||
|
poliName: g.poliName,
|
||||||
|
poliId: g.poliId,
|
||||||
|
dokter: g.dokter,
|
||||||
|
jadwalRutin,
|
||||||
|
raw: g.raw,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data tabel yang sudah difilter berdasarkan poli, tanggal, dan keyword nama dokter.
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
const filteredData = computed(() => {
|
||||||
|
let data = tableData.value;
|
||||||
|
|
||||||
|
// Filter poli
|
||||||
|
if (filterPoli.value) {
|
||||||
|
data = data.filter(d => d.poliId === filterPoli.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter tanggal — cocokkan dengan hari dari jadwal rutin
|
||||||
|
if (filterDate.value) {
|
||||||
|
const date = new Date(filterDate.value);
|
||||||
|
const dayName = DAY_MAP[date.getDay()];
|
||||||
|
data = data.filter(d =>
|
||||||
|
d.raw.some(j => j.tipe === 'Rutin' && j.hari?.includes(dayName))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter nama dokter
|
||||||
|
if (filterDokter.value) {
|
||||||
|
const keyword = filterDokter.value.toLowerCase();
|
||||||
|
data = data.filter(d => d.dokter.toLowerCase().includes(keyword));
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Total data setelah filter */
|
||||||
|
const totalItems = computed(() => filteredData.value.length);
|
||||||
|
|
||||||
|
/** Total halaman */
|
||||||
|
const totalPages = computed(() => Math.max(1, Math.ceil(totalItems.value / itemsPerPage.value)));
|
||||||
|
|
||||||
|
/** Data untuk halaman aktif (sliced) */
|
||||||
|
const paginatedData = computed(() => {
|
||||||
|
const start = (currentPage.value - 1) * itemsPerPage.value;
|
||||||
|
return filteredData.value.slice(start, start + itemsPerPage.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Label info pagination: "Menampilkan 1-10 dari 124 dokter spesialis" */
|
||||||
|
const paginationLabel = computed(() => {
|
||||||
|
if (totalItems.value === 0) return 'Tidak ada data';
|
||||||
|
const start = (currentPage.value - 1) * itemsPerPage.value + 1;
|
||||||
|
const end = Math.min(currentPage.value * itemsPerPage.value, totalItems.value);
|
||||||
|
return `Menampilkan ${start}-${end} dari ${totalItems.value} dokter spesialis`;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Reset halaman saat filter berubah */
|
||||||
|
watch([filterPoli, filterDate, filterDokter], () => {
|
||||||
|
currentPage.value = 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigasi ke halaman tertentu.
|
||||||
|
* @param {number} page
|
||||||
|
*/
|
||||||
|
const goToPage = (page) => {
|
||||||
|
if (page >= 1 && page <= totalPages.value) {
|
||||||
|
currentPage.value = page;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- ── Filter Card ─────────────────────────────────────── -->
|
||||||
|
<v-card class="pa-5 mb-6" elevation="2" rounded="lg" color="white">
|
||||||
|
<v-row align="center">
|
||||||
|
<v-col cols="12" sm="4">
|
||||||
|
<div class="text-caption text-grey-darken-1 mb-1 font-weight-medium">Poli Klinik</div>
|
||||||
|
<v-select v-model="filterPoli" :items="clinicList" item-title="title" item-value="id"
|
||||||
|
placeholder="Semua Poli" variant="outlined" density="compact" clearable
|
||||||
|
prepend-inner-icon="mdi-hospital-building" hide-details />
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="4">
|
||||||
|
<div class="text-caption text-grey-darken-1 mb-1 font-weight-medium">Tanggal</div>
|
||||||
|
<v-text-field v-model="filterDate" type="date" placeholder="Pilih Tanggal" variant="outlined"
|
||||||
|
density="compact" clearable prepend-inner-icon="mdi-calendar" hide-details />
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="4">
|
||||||
|
<div class="text-caption text-grey-darken-1 mb-1 font-weight-medium">Nama Dokter</div>
|
||||||
|
<v-text-field v-model="filterDokter" placeholder="Cari Nama Dokter..." variant="outlined"
|
||||||
|
density="compact" clearable prepend-inner-icon="mdi-account-search" hide-details />
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<!-- ── Tabel Jadwal ────────────────────────────────────── -->
|
||||||
|
<v-card elevation="2" rounded="lg" color="white">
|
||||||
|
<v-table class="jadwal-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-left" style="width: 60px;">NO</th>
|
||||||
|
<th class="text-left" style="width: 140px;">POLI</th>
|
||||||
|
<th class="text-left">NAMA DOKTER</th>
|
||||||
|
<th class="text-left" style="width: 200px;">JADWAL RUTIN</th>
|
||||||
|
<th class="text-center" style="width: 100px;">AKSI</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-if="paginatedData.length === 0">
|
||||||
|
<td colspan="5" class="text-center pa-8 text-grey">
|
||||||
|
<v-icon size="48" color="grey-lighten-1" class="mb-2">mdi-calendar-blank-outline</v-icon>
|
||||||
|
<div class="text-body-2">Belum ada data jadwal dokter</div>
|
||||||
|
<div class="text-caption text-grey-lighten-1">Tambahkan jadwal dokter melalui mode
|
||||||
|
kalender</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-for="(item, index) in paginatedData" :key="item.id" class="jadwal-row">
|
||||||
|
<td class="text-body-2 font-weight-medium">
|
||||||
|
{{ (currentPage - 1) * itemsPerPage + index + 1 }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<v-chip size="small" color="primary" variant="tonal">
|
||||||
|
{{ item.poliName }}
|
||||||
|
</v-chip>
|
||||||
|
</td>
|
||||||
|
<td class="text-body-2 font-weight-bold">
|
||||||
|
{{ item.dokter }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="text-body-2" style="white-space: pre-line; line-height: 1.5;">
|
||||||
|
{{ item.jadwalRutin }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<v-btn variant="outlined" size="small" color="primary" rounded="lg"
|
||||||
|
prepend-icon="mdi-pencil" @click="emit('edit', item)">
|
||||||
|
Edit
|
||||||
|
</v-btn>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-table>
|
||||||
|
|
||||||
|
<!-- ── Pagination Footer ──────────────────────────── -->
|
||||||
|
<v-divider />
|
||||||
|
<div class="d-flex align-center justify-space-between px-4 py-3 pagination-footer">
|
||||||
|
<span class="text-caption text-grey-darken-1">{{ paginationLabel }}</span>
|
||||||
|
|
||||||
|
<div class="d-flex align-center gap-3">
|
||||||
|
<div class="d-flex align-center gap-2">
|
||||||
|
<span class="text-caption text-grey-darken-1 mr-2">Baris per halaman:</span>
|
||||||
|
<v-select v-model="itemsPerPage" :items="perPageOptions" variant="plain" density="compact"
|
||||||
|
hide-details style="max-width: 70px;" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex align-center gap-1">
|
||||||
|
<v-btn icon size="x-small" variant="text" :disabled="currentPage <= 1" @click="goToPage(1)">
|
||||||
|
<v-icon size="16">mdi-page-first</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn icon size="x-small" variant="text" :disabled="currentPage <= 1"
|
||||||
|
@click="goToPage(currentPage - 1)">
|
||||||
|
<v-icon size="16">mdi-chevron-left</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn icon size="x-small" variant="text" :disabled="currentPage >= totalPages"
|
||||||
|
@click="goToPage(currentPage + 1)">
|
||||||
|
<v-icon size="16">mdi-chevron-right</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn icon size="x-small" variant="text" :disabled="currentPage >= totalPages"
|
||||||
|
@click="goToPage(totalPages)">
|
||||||
|
<v-icon size="16">mdi-page-last</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.jadwal-table {
|
||||||
|
border-radius: 12px 12px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jadwal-table thead th {
|
||||||
|
background-color: #f5f6fa !important;
|
||||||
|
color: #5a607f !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
font-size: 0.75rem !important;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
border-bottom: 2px solid #e8eaf0 !important;
|
||||||
|
padding: 14px 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jadwal-row {
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jadwal-row:hover {
|
||||||
|
background-color: #f8f9fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jadwal-row td {
|
||||||
|
padding: 16px !important;
|
||||||
|
border-bottom: 1px solid #f0f1f5 !important;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.pagination-footer {
|
||||||
|
background-color: #fafbfc;
|
||||||
|
border-radius: 0 0 12px 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
tipe: { type: String, default: 'Rutin' },
|
||||||
|
hari: { type: String, default: '' },
|
||||||
|
adhocTanggal: { type: String, default: '' },
|
||||||
|
jadwalList: { type: Array, default: () => [] },
|
||||||
|
poliId: { type: [String, Number, null], default: null },
|
||||||
|
dokter: { type: String, default: null }
|
||||||
|
});
|
||||||
|
|
||||||
|
const MONTH_NAMES = [
|
||||||
|
'Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni',
|
||||||
|
'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'
|
||||||
|
];
|
||||||
|
|
||||||
|
const DAY_MAP = {
|
||||||
|
'Minggu': 0, 'Senin': 1, 'Selasa': 2, 'Rabu': 3,
|
||||||
|
'Kamis': 4, 'Jumat': 5, 'Sabtu': 6
|
||||||
|
};
|
||||||
|
|
||||||
|
const viewDate = ref(new Date());
|
||||||
|
|
||||||
|
const calendarKey = computed(() =>
|
||||||
|
`cal-mini-${viewDate.value.getFullYear()}-${viewDate.value.getMonth()}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const calendarTitle = computed(() =>
|
||||||
|
`${MONTH_NAMES[viewDate.value.getMonth()]} ${viewDate.value.getFullYear()}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const prevMonth = () => {
|
||||||
|
const d = new Date(viewDate.value);
|
||||||
|
d.setMonth(d.getMonth() - 1);
|
||||||
|
viewDate.value = new Date(d);
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextMonth = () => {
|
||||||
|
const d = new Date(viewDate.value);
|
||||||
|
d.setMonth(d.getMonth() + 1);
|
||||||
|
viewDate.value = new Date(d);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toDateStr = (date) => {
|
||||||
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const calendarEvents = computed(() => {
|
||||||
|
const events = [];
|
||||||
|
const year = viewDate.value.getFullYear();
|
||||||
|
const month = viewDate.value.getMonth();
|
||||||
|
|
||||||
|
// 1. Generate existing events for the selected doctor
|
||||||
|
const filtered = props.jadwalList.filter(j => {
|
||||||
|
if (props.poliId && j.poliId !== props.poliId) return false;
|
||||||
|
if (props.dokter && j.dokter !== props.dokter) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
filtered.forEach(jadwal => {
|
||||||
|
if (jadwal.tipe === 'Rutin') {
|
||||||
|
for (let mOffset = -1; mOffset <= 1; mOffset++) {
|
||||||
|
const tYear = month + mOffset < 0 ? year - 1 : month + mOffset > 11 ? year + 1 : year;
|
||||||
|
const tMonth = (month + mOffset + 12) % 12;
|
||||||
|
const daysInMonth = new Date(tYear, tMonth + 1, 0).getDate();
|
||||||
|
|
||||||
|
for (let d = 1; d <= daysInMonth; d++) {
|
||||||
|
const date = new Date(tYear, tMonth, d);
|
||||||
|
const dayName = Object.keys(DAY_MAP).find(k => DAY_MAP[k] === date.getDay());
|
||||||
|
|
||||||
|
let jadwalHariArray = [];
|
||||||
|
if (Array.isArray(jadwal.hari)) {
|
||||||
|
jadwalHariArray = jadwal.hari;
|
||||||
|
} else if (typeof jadwal.hari === 'string') {
|
||||||
|
jadwalHariArray = [jadwal.hari];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!jadwalHariArray.includes(dayName)) continue;
|
||||||
|
|
||||||
|
const dateStr = toDateStr(date);
|
||||||
|
const jamLabel = jadwal.jamMulai && jadwal.jamSelesai
|
||||||
|
? ` ${jadwal.jamMulai} - ${jadwal.jamSelesai}`
|
||||||
|
: '';
|
||||||
|
events.push({
|
||||||
|
name: `${jamLabel}`,
|
||||||
|
start: jadwal.jamMulai ? new Date(`${dateStr}T${jadwal.jamMulai}:00`) : date,
|
||||||
|
end: jadwal.jamSelesai ? new Date(`${dateStr}T${jadwal.jamSelesai}:00`) : date,
|
||||||
|
color: 'primary',
|
||||||
|
allDay: !jadwal.jamMulai,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const adhocMulaiTime = jadwal.mulai ? jadwal.mulai.split('T')[1]?.slice(0, 5) : '';
|
||||||
|
const adhocSelesaiTime = jadwal.selesai ? jadwal.selesai.split('T')[1]?.slice(0, 5) : '';
|
||||||
|
const adhocJamLabel = adhocMulaiTime && adhocSelesaiTime
|
||||||
|
? ` ${adhocMulaiTime} - ${adhocSelesaiTime}`
|
||||||
|
: '';
|
||||||
|
events.push({
|
||||||
|
name: `${adhocJamLabel}`,
|
||||||
|
start: new Date(jadwal.mulai),
|
||||||
|
end: new Date(jadwal.selesai),
|
||||||
|
color: 'secondary',
|
||||||
|
allDay: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return events;
|
||||||
|
});
|
||||||
|
|
||||||
|
const calendarRef = ref(null);
|
||||||
|
|
||||||
|
const updateHighlight = () => {
|
||||||
|
// Wait for calendar to fully render internally
|
||||||
|
setTimeout(() => {
|
||||||
|
const calendarEl = calendarRef.value?.$el || document.querySelector('.mini-calendar');
|
||||||
|
if (!calendarEl) return;
|
||||||
|
|
||||||
|
// Remove previous highlights
|
||||||
|
const previous = calendarEl.querySelectorAll('.adhoc-highlight, .rutin-highlight');
|
||||||
|
previous.forEach(el => {
|
||||||
|
el.classList.remove('adhoc-highlight', 'rutin-highlight');
|
||||||
|
el.style.border = '';
|
||||||
|
el.style.borderRadius = '';
|
||||||
|
el.style.color = '';
|
||||||
|
el.style.fontWeight = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const allDays = calendarEl.querySelectorAll('.v-calendar-weekly__day');
|
||||||
|
if (allDays.length === 0) return;
|
||||||
|
|
||||||
|
if (props.tipe === 'Rutin' && props.hari) {
|
||||||
|
const targetCol = DAY_MAP[props.hari];
|
||||||
|
allDays.forEach((dayEl, index) => {
|
||||||
|
// Check if index matches column and it's not from adjacent month
|
||||||
|
if (index % 7 === targetCol && !dayEl.classList.contains('v-calendar-weekly__day--adjacent')) {
|
||||||
|
const labelBtn = dayEl.querySelector('.v-calendar-weekly__day-label .v-btn, .v-calendar-weekly__day-label');
|
||||||
|
if (labelBtn) {
|
||||||
|
labelBtn.classList.add('rutin-highlight');
|
||||||
|
// Fallback inline styles in case CSS scoped issue occurs
|
||||||
|
labelBtn.style.border = '2px solid rgb(var(--v-theme-primary))';
|
||||||
|
labelBtn.style.borderRadius = '50%';
|
||||||
|
labelBtn.style.color = 'rgb(var(--v-theme-primary))';
|
||||||
|
labelBtn.style.fontWeight = 'bold';
|
||||||
|
labelBtn.style.width = '28px';
|
||||||
|
labelBtn.style.height = '28px';
|
||||||
|
labelBtn.style.display = 'flex';
|
||||||
|
labelBtn.style.alignItems = 'center';
|
||||||
|
labelBtn.style.justifyContent = 'center';
|
||||||
|
labelBtn.style.margin = '2px auto';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (props.tipe === 'Adhoc' && props.adhocTanggal) {
|
||||||
|
const adhocDate = new Date(props.adhocTanggal);
|
||||||
|
if (adhocDate.getMonth() !== viewDate.value.getMonth() || adhocDate.getFullYear() !== viewDate.value.getFullYear()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayToHighlight = adhocDate.getDate().toString();
|
||||||
|
allDays.forEach(dayEl => {
|
||||||
|
if (!dayEl.classList.contains('v-calendar-weekly__day--adjacent')) {
|
||||||
|
const labelTextEl = dayEl.querySelector('.v-calendar-weekly__day-label .v-btn__content, .v-calendar-weekly__day-label');
|
||||||
|
const labelBtn = dayEl.querySelector('.v-calendar-weekly__day-label .v-btn, .v-calendar-weekly__day-label');
|
||||||
|
if (labelTextEl && labelTextEl.textContent.trim() === dayToHighlight) {
|
||||||
|
labelBtn.classList.add('adhoc-highlight');
|
||||||
|
labelBtn.style.border = '2px solid rgb(var(--v-theme-primary))';
|
||||||
|
labelBtn.style.borderRadius = '50%';
|
||||||
|
labelBtn.style.color = 'rgb(var(--v-theme-primary))';
|
||||||
|
labelBtn.style.fontWeight = 'bold';
|
||||||
|
labelBtn.style.width = '28px';
|
||||||
|
labelBtn.style.height = '28px';
|
||||||
|
labelBtn.style.display = 'flex';
|
||||||
|
labelBtn.style.alignItems = 'center';
|
||||||
|
labelBtn.style.justifyContent = 'center';
|
||||||
|
labelBtn.style.margin = '2px auto';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 150);
|
||||||
|
};
|
||||||
|
|
||||||
|
import { watch, nextTick } from 'vue';
|
||||||
|
watch([() => props.adhocTanggal, () => props.hari, () => props.tipe, viewDate], () => {
|
||||||
|
nextTick(() => updateHighlight());
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-card class="mt-4 border border-grey-lighten-3 rounded-lg" elevation="0">
|
||||||
|
<div class="d-flex align-center justify-space-between px-4 pt-3 pb-2">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-btn icon="mdi-chevron-left" variant="text" size="x-small" @click="prevMonth" />
|
||||||
|
<v-btn icon="mdi-chevron-right" variant="text" size="x-small" @click="nextMonth" />
|
||||||
|
<span class="text-caption font-weight-bold mx-2" style="min-width:100px;text-align:center;">
|
||||||
|
{{ calendarTitle }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- We use a custom class to inject CSS for highlighted days if they have preview event -->
|
||||||
|
<v-calendar ref="calendarRef" :interval-height="40" locale="id" :key="calendarKey" :model-value="viewDate"
|
||||||
|
:events="calendarEvents" view-mode="month"
|
||||||
|
:class="['mini-calendar', props.tipe === 'Rutin' && props.hari ? 'preview-hari-' + props.hari : '']" />
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.v-calendar-weekly__day) {
|
||||||
|
height: 70px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-calendar {
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
background-color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header hari */
|
||||||
|
:deep(.v-calendar-weekly__weekday) {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #555;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nomor tanggal */
|
||||||
|
:deep(.v-calendar-weekly__day-label) {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #424242;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lingkaran Preview via JS Class */
|
||||||
|
:deep(.rutin-highlight),
|
||||||
|
:deep(.adhoc-highlight) {
|
||||||
|
border: 2px solid rgb(var(--v-theme-primary)) !important;
|
||||||
|
border-radius: 50% !important;
|
||||||
|
color: rgb(var(--v-theme-primary)) !important;
|
||||||
|
font-weight: bold !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Event styles */
|
||||||
|
:deep(.v-calendar-weekly__event) {
|
||||||
|
font-size: 0.6rem !important;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
padding: 0 4px !important;
|
||||||
|
margin-bottom: 2px !important;
|
||||||
|
min-height: 16px !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,383 @@
|
|||||||
|
<script setup>
|
||||||
|
/**
|
||||||
|
* Halaman Jadwal Dokter — Orchestrator utama.
|
||||||
|
* Menyediakan 2 mode tampilan:
|
||||||
|
* 1. List (default) — tabel dengan filter poli, tanggal, nama dokter
|
||||||
|
* 2. Calendar — kalender bulanan dengan event jadwal
|
||||||
|
*
|
||||||
|
* State management (jadwalList, selectedPoli, selectedDoctor) dikelola di sini
|
||||||
|
* dan diteruskan ke child component via props/defineModel.
|
||||||
|
*/
|
||||||
|
import PageHeader from '@/components/common/PageHeader.vue';
|
||||||
|
import JadwalDokterList from '@/components/features/master/jadwalDokter/JadwalDokterList.vue';
|
||||||
|
import JadwalDokterCalendar from '@/components/features/master/jadwalDokter/JadwalDokterCalendar.vue';
|
||||||
|
import DialogTambahJadwal from '@/components/features/master/jadwalDokter/DialogTambahJadwal.vue';
|
||||||
|
import DialogDetailEvent from '@/components/features/master/jadwalDokter/DialogDetailEvent.vue';
|
||||||
|
import { useClinicStore } from '@/stores/clinicStore.js';
|
||||||
|
|
||||||
|
// ── Tanggal Header ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Mengembalikan string tanggal hari ini dalam format Indonesia */
|
||||||
|
const currentDate = computed(() => {
|
||||||
|
const now = new Date();
|
||||||
|
const days = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu'];
|
||||||
|
const months = ['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni',
|
||||||
|
'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'];
|
||||||
|
return `${days[now.getDay()]}, ${now.getDate()} ${months[now.getMonth()]} ${now.getFullYear()}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Mode Toggle ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Mode tampilan aktif: 'list' (default) atau 'calendar' */
|
||||||
|
const viewMode = ref('list');
|
||||||
|
|
||||||
|
// ── Store & Data ───────────────────────────────────────────────────────────
|
||||||
|
const clinicStore = useClinicStore();
|
||||||
|
|
||||||
|
/** Daftar semua klinik/poli dari store */
|
||||||
|
const clinicList = computed(() =>
|
||||||
|
clinicStore.clinics.map(c => ({ id: c.id, title: c.name, kode: c.kode, doctors: c.doctors ?? [] }))
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Poli yang dipilih (untuk mode calendar & dialog) */
|
||||||
|
const selectedPoli = ref(null);
|
||||||
|
|
||||||
|
/** Dokter yang dipilih (untuk mode calendar & dialog) */
|
||||||
|
const selectedDoctor = ref(null);
|
||||||
|
|
||||||
|
// ── Konstanta ──────────────────────────────────────────────────────────────
|
||||||
|
const DAY_MAP = {
|
||||||
|
'Minggu': 0, 'Senin': 1, 'Selasa': 2, 'Rabu': 3,
|
||||||
|
'Kamis': 4, 'Jumat': 5, 'Sabtu': 6
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Storage Jadwal (local state) ───────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Daftar jadwal yang sudah ditambahkan.
|
||||||
|
* Setiap entry: { id, poliId, poliName, dokter, tipe, hari?, jamMulai?, jamSelesai?, mulai?, selesai? }
|
||||||
|
*/
|
||||||
|
const jadwalList = ref([]);
|
||||||
|
|
||||||
|
jadwalList.value = [
|
||||||
|
{
|
||||||
|
"id": 1784689277259,
|
||||||
|
"poliId": 1000,
|
||||||
|
"poliName": "ANAK",
|
||||||
|
"dokter": "dr. Sarah Putri, Sp.A",
|
||||||
|
"tipe": "Adhoc",
|
||||||
|
"mulai": "2026-07-05T07:00:00",
|
||||||
|
"selesai": "2026-07-05T08:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1784689301842,
|
||||||
|
"poliId": 1000,
|
||||||
|
"poliName": "ANAK",
|
||||||
|
"dokter": "dr. Sarah Putri, Sp.A",
|
||||||
|
"tipe": "Rutin",
|
||||||
|
"hari": [
|
||||||
|
"Minggu",
|
||||||
|
],
|
||||||
|
"jamMulai": "10:00",
|
||||||
|
"jamSelesai": "12:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1784689301842,
|
||||||
|
"poliId": 1000,
|
||||||
|
"poliName": "ANAK",
|
||||||
|
"dokter": "dr. Sarah Putri, Sp.A",
|
||||||
|
"tipe": "Rutin",
|
||||||
|
"hari": [
|
||||||
|
"Kamis",
|
||||||
|
],
|
||||||
|
"jamMulai": "11:00",
|
||||||
|
"jamSelesai": "13:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1784689305888,
|
||||||
|
"poliId": 1000,
|
||||||
|
"poliName": "ANAK",
|
||||||
|
"dokter": "dr. Sarah Putri, Sp.A",
|
||||||
|
"tipe": "Adhoc",
|
||||||
|
"mulai": "2026-07-05T13:00:00",
|
||||||
|
"selesai": "2026-07-05T14:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 178468930123,
|
||||||
|
"poliId": 1000,
|
||||||
|
"poliName": "ANAK",
|
||||||
|
"dokter": "dr. Sarah Putri, Sp.A",
|
||||||
|
"tipe": "Adhoc",
|
||||||
|
"mulai": "2026-07-05T19:00:00",
|
||||||
|
"selesai": "2026-07-05T20:00:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// ── Generate Events Kalender ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format tanggal ke string YYYY-MM-DD.
|
||||||
|
* @param {Date} date
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const toDateStr = (date) => {
|
||||||
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Tanggal referensi untuk navigasi bulan (digunakan oleh calendarEvents) */
|
||||||
|
const viewDate = ref(new Date());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menghasilkan event kalender dari jadwalList yang tersimpan,
|
||||||
|
* difilter berdasarkan selectedPoli dan selectedDoctor.
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
const calendarEvents = computed(() => {
|
||||||
|
const events = [];
|
||||||
|
const year = viewDate.value.getFullYear();
|
||||||
|
const month = viewDate.value.getMonth();
|
||||||
|
|
||||||
|
const filtered = jadwalList.value.filter(j => {
|
||||||
|
if (selectedPoli.value && j.poliId !== selectedPoli.value) return false;
|
||||||
|
if (selectedDoctor.value && j.dokter !== selectedDoctor.value) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
filtered.forEach(jadwal => {
|
||||||
|
if (jadwal.tipe === 'Rutin') {
|
||||||
|
for (let mOffset = -1; mOffset <= 1; mOffset++) {
|
||||||
|
const tYear = month + mOffset < 0 ? year - 1 : month + mOffset > 11 ? year + 1 : year;
|
||||||
|
const tMonth = (month + mOffset + 12) % 12;
|
||||||
|
const daysInMonth = new Date(tYear, tMonth + 1, 0).getDate();
|
||||||
|
|
||||||
|
for (let d = 1; d <= daysInMonth; d++) {
|
||||||
|
const date = new Date(tYear, tMonth, d);
|
||||||
|
const dayName = Object.keys(DAY_MAP).find(k => DAY_MAP[k] === date.getDay());
|
||||||
|
if (!jadwal.hari.includes(dayName)) continue;
|
||||||
|
|
||||||
|
const dateStr = toDateStr(date);
|
||||||
|
/** Tambahkan jam ke title jika tersedia */
|
||||||
|
const jamLabel = jadwal.jamMulai && jadwal.jamSelesai
|
||||||
|
? ` ${jadwal.jamMulai} - ${jadwal.jamSelesai}`
|
||||||
|
: '';
|
||||||
|
events.push({
|
||||||
|
name: `${jamLabel}`,
|
||||||
|
start: jadwal.jamMulai ? new Date(`${dateStr}T${jadwal.jamMulai}:00`) : date,
|
||||||
|
end: jadwal.jamSelesai ? new Date(`${dateStr}T${jadwal.jamSelesai}:00`) : date,
|
||||||
|
color: 'primary',
|
||||||
|
allDay: !jadwal.jamMulai,
|
||||||
|
extendedProps: {
|
||||||
|
id: jadwal.id,
|
||||||
|
poli: jadwal.poliName,
|
||||||
|
dokter: jadwal.dokter,
|
||||||
|
tipe: 'Rutin',
|
||||||
|
hari: jadwal.hari.join(', '),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/** Format jam adhoc dari datetime string */
|
||||||
|
const adhocMulaiTime = jadwal.mulai ? jadwal.mulai.split('T')[1]?.slice(0, 5) : '';
|
||||||
|
const adhocSelesaiTime = jadwal.selesai ? jadwal.selesai.split('T')[1]?.slice(0, 5) : '';
|
||||||
|
const adhocJamLabel = adhocMulaiTime && adhocSelesaiTime
|
||||||
|
? ` ${adhocMulaiTime} - ${adhocSelesaiTime}`
|
||||||
|
: '';
|
||||||
|
events.push({
|
||||||
|
name: `${adhocJamLabel}`,
|
||||||
|
start: new Date(jadwal.mulai),
|
||||||
|
end: new Date(jadwal.selesai),
|
||||||
|
color: 'secondary',
|
||||||
|
allDay: false,
|
||||||
|
extendedProps: {
|
||||||
|
id: jadwal.id,
|
||||||
|
poli: jadwal.poliName,
|
||||||
|
dokter: jadwal.dokter,
|
||||||
|
tipe: 'Adhoc',
|
||||||
|
shift: `${formatDatetime(jadwal.mulai)} – ${formatDatetime(jadwal.selesai)}`,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return events;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
watch(jadwalList, () => {
|
||||||
|
console.log(jadwalList.value)
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format ISO datetime ke string Indonesia yang ringkas.
|
||||||
|
* @param {string} isoStr
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const formatDatetime = (isoStr) => {
|
||||||
|
if (!isoStr) return '';
|
||||||
|
const d = new Date(isoStr);
|
||||||
|
return d.toLocaleString('id-ID', {
|
||||||
|
day: '2-digit', month: 'short', year: 'numeric',
|
||||||
|
hour: '2-digit', minute: '2-digit'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Dialog Tambah Jadwal ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Apakah dialog tambah jadwal terbuka */
|
||||||
|
const isAddOpen = ref(false);
|
||||||
|
|
||||||
|
/** Tanggal yang diklik sebagai default untuk form */
|
||||||
|
const clickedDate = ref(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menangani klik pada tanggal di kalender.
|
||||||
|
* @param {Date} date - Tanggal yang diklik (sudah dinormalisasi oleh child)
|
||||||
|
*/
|
||||||
|
const handleDateClick = (date) => {
|
||||||
|
clickedDate.value = date;
|
||||||
|
isAddOpen.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menyimpan jadwal baru ke jadwalList dari dialog.
|
||||||
|
* @param {object|Array} jadwalData - Data jadwal dari dialog
|
||||||
|
*/
|
||||||
|
const handleSaveJadwal = (jadwalData) => {
|
||||||
|
if (Array.isArray(jadwalData)) {
|
||||||
|
jadwalList.value.push(...jadwalData);
|
||||||
|
} else {
|
||||||
|
jadwalList.value.push(jadwalData);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Membuka dialog tambah dari tombol header */
|
||||||
|
const openAddDialog = () => {
|
||||||
|
clickedDate.value = new Date();
|
||||||
|
isAddOpen.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Detail Event ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Event yang sedang diklik */
|
||||||
|
const selectedEvent = ref(null);
|
||||||
|
|
||||||
|
/** Apakah dialog detail terbuka */
|
||||||
|
const isDetailOpen = ref(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menangani klik event dari kalender.
|
||||||
|
* @param {object} eventData - Data event yang sudah dinormalisasi oleh child
|
||||||
|
*/
|
||||||
|
const handleEventClick = (eventData) => {
|
||||||
|
selectedEvent.value = eventData;
|
||||||
|
isDetailOpen.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menghapus jadwal dari jadwalList berdasarkan ID event.
|
||||||
|
* @param {number} id - ID jadwal yang akan dihapus
|
||||||
|
*/
|
||||||
|
const handleDeleteJadwal = (id) => {
|
||||||
|
if (!id) return;
|
||||||
|
jadwalList.value = jadwalList.value.filter(j => j.id !== id);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menangani klik edit dari mode list.
|
||||||
|
* @param {object} item - Item tabel yang akan di-edit
|
||||||
|
*/
|
||||||
|
const handleEdit = (item) => {
|
||||||
|
// Untuk saat ini, tampilkan detail event dari item pertama
|
||||||
|
if (item.raw && item.raw.length > 0) {
|
||||||
|
const first = item.raw[0];
|
||||||
|
selectedEvent.value = {
|
||||||
|
id: first.id,
|
||||||
|
poli: first.poliName,
|
||||||
|
dokter: first.dokter,
|
||||||
|
tipe: first.tipe,
|
||||||
|
hari: first.hari ? first.hari.join(', ') : null,
|
||||||
|
tanggal: new Date().toLocaleDateString('id-ID', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }),
|
||||||
|
};
|
||||||
|
isDetailOpen.value = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div style="background: var(--color-neutral-300); height: 100%;">
|
||||||
|
<PageHeader icon="mdi-calendar-clock" title="Jadwal Dokter" :subtitle="currentDate" :show-add-button="false"
|
||||||
|
theme="primary">
|
||||||
|
<template #actions>
|
||||||
|
<div class="d-flex align-center gap-3">
|
||||||
|
<!-- Mode Toggle -->
|
||||||
|
<v-btn-toggle v-model="viewMode" mandatory density="compact" variant="outlined" color="white"
|
||||||
|
class="mode-toggle" divided>
|
||||||
|
<v-btn value="list" size="small">
|
||||||
|
<v-icon start size="18">mdi-format-list-bulleted</v-icon>
|
||||||
|
List
|
||||||
|
</v-btn>
|
||||||
|
<v-btn value="calendar" size="small">
|
||||||
|
<v-icon start size="18">mdi-calendar-month</v-icon>
|
||||||
|
Calendar
|
||||||
|
</v-btn>
|
||||||
|
</v-btn-toggle>
|
||||||
|
|
||||||
|
<!-- Tombol Tambah Dokter -->
|
||||||
|
<v-btn color="white" elevation="0" class="add-btn-primary ml-5" @click="openAddDialog">
|
||||||
|
<v-icon left size="20">mdi-plus-circle</v-icon>
|
||||||
|
Tambah Jadwal
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
<v-container class="py-6">
|
||||||
|
<!-- ── Mode List (default) ──────────────────────────── -->
|
||||||
|
<JadwalDokterList v-if="viewMode === 'list'" :clinic-list="clinicList" :jadwal-list="jadwalList"
|
||||||
|
@edit="handleEdit" @add="openAddDialog" />
|
||||||
|
|
||||||
|
<!-- ── Mode Calendar ────────────────────────────────── -->
|
||||||
|
<JadwalDokterCalendar v-else v-model:selected-poli="selectedPoli" v-model:selected-doctor="selectedDoctor"
|
||||||
|
:clinic-list="clinicList" :calendar-events="calendarEvents" @date-click="handleDateClick"
|
||||||
|
@event-click="handleEventClick" />
|
||||||
|
</v-container>
|
||||||
|
|
||||||
|
<!-- ══ Dialog Tambah Jadwal (shared) ═══════════════════════ -->
|
||||||
|
<DialogTambahJadwal v-model="isAddOpen" :view-mode="viewMode" :clinic-list="clinicList"
|
||||||
|
:selected-poli="selectedPoli" :selected-doctor="selectedDoctor" :clicked-date="clickedDate"
|
||||||
|
:jadwal-list="jadwalList"
|
||||||
|
@save="handleSaveJadwal" />
|
||||||
|
|
||||||
|
<!-- ══ Dialog Detail Event (shared) ═══════════════════════ -->
|
||||||
|
<DialogDetailEvent v-model="isDetailOpen" :event="selectedEvent" @delete="handleDeleteJadwal" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.mode-toggle {
|
||||||
|
border-radius: 8px !important;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-toggle .v-btn {
|
||||||
|
color: rgba(255, 255, 255, 0.8) !important;
|
||||||
|
text-transform: none !important;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-toggle .v-btn--active {
|
||||||
|
background-color: rgba(255, 255, 255, 0.2) !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn-primary {
|
||||||
|
text-transform: none !important;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -331,11 +331,7 @@ const handleAddMember = () => {
|
|||||||
<div class="text-body-2">{{ profileData.birthDate }}</div>
|
<div class="text-body-2">{{ profileData.birthDate }}</div>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
<v-col cols="3">
|
<v-col cols="2">
|
||||||
<div class="text-caption text-uppercase text-grey-darken-1 font-weight-bold mb-1">
|
|
||||||
Nomor Telepon
|
|
||||||
</div>
|
|
||||||
<div class="text-body-2">{{ profileData.registrationNumber }}</div>
|
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
<v-col cols="7">
|
<v-col cols="7">
|
||||||
|
|||||||
+17
-16
@@ -16,11 +16,11 @@ interface NavItem {
|
|||||||
// Initial default navigation items
|
// Initial default navigation items
|
||||||
const defaultNavItems: NavItem[] = [
|
const defaultNavItems: NavItem[] = [
|
||||||
{ id: 1, name: "Dashboard", icon: "mdi-view-dashboard", path: "/dashboard" },
|
{ id: 1, name: "Dashboard", icon: "mdi-view-dashboard", path: "/dashboard" },
|
||||||
{ id: 2, name: "Verifikasi Akun", icon: "mdi-account-check-outline", path:"/verifikasiAkun/VerifikasiAkun" },
|
{ id: 2, name: "Verifikasi Akun", icon: "mdi-account-check-outline", path: "/verifikasiAkun/VerifikasiAkun" },
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
name: "Check In",
|
name: "Check In",
|
||||||
icon: "mdi-file-document-edit-outline",
|
icon: "mdi-file-document-edit-outline",
|
||||||
path: "/CheckInPasien/checkIn"
|
path: "/CheckInPasien/checkIn"
|
||||||
// badge: "3",
|
// badge: "3",
|
||||||
},
|
},
|
||||||
@@ -38,14 +38,14 @@ const defaultNavItems: NavItem[] = [
|
|||||||
// { id: 10, name: "Anjungan", path: "/Anjungan/Anjungan", icon: "mdi-circle-small" },
|
// { id: 10, name: "Anjungan", path: "/Anjungan/Anjungan", icon: "mdi-circle-small" },
|
||||||
{ id: 11, name: "Anjungan", path: "/anjungan/anjungancopy", icon: "mdi-circle-small" },
|
{ id: 11, name: "Anjungan", path: "/anjungan/anjungancopy", icon: "mdi-circle-small" },
|
||||||
// { id: 11, name: "Klinik", path: "/Anjungan/AntrianKlinik", icon: "mdi-circle-small" },
|
// { id: 11, name: "Klinik", path: "/Anjungan/AntrianKlinik", icon: "mdi-circle-small" },
|
||||||
{ id: 12, name: "Klinik Ruang", path: "/Anjungan/AntrianKlinikRuang", icon: "mdi-circle-small"},
|
{ id: 12, name: "Klinik Ruang", path: "/Anjungan/AntrianKlinikRuang", icon: "mdi-circle-small" },
|
||||||
// { id: 13, name: "Penunjang", path: "/Anjungan/AntrianPenunjang", icon: "mdi-circle-small"},
|
// { id: 13, name: "Penunjang", path: "/Anjungan/AntrianPenunjang", icon: "mdi-circle-small"},
|
||||||
{id: 14, name: "Loket", path: "/Anjungan/AntrianLoket", icon: "mdi-circle-small"},
|
{ id: 14, name: "Loket", path: "/Anjungan/AntrianLoket", icon: "mdi-circle-small" },
|
||||||
{id: 15, name: "Antrean Masuk", path: "/Anjungan/AntreanMasuk", icon: "mdi-circle-small"},
|
{ id: 15, name: "Antrean Masuk", path: "/Anjungan/AntreanMasuk", icon: "mdi-circle-small" },
|
||||||
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 15,
|
id: 15,
|
||||||
name: "Master Data",
|
name: "Master Data",
|
||||||
icon: "mdi-cog-outline",
|
icon: "mdi-cog-outline",
|
||||||
@@ -53,6 +53,7 @@ const defaultNavItems: NavItem[] = [
|
|||||||
children: [
|
children: [
|
||||||
{ id: 16, name: "Hak Akses", path: "/Setting/HakAkses", icon: "mdi-circle-small" },
|
{ id: 16, name: "Hak Akses", path: "/Setting/HakAkses", icon: "mdi-circle-small" },
|
||||||
{ id: 17, name: "User Login", path: "/Setting/UserLogin", icon: "mdi-circle-small" },
|
{ id: 17, name: "User Login", path: "/Setting/UserLogin", icon: "mdi-circle-small" },
|
||||||
|
{ id: 25, name: "Master Jadwal Dokter", path: "/Setting/JadwalDokter", icon: "mdi-circle-small" },
|
||||||
{ id: 18, name: "Master Anjungan", path: "/Setting/MasterAnjungan", icon: "mdi-circle-small" },
|
{ id: 18, name: "Master Anjungan", path: "/Setting/MasterAnjungan", icon: "mdi-circle-small" },
|
||||||
{ id: 19, name: "Master Loket", path: "/Setting/MasterLoket", icon: "mdi-circle-small" },
|
{ id: 19, name: "Master Loket", path: "/Setting/MasterLoket", icon: "mdi-circle-small" },
|
||||||
{ id: 20, name: "Master Klinik", path: "/Setting/MasterKlinik", icon: "mdi-circle-small" },
|
{ id: 20, name: "Master Klinik", path: "/Setting/MasterKlinik", icon: "mdi-circle-small" },
|
||||||
@@ -82,7 +83,7 @@ export const useNavItemsStore = defineStore('navItems', () => {
|
|||||||
// Jika navItems.value null, undefined, atau bukan array yang valid,
|
// Jika navItems.value null, undefined, atau bukan array yang valid,
|
||||||
// kembalikan array default.
|
// kembalikan array default.
|
||||||
if (!Array.isArray(navItems.value) || navItems.value === null || navItems.value === undefined) {
|
if (!Array.isArray(navItems.value) || navItems.value === null || navItems.value === undefined) {
|
||||||
return defaultNavItems;
|
return defaultNavItems;
|
||||||
}
|
}
|
||||||
return navItems.value;
|
return navItems.value;
|
||||||
});
|
});
|
||||||
@@ -106,12 +107,12 @@ export const useNavItemsStore = defineStore('navItems', () => {
|
|||||||
async function refreshNavItems() {
|
async function refreshNavItems() {
|
||||||
const { getAllowedPages } = useHakAkses();
|
const { getAllowedPages } = useHakAkses();
|
||||||
const allowedPages = await getAllowedPages();
|
const allowedPages = await getAllowedPages();
|
||||||
|
|
||||||
if (allowedPages.length === 0) {
|
if (allowedPages.length === 0) {
|
||||||
// If no hak akses defined (maybe new system not setup yet),
|
// If no hak akses defined (maybe new system not setup yet),
|
||||||
// keep default or clear? Let's keep for now for safety during transition
|
// keep default or clear? Let's keep for now for safety during transition
|
||||||
filteredNavItems.value = defaultNavItems;
|
filteredNavItems.value = defaultNavItems;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterItems = (items: NavItem[]): NavItem[] => {
|
const filterItems = (items: NavItem[]): NavItem[] => {
|
||||||
@@ -125,7 +126,7 @@ export const useNavItemsStore = defineStore('navItems', () => {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's a child or leaf, check if path is in allowedPages
|
// If it's a child or leaf, check if path is in allowedPages
|
||||||
return allowedPages.includes(item.path);
|
return allowedPages.includes(item.path);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user