1<template>
2 <div class="rounded-5 border p-1 border-outline-gray-2 text-sm">
3 <div
4 class="grid p-2 items-center"
5 :style="{
6 gridTemplateColumns: '1fr 4fr 22px',
7 }"
8 v-if="holidays?.length !== 0"
9 >
10 <div
11 v-for="column in columns"
12 :key="column.key"
13 class="text-ink-gray-5 overflow-hidden whitespace-nowrap text-ellipsis"
14 >
15 {{ column.label }}
16 </div>
17 </div>
18 <hr class="my-0.5" v-if="holidays?.length !== 0" />
19 <div v-for="(holiday, index) in holidays" :key="holiday.day">
20 <div
21 class="grid gap-2 px-2 py-2 items-center"
22 :style="{ gridTemplateColumns: '1fr 4fr 22px' }"
23 >
24 <div
25 v-for="column in columns"
26 :key="column.key"
27 class="w-full py-2 overflow-hidden whitespace-nowrap text-ellipsis"
28 >
29 <div v-if="column.key === 'repetition'">
30 {{ getRepetitionText(holiday[column.key]) }}
31 </div>
32 <div v-else>{{ holiday[column.key] }}</div>
33 </div>
34 <div class="flex justify-end">
35 <Dropdown
36 align="end"
37 :options="dropdownOptions(holiday)"
38 @update:open="(open) => open && (isConfirmingDelete = false)"
39 >
40 <Button icon="lucide-more-horizontal" variant="ghost" />
41 </Dropdown>
42 </div>
43 </div>
44 <hr class="my-0.5" v-if="index !== holidays.length - 1" />
45 </div>
46 <div v-if="holidays?.length === 0" class="text-center p-4 text-ink-gray-5">
47 No items in the list
48 </div>
49 </div>
50 <Button
51 variant="subtle"
52 @click="addHoliday"
53 class="mt-2.5"
54 label="Add Recurring Holiday"
55 icon-left="lucide-plus"
56 />
57 <Dialog
58 v-model:open="dialog"
59 size="md"
60 :title="
61 recurringHolidayData.isEditing
62 ? 'Edit Recurring Holiday'
63 : 'Add Recurring Holiday'
64 "
65 >
66 <template #default>
67 <div v-if="!props.holidayData.from_date || !props.holidayData.to_date">
68 <div class="text-center p-4 text-ink-gray-5">
69 Please select start and end date first
70 </div>
71 </div>
72 <div v-else class="flex flex-col gap-4">
73 <div class="flex flex-col gap-1.5">
74 <FormLabel label="Day" required />
75 <Select
76 :options="availableWorkDays"
77 v-model="recurringHolidayData.day"
78 :disabled="availableWorkDays.length === 0"
79 />
80 </div>
81 <div class="flex flex-col gap-1.5">
82 <FormLabel label="Repetition" required />
83 <div class="grid grid-cols-2 gap-2 mt-2">
84 <Checkbox
85 v-model="recurringHolidayData.repetition.all"
86 label="Every week"
87 :disabled="
88 recurringHolidayData.repetition.first ||
89 recurringHolidayData.repetition.second ||
90 recurringHolidayData.repetition.third ||
91 recurringHolidayData.repetition.fourth
92 "
93 />
94 <Checkbox
95 v-model="recurringHolidayData.repetition.first"
96 label="Every first week"
97 :disabled="recurringHolidayData.repetition.all"
98 />
99 <Checkbox
100 v-model="recurringHolidayData.repetition.second"
101 label="Every second week"
102 :disabled="recurringHolidayData.repetition.all"
103 />
104 <Checkbox
105 v-model="recurringHolidayData.repetition.third"
106 label="Every third week"
107 :disabled="recurringHolidayData.repetition.all"
108 />
109 <Checkbox
110 v-model="recurringHolidayData.repetition.fourth"
111 label="Every fourth week"
112 :disabled="recurringHolidayData.repetition.all"
113 />
114 <Checkbox
115 v-model="recurringHolidayData.repetition.fifth"
116 label="Every fifth week"
117 :disabled="recurringHolidayData.repetition.all"
118 />
119 </div>
120 </div>
121 </div>
122 </template>
123 <template #actions>
124 <Button
125 variant="solid"
126 @click="saveHoliday"
127 class="w-full"
128 v-if="props.holidayData.from_date && props.holidayData.to_date"
129 :label="
130 recurringHolidayData.isEditing ? 'Update Holiday' : 'Add Holiday'
131 "
132 :icon-left="
133 recurringHolidayData.isEditing ? 'lucide-edit-2' : 'lucide-plus'
134 "
135 />
136 </template>
137 </Dialog>
138</template>
139
140<script setup lang="ts">
141import { updateWeeklyOffDates } from "@/stores/holidayList";
142import { ConfirmDelete } from "@/utils";
143import isSameOrBefore from "dayjs/plugin/isSameOrBefore";
144import weekday from "dayjs/plugin/weekday";
145import { Checkbox, dayjs, Dropdown, FormLabel, Select, toast } from "frappe-ui";
146import { computed, ref } from "vue";
147import { getRepetitionText } from "./utils";
148
149dayjs.extend(weekday);
150dayjs.extend(isSameOrBefore);
151
152const dialog = ref(false);
153const recurringHolidayData = ref({
154 day: null,
155 repetition: {
156 all: false,
157 first: false,
158 second: false,
159 third: false,
160 fourth: false,
161 fifth: false,
162 },
163 isEditing: false,
164 editIndex: -1,
165});
166
167const props = defineProps({
168 holidays: {
169 type: Array<any>,
170 default: () => [],
171 required: true,
172 },
173 holidayData: {
174 type: Object,
175 default: () => {},
176 required: true,
177 },
178});
179
180const columns = [
181 {
182 label: "Day",
183 key: "day",
184 },
185 {
186 label: "Repetition",
187 key: "repetition",
188 },
189];
190
191const workDays = ref([
192 {
193 label: "Monday",
194 value: "Monday",
195 },
196 {
197 label: "Tuesday",
198 value: "Tuesday",
199 },
200 {
201 label: "Wednesday",
202 value: "Wednesday",
203 },
204 {
205 label: "Thursday",
206 value: "Thursday",
207 },
208 {
209 label: "Friday",
210 value: "Friday",
211 },
212 {
213 label: "Saturday",
214 value: "Saturday",
215 },
216 {
217 label: "Sunday",
218 value: "Sunday",
219 },
220]);
221
222const dropdownOptions = (holiday: any) => [
223 {
224 label: "Edit",
225 onClick: () => editHoliday(holiday),
226 icon: "lucide-edit",
227 },
228 ...ConfirmDelete({
229 onConfirmDelete: () => deleteHoliday(holiday),
230 isConfirmingDelete,
231 }),
232];
233
234const availableWorkDays = computed(() => {
235 const usedDays = new Set(
236 props.holidays
237 .filter(
238 (_, index) =>
239 !recurringHolidayData.value.isEditing ||
240 index !== recurringHolidayData.value.editIndex
241 )
242 .map((h) => h.day)
243 );
244
245 if (recurringHolidayData.value.isEditing && recurringHolidayData.value.day) {
246 usedDays.delete(recurringHolidayData.value.day);
247 }
248
249 return workDays.value.filter((day) => !usedDays.has(day.value));
250});
251
252const isConfirmingDelete = ref(false);
253
254const addHoliday = () => {
255 recurringHolidayData.value = {
256 day: null,
257 repetition: {
258 all: false,
259 first: false,
260 second: false,
261 third: false,
262 fourth: false,
263 fifth: false,
264 },
265 isEditing: false,
266 editIndex: -1,
267 };
268 dialog.value = true;
269};
270
271const editHoliday = (holiday: any) => {
272 const index = props.holidays.findIndex((h) => h.day === holiday.day);
273 recurringHolidayData.value = {
274 ...JSON.parse(JSON.stringify(holiday)),
275 isEditing: true,
276 editIndex: index,
277 };
278 dialog.value = true;
279};
280
281const saveHoliday = () => {
282 if (!recurringHolidayData.value.day) {
283 toast.error("Please select a day of the week");
284 return;
285 }
286
287 const { all, first, second, third, fourth, fifth } =
288 recurringHolidayData.value.repetition;
289 if (!all && !first && !second && !third && !fourth && !fifth) {
290 toast.error("Please select at least one repetition option");
291 return;
292 }
293
294 const holidayData = { ...recurringHolidayData.value };
295
296 if (holidayData.isEditing) {
297 if (holidayData.editIndex !== undefined && holidayData.editIndex >= 0) {
298 props.holidays[holidayData.editIndex] = { ...holidayData };
299 updateWeeklyOffDates();
300 } else {
301 toast.error("Error: Unable to find the holiday to update");
302 return;
303 }
304 } else {
305 const isDuplicate = props.holidays.some(
306 (holiday) =>
307 holiday.day === holidayData.day &&
308 JSON.stringify(holiday.repetition) ===
309 JSON.stringify(holidayData.repetition)
310 );
311
312 if (isDuplicate) {
313 toast.error("Holiday with the same day and repetition already exists");
314 return;
315 }
316
317 props.holidays.push({
318 ...holidayData,
319 isEditing: false,
320 });
321 updateWeeklyOffDates();
322 }
323 dialog.value = false;
324};
325
326const deleteHoliday = (holiday: any) => {
327 if (!isConfirmingDelete.value) {
328 isConfirmingDelete.value = true;
329 return;
330 }
331 const index = props.holidays.findIndex((h: any) => {
332 return h.day === holiday.day;
333 });
334 props.holidays.splice(index, 1);
335 updateWeeklyOffDates();
336 dialog.value = false;
337 isConfirmingDelete.value = false;
338};
339</script>
340