1<template>
2 <div class="w-max mx-auto">
3 <div class="text-base-medium mb-2 text-ink-gray-8 ms-2.5">
4 {{ formattedMonth }}
5 </div>
6 <div class="rounded-5 text-sm">
7 <div class="flex items-center text-xs uppercase">
8 <div
9 class="flex size-7.5 items-center justify-center text-center text-ink-gray-5"
10 v-for="(d, i) in ['s', 'm', 't', 'w', 't', 'f', 's']"
11 :key="i"
12 >
13 {{ d }}
14 </div>
15 </div>
16 <div class="flex items-center" v-for="(week, i) in datesAsWeeks" :key="i">
17 <div v-for="date in week" :key="getFormattedDate(date)">
18 <Popover v-if="isHoliday(date)">
19 <template #trigger="{ close, setOpen }">
20 <!--
21 Hover opens this one. reka turns the whole trigger subtree into
22 the toggle, so the cell stops its click before it gets there.
23 -->
24 <div>
25 <div
26 class="flex size-7 cursor-pointer text-ink-orange-6 bg-surface-yellow-2 items-center justify-center rounded-4 hover:bg-surface-yellow-2 select-none m-[1px]"
27 :class="{
28 '!text-ink-gray-4 !bg-surface-gray-2': isWeekOff(date),
29 }"
30 @mouseover="
31 handleMouseEnter(getFormattedDate(date), () =>
32 setOpen(true)
33 )
34 "
35 @mouseleave="handleMouseLeave(getFormattedDate(date), close)"
36 @click.stop="
37 () => {
38 if (isWeekOff(date)) return;
39 close();
40 editHoliday(date);
41 }
42 "
43 >
44 {{ date.getDate() }}
45 </div>
46 </div>
47 </template>
48 <template
49 #default="{ close: closePopover, setOpen: setPopoverOpen }"
50 >
51 <div
52 class="p-3 flex gap-2.5 text-ink-gray-9 w-80 border border-outline-gray-1 rounded-5"
53 @mouseover="
54 handleMouseEnter(getFormattedDate(date), () =>
55 setPopoverOpen(true)
56 )
57 "
58 @mouseleave="
59 handleMouseLeave(getFormattedDate(date), closePopover)
60 "
61 >
62 <div class="w-[5%]">
63 <div class="size-3.5 bg-surface-orange-5 rounded-1 mt-1" />
64 </div>
65 <div class="grow">
66 <div class="text-sm-semibold">
67 {{ getHolidayDescription(date) }}
68 </div>
69 <div class="text-xs mt-1">
70 {{ date.toLocaleDateString() }}
71 </div>
72 </div>
73 <Popover
74 v-if="!isWeekOff(date)"
75 @close="isConfirmingDelete = false"
76 >
77 <template #trigger="{ close }">
78 <Button
79 icon="lucide-more-horizontal"
80 variant="ghost"
81 @mouseleave="
82 handleMouseLeave(
83 getFormattedDate(date) + 'dropdown',
84 close
85 )
86 "
87 />
88 </template>
89 <template
90 #default="{
91 close: closeDropdown,
92 setOpen: setDropdownOpen,
93 }"
94 >
95 <div
96 class="p-2 flex flex-col gap-1 w-40 text-ink-gray-9 border border-outline-gray-1 rounded-5"
97 @mouseover="
98 handleMouseEnter(getFormattedDate(date), () =>
99 setPopoverOpen(true)
100 );
101 handleMouseEnter(
102 getFormattedDate(date) + 'dropdown',
103 () => setDropdownOpen(true)
104 );
105 "
106 @mouseleave="
107 handleMouseLeave(getFormattedDate(date), closePopover);
108 handleMouseLeave(
109 getFormattedDate(date) + 'dropdown',
110 closeDropdown
111 );
112 "
113 >
114 <Button
115 class="w-full flex !justify-start"
116 icon-left="lucide-edit"
117 variant="ghost"
118 label="Edit"
119 @click="
120 () => {
121 closePopover();
122 closeDropdown();
123 editHoliday(date);
124 }
125 "
126 />
127 <Button
128 class="w-full flex !justify-start"
129 icon-left="lucide-trash-2"
130 variant="ghost"
131 :label="
132 isConfirmingDelete ? 'Confirm Delete' : 'Delete'
133 "
134 :theme="isConfirmingDelete ? 'red' : 'gray'"
135 @click="
136 (e) => {
137 deleteHoliday(e, date, () => {
138 closePopover();
139 closeDropdown();
140 });
141 }
142 "
143 />
144 </div>
145 </template>
146 </Popover>
147 </div>
148 </template>
149 </Popover>
150 <div
151 v-else
152 class="flex size-7 items-center justify-center rounded-4 m-[1px] select-none"
153 :class="{
154 'cursor-pointer hover:bg-surface-gray-2': isDateInRange(date),
155 'text-ink-gray-3':
156 // @ts-ignore
157 date.getMonth() !== currentMonth - 1 || !isDateInRange(date),
158 'bg-surface-gray-10 text-ink-base hover:!bg-surface-gray-10/80 hover:text-ink-base':
159 getFormattedDate(date) === dateValue && isDateInRange(date),
160 'opacity-50 cursor-not-allowed': !isDateInRange(date),
161 }"
162 @click="isDateInRange(date) ? addHoliday(date) : null"
163 >
164 {{ date.getDate() }}
165 </div>
166 </div>
167 </div>
168 </div>
169 </div>
170 <AddHolidayModal v-model="dialog" />
171</template>
172
173<script setup lang="ts">
174import { holidayData } from "@/stores/holidayList";
175import { getFormattedDate, htmlToText } from "@/utils";
176import { dayjs, Popover } from "frappe-ui";
177import { useDatePicker } from "@/composables/useDatePicker";
178import { ref, watch } from "vue";
179import AddHolidayModal from "./Modals/AddHolidayModal.vue";
180
181const dialog = ref({
182 show: false,
183 holiday_date: null,
184 description: "",
185 editing: null,
186});
187
188const {
189 currentYear,
190 currentMonth,
191 today,
192 datesAsWeeks: daw,
193 formattedMonth,
194} = useDatePicker();
195// Workaround to fix type errors
196const datesAsWeeks = daw as unknown as Date[][];
197const isConfirmingDelete = ref(false);
198const popoverTimeouts = ref({});
199
200const props = defineProps({
201 year: {
202 type: Number,
203 required: true,
204 },
205 month: {
206 type: Number,
207 required: true,
208 },
209 holidays: {
210 type: Array<any>,
211 required: true,
212 },
213});
214
215const dateValue = ref(getFormattedDate(today.value));
216
217const handleMouseEnter = (date, callback) => {
218 if (popoverTimeouts.value[date]) {
219 clearTimeout(popoverTimeouts.value[date]);
220 }
221 popoverTimeouts.value[date] = setTimeout(() => {
222 callback();
223 }, 350);
224};
225
226const handleMouseLeave = (date, callback) => {
227 if (popoverTimeouts.value[date]) {
228 clearTimeout(popoverTimeouts.value[date]);
229 }
230 popoverTimeouts.value[date] = setTimeout(() => {
231 callback();
232 }, 350);
233};
234
235const addHoliday = (date) => {
236 dialog.value = {
237 holiday_date: date,
238 description: "",
239 show: true,
240 editing: null,
241 };
242};
243
244const isDateInRange = (date: Date): boolean => {
245 if (!holidayData.value.from_date || !holidayData.value.to_date) return false;
246
247 const checkDate = dayjs(date).startOf("day");
248 const from = dayjs(holidayData.value.from_date).startOf("day");
249 const to = dayjs(holidayData.value.to_date).endOf("day");
250
251 return (
252 (checkDate.isAfter(from) || checkDate.isSame(from, "day")) &&
253 (checkDate.isBefore(to) || checkDate.isSame(to, "day"))
254 );
255};
256
257const isHoliday = (date: Date): boolean => {
258 if (!props.holidays?.length) return false;
259 const inputDate = dayjs(date).startOf("day");
260
261 return props.holidays.some((holiday) => {
262 const holidayDate = dayjs(holiday.holiday_date).startOf("day");
263 return holidayDate.isSame(inputDate);
264 });
265};
266
267const isWeekOff = (date: Date): boolean => {
268 if (!props.holidays?.length) return false;
269
270 const inputDate = dayjs(date).startOf("day");
271 return props.holidays.some((holiday) => {
272 const holidayDate = dayjs(holiday.holiday_date).startOf("day");
273 return holidayDate.isSame(inputDate) && holiday.weekly_off == 1;
274 });
275};
276
277const editHoliday = (date) => {
278 const holiday = props.holidays.find((h) => {
279 const holidayDate = getFormattedDate(h.holiday_date);
280 const editDate = getFormattedDate(date);
281 return holidayDate === editDate;
282 });
283 dialog.value = {
284 show: true,
285 holiday_date: holiday.holiday_date,
286 description: holiday.description,
287 editing: holiday,
288 };
289};
290
291const deleteHoliday = (event, date, callback) => {
292 event.stopPropagation();
293 event.preventDefault();
294 if (!isConfirmingDelete.value) {
295 isConfirmingDelete.value = true;
296 return;
297 }
298 const holidayToDelete = props.holidays.find((h) => {
299 const holidayDate = getFormattedDate(h.holiday_date);
300 const editDate = getFormattedDate(date);
301 return holidayDate === editDate;
302 });
303 const index = props.holidays.findIndex((h) => {
304 const holidayDate = getFormattedDate(h.holiday_date);
305 const editDate = getFormattedDate(holidayToDelete.holiday_date);
306 return holidayDate === editDate;
307 });
308
309 props.holidays.splice(index, 1);
310 dialog.value = {
311 show: false,
312 holiday_date: null,
313 description: "",
314 editing: null,
315 };
316 isConfirmingDelete.value = false;
317 callback();
318};
319
320const getHolidayDescription = (date: Date): string => {
321 const holiday = props.holidays.find((h) => {
322 const holidayDate = getFormattedDate(h.holiday_date);
323 const editDate = getFormattedDate(date);
324 return holidayDate === editDate;
325 });
326
327 const text = holiday?.weekly_off
328 ? `${htmlToText(holiday?.description || "")}: Recurring holiday`
329 : htmlToText(holiday?.description || "");
330
331 return text;
332};
333
334watch(
335 () => [props.year, props.month],
336 ([newYear, newMonth]) => {
337 currentYear.value = newYear;
338 currentMonth.value = newMonth;
339 },
340 { immediate: true }
341);
342</script>
343