fix : calender view jadwal dokter
This commit is contained in:
@@ -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>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="3">
|
||||
<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 cols="2">
|
||||
</v-col>
|
||||
|
||||
<v-col cols="7">
|
||||
|
||||
+17
-16
@@ -16,11 +16,11 @@ interface NavItem {
|
||||
// Initial default navigation items
|
||||
const defaultNavItems: NavItem[] = [
|
||||
{ id: 1, name: "Dashboard", icon: "mdi-view-dashboard", path: "/dashboard" },
|
||||
{ id: 2, name: "Verifikasi Akun", icon: "mdi-account-check-outline", path:"/verifikasiAkun/VerifikasiAkun" },
|
||||
{
|
||||
id: 3,
|
||||
name: "Check In",
|
||||
icon: "mdi-file-document-edit-outline",
|
||||
{ id: 2, name: "Verifikasi Akun", icon: "mdi-account-check-outline", path: "/verifikasiAkun/VerifikasiAkun" },
|
||||
{
|
||||
id: 3,
|
||||
name: "Check In",
|
||||
icon: "mdi-file-document-edit-outline",
|
||||
path: "/CheckInPasien/checkIn"
|
||||
// badge: "3",
|
||||
},
|
||||
@@ -38,14 +38,14 @@ const defaultNavItems: NavItem[] = [
|
||||
// { 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: "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: 14, name: "Loket", path: "/Anjungan/AntrianLoket", icon: "mdi-circle-small"},
|
||||
{id: 15, name: "Antrean Masuk", path: "/Anjungan/AntreanMasuk", 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: "Master Data",
|
||||
icon: "mdi-cog-outline",
|
||||
@@ -53,6 +53,7 @@ const defaultNavItems: NavItem[] = [
|
||||
children: [
|
||||
{ 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: 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: 19, name: "Master Loket", path: "/Setting/MasterLoket", 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,
|
||||
// kembalikan array default.
|
||||
if (!Array.isArray(navItems.value) || navItems.value === null || navItems.value === undefined) {
|
||||
return defaultNavItems;
|
||||
return defaultNavItems;
|
||||
}
|
||||
return navItems.value;
|
||||
});
|
||||
@@ -106,12 +107,12 @@ export const useNavItemsStore = defineStore('navItems', () => {
|
||||
async function refreshNavItems() {
|
||||
const { getAllowedPages } = useHakAkses();
|
||||
const allowedPages = await getAllowedPages();
|
||||
|
||||
|
||||
if (allowedPages.length === 0) {
|
||||
// If no hak akses defined (maybe new system not setup yet),
|
||||
// keep default or clear? Let's keep for now for safety during transition
|
||||
filteredNavItems.value = defaultNavItems;
|
||||
return;
|
||||
// If no hak akses defined (maybe new system not setup yet),
|
||||
// keep default or clear? Let's keep for now for safety during transition
|
||||
filteredNavItems.value = defaultNavItems;
|
||||
return;
|
||||
}
|
||||
|
||||
const filterItems = (items: NavItem[]): NavItem[] => {
|
||||
@@ -125,7 +126,7 @@ export const useNavItemsStore = defineStore('navItems', () => {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// If it's a child or leaf, check if path is in allowedPages
|
||||
return allowedPages.includes(item.path);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user