IKHSAN
ENID Baca dalam Bahasa Indonesia

opendesk

Public
Branch
main
Latest commit
08d9f3328 October 2026

HolidayView.vue

Open raw
<template>
  <SettingsLayoutBase
    :back-label="holidayData?.holiday_list_name || __('New Business Holiday')"
    :on-back="goBack"
    :dirty="isDirty"
  >
    <template #header-actions>
      <Button
        :label="__('Save')"
        theme="gray"
        variant="solid"
        @click="saveHoliday()"
        :disabled="Boolean(!isDirty && holidayListActiveScreen.data)"
        :loading="
          holidayList?.list.loading ||
          holidayList?.setValue.loading ||
          renameHolidayResource.loading ||
          getHolidayData.loading
        "
      />
    </template>
    <template #content>
      <div v-if="!holidayData.loading" class="h-full">
        <div class="flex items-center gap-2 mt-2">
          <span class="text-sm">
            There are in total <b>{{ holidayData.holidays.length }}</b> holidays
            in this list</span
          >
        </div>
        <hr class="mb-8 mt-2" />
        <div class="grid grid-cols-1 md:grid-cols-2 gap-5">
          <div>
            <FormControl
              type="text"
              size="sm"
              variant="subtle"
              :placeholder="__('Name')"
              :label="__('Name')"
              v-model="holidayData.holiday_list_name"
              required
              maxlength="50"
              @change="validateHoliday('holiday_list_name')"
            />
            <ErrorMessage
              :message="holidayDataErrors.holiday_list_name"
              class="mt-2"
            />
          </div>
          <FormControl
            type="textarea"
            size="sm"
            variant="subtle"
            :placeholder="__('Description')"
            :label="__('Description')"
            v-model="holidayData.description"
            maxlength="140"
          />
        </div>
        <hr class="my-8" />
        <div>
          <div class="flex flex-col gap-1">
            <span class="text-md-semibold text-ink-gray-8">{{
              __("Valid From")
            }}</span>
            <span class="text-p-sm text-ink-gray-6">
              {{ __("Choose the duration of this holiday list.") }}
            </span>
          </div>
          <div class="mt-3.5 flex gap-5 flex-col md:flex-row">
            <div class="w-full space-y-1.5">
              <FormLabel :label="__('From date')" for="from_date" required />
              <DatePicker
                v-model="holidayData.from_date"
                variant="subtle"
                placeholder="11/01/2025"
                class="w-full"
                id="from_date"
                :format="getDateFormat()"
                @update:model-value="updateDuration('from_date')"
              >
                <template #prefix>
                  <LucideCalendar class="size-4" />
                </template>
              </DatePicker>
              <ErrorMessage
                :message="
                  holidayDataErrors.from_date || holidayDataErrors.dateRange
                "
              />
            </div>
            <div class="w-full space-y-1.5">
              <FormLabel :label="__('To date')" for="to_date" required />
              <DatePicker
                v-model="holidayData.to_date"
                variant="subtle"
                placeholder="25/12/2025"
                class="w-full"
                id="to_date"
                :format="getDateFormat()"
                @update:model-value="updateDuration('to_date')"
              >
                <template #prefix>
                  <LucideCalendar class="size-4" />
                </template>
              </DatePicker>
              <ErrorMessage :message="holidayDataErrors.to_date" />
            </div>
          </div>
        </div>
        <hr class="my-8" />
        <div>
          <div class="flex flex-col gap-1">
            <div class="text-md-semibold text-ink-gray-8">
              {{ __("Recurring Holidays") }}
            </div>
            <div class="text-p-sm text-ink-gray-6">
              {{ __("Add recurring holidays such as weekends.") }}
            </div>
          </div>
          <div class="mt-5">
            <RecurringHolidaysList
              :holidayData="holidayData"
              :holidays="holidayData.recurring_holidays"
            />
          </div>
        </div>
        <hr class="my-8" />
        <div>
          <div class="flex justify-between items-center">
            <div class="flex justify-between flex-col gap-1">
              <span class="text-md-semibold text-ink-gray-8">
                {{ __("Holidays") }}
              </span>
              <div class="text-p-sm text-ink-gray-6">
                {{
                  __(
                    "Add holidays here to make sure they’re excluded from SLA calculations."
                  )
                }}
              </div>
            </div>
            <TabButtons
              :options="[
                {
                  value: 'calendar',
                  label: __('Calendar'),
                  icon: 'lucide-calendar',
                },
                {
                  value: 'list',
                  label: __('List'),
                  icon: 'lucide-list',
                },
              ]"
              v-model="holidayListView"
            />
          </div>
          <div class="mt-5">
            <HolidaysTableView v-if="holidayListView === 'list'" />
            <HolidaysCalendarView v-else />
          </div>
          <div class="mt-2.5 flex justify-between items-center">
            <Button
              variant="subtle"
              :label="__('Add Holiday')"
              @click="dialog.show = true"
              icon-left="lucide-plus"
            />
            <!-- Indicators -->
            <div class="flex gap-4" v-if="holidayListView === 'calendar'">
              <div class="gap-1 flex items-center">
                <span class="bg-surface-yellow-2 size-4 rounded-1" />
                <span class="text-sm text-ink-gray-6">{{
                  __("Holidays")
                }}</span>
              </div>
              <div class="gap-1 flex items-center">
                <span class="bg-surface-gray-2 size-4 rounded-1" />
                <span class="text-sm text-ink-gray-6">{{
                  __("Recurring holidays")
                }}</span>
              </div>
            </div>
          </div>
        </div>
      </div>
    </template>
  </SettingsLayoutBase>
  <AddHolidayModal v-model="dialog" />
  <ConfirmDialog
    v-model="showConfirmDialog"
    :title="__('Unsaved changes')"
    :message="
      __('Are you sure you want to go back? Unsaved changes will be lost.')
    "
    :onConfirm="goBack"
    :onCancel="() => (showConfirmDialog = false)"
  />
</template>

<script setup lang="ts">
import {
  holidayData,
  holidayDataErrors,
  holidayListActiveScreen,
  updateWeeklyOffDates,
  validateHoliday,
} from "@/stores/holidayList";
import {
  Button,
  createResource,
  DatePicker,
  FormControl,
  LoadingIndicator,
  TabButtons,
  toast,
} from "frappe-ui";
import { inject, onMounted, onUnmounted, ref, watch } from "vue";
import HolidaysTableView from "./HolidaysTableView.vue";
import RecurringHolidaysList from "./RecurringHolidaysList.vue";

import ConfirmDialog from "@/components/ConfirmDialog.vue";
import { slaActiveScreen } from "@/stores/sla";
import { getDateFormat, htmlToText } from "@/utils";
import { dayjs, FormLabel } from "frappe-ui";
import {
  disableSettingModalOutsideClick,
  setActiveSettingsTab,
} from "../settingsModal";
import HolidaysCalendarView from "./HolidaysCalendarView.vue";
import AddHolidayModal from "./Modals/AddHolidayModal.vue";
import { __ } from "@/translation";
import SettingsLayoutBase from "@/components/layouts/SettingsLayoutBase.vue";
import { HolidayListResourceSymbol } from "@/types";
import { ODServiceHolidayList } from "@/types/doctypes";

const dialog = ref({
  show: false,
  holiday_date: new Date(),
  description: "",
  editing: null,
});

const isDirty = ref(false);
const initialData = ref(null);
const holidayListView = ref("calendar");

const showConfirmDialog = ref(false);

const holidayList = inject(HolidayListResourceSymbol);

const getHolidayData = createResource({
  url: "frappe.client.get",
  params: {
    doctype: "OD Service Holiday List",
    name: holidayListActiveScreen.value.data?.name,
  },
  onSuccess(data: ODServiceHolidayList) {
    holidayData.value = data;
    initialData.value = JSON.stringify(data);
  },
  transform(data: ODServiceHolidayList) {
    for (let holiday of data.holidays) {
      holiday.description = htmlToText(holiday.description);
    }
    data.recurring_holidays = JSON.parse(data.recurring_holidays || "[]");
    return data;
  },
});

const renameHolidayResource = createResource({
  url: "frappe.client.rename_doc",
  makeParams() {
    return {
      doctype: "OD Service Holiday List",
      old_name: holidayData.value.name,
      new_name: holidayData.value.holiday_list_name,
    };
  },
});

if (holidayListActiveScreen.value.data?.name) {
  holidayData.value.loading = true;
  getHolidayData.fetch();
} else {
  initialData.value = JSON.stringify(holidayData.value);
}

const updateDuration = (key) => {
  validateHoliday(key);
  if (
    !holidayDataErrors.value.dateRange ||
    holidayDataErrors.value.dateRange === ""
  ) {
    updateWeeklyOffDates();
  }
};

const goBack = () => {
  if (isDirty.value && !showConfirmDialog.value) {
    showConfirmDialog.value = true;
    return;
  }
  if (holidayListActiveScreen.value.previousScreen) {
    setActiveSettingsTab("SLA Policies");

    slaActiveScreen.value = {
      screen: "view",
      data: { name: holidayListActiveScreen.value.previousScreen.data },
      fetchData: false,
    };
  }
  showConfirmDialog.value = false;
  // Workaround fix for settings modal not closing after going back
  setTimeout(() => {
    holidayListActiveScreen.value = {
      screen: "list",
      data: null,
    };
  }, 250);
};

const saveHoliday = () => {
  const validationErrors = validateHoliday();
  if (Object.values(validationErrors).some((error) => error)) {
    toast.error(
      __("Invalid fields, check if all are filled in and values are correct.")
    );
    return;
  }

  if (holidayListActiveScreen.value.data) {
    updateHoliday();
  } else {
    createHoliday();
  }
};

const createHoliday = () => {
  const holidays = holidayData.value.holidays.map((holiday) => {
    return {
      ...holiday,
      holiday_date: dayjs(holiday.holiday_date).format("YYYY-MM-DD"),
    };
  });
  holidayList?.insert.submit(
    {
      holiday_list_name: holidayData.value.holiday_list_name,
      description: holidayData.value.description,
      from_date: holidayData.value.from_date,
      to_date: holidayData.value.to_date,
      holidays: holidays,
      recurring_holidays: JSON.stringify(holidayData.value.recurring_holidays),
    },
    {
      onSuccess(data) {
        toast.success(__("Holiday list created successfully."));
        holidayListActiveScreen.value.data = data;
        holidayListActiveScreen.value.screen = "view";
        getHolidayData.submit({
          doctype: "OD Service Holiday List",
          name: data.name,
        });
      },
    }
  );
};

const updateHoliday = async () => {
  const holidays = holidayData.value.holidays.map((holiday) => {
    return {
      ...holiday,
      holiday_date: dayjs(holiday.holiday_date).format("YYYY-MM-DD"),
    };
  });

  await holidayList?.setValue.submit(
    {
      name: holidayListActiveScreen.value?.data?.name,
      holiday_list_name: holidayData.value.holiday_list_name,
      description: holidayData.value.description,
      from_date: holidayData.value.from_date,
      to_date: holidayData.value.to_date,
      holidays: holidays,
      recurring_holidays: JSON.stringify(holidayData.value.recurring_holidays),
    },
    {
      onError(err) {
        const message = err?.messages?.[0];
        toast.error(
          message || __("Some error occurred while updating holiday list")
        );
      },
    }
  );

  if (holidayData.value.name !== holidayData.value.holiday_list_name) {
    await renameHolidayResource.submit().catch(async (err) => {
      const error =
        err?.messages?.[0] ||
        __("Some error occurred while renaming holiday list");
      toast.error(error);
      // Reset holiday data to previous state
      await getHolidayData.reload();
    });

    // Update the active screen data to reflect the new name
    holidayListActiveScreen.value.data.name =
      holidayData.value.holiday_list_name;

    getHolidayData.submit({
      doctype: "OD Service Holiday List",
      name: holidayData.value.holiday_list_name,
    });
  } else {
    await getHolidayData.reload();
  }

  toast.success(__("Holiday list updated successfully."));
  holidayList?.reload();
};

watch(
  holidayData,
  (newVal) => {
    if (!initialData.value) return;
    isDirty.value = JSON.stringify(newVal) != initialData.value;
    if (isDirty.value) {
      disableSettingModalOutsideClick.value = true;
    } else {
      disableSettingModalOutsideClick.value = false;
    }
  },
  { deep: true }
);

const beforeUnloadHandler = (event) => {
  if (!isDirty.value) return;
  event.preventDefault();
  event.returnValue = true;
};

onMounted(() => {
  addEventListener("beforeunload", beforeUnloadHandler);
});

onUnmounted(() => {
  removeEventListener("beforeunload", beforeUnloadHandler);
  holidayDataErrors.value = {
    holiday_list_name: "",
    from_date: "",
    to_date: "",
  };
  disableSettingModalOutsideClick.value = false;
});
</script>

<style scoped>
input[type="radio"] {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  border: 2px solid #c5c2c2;
  border-radius: 50%;
  outline: none;
  transition: all 0.2s ease;
  background-color: white;
}

input[type="radio"]:checked {
  background-color: black;
  border: 2px solid #000;
}

input[type="radio"]:checked::after {
  content: "";
  background-color: #fff;
}

input[type="radio"]:focus {
  outline: none !important;
  box-shadow: none !important;
}
</style>
bytes
14,116
lines
486
Type
vue
Latest commit
28 October 2026