260 lines
9.9 KiB
Vue
260 lines
9.9 KiB
Vue
<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>
|