diff --git a/src/components/displays/buttons/ActionSettingsWheelButton.vue b/src/components/displays/buttons/ActionSettingsWheelButton.vue index 663beddd..b015d04f 100644 --- a/src/components/displays/buttons/ActionSettingsWheelButton.vue +++ b/src/components/displays/buttons/ActionSettingsWheelButton.vue @@ -86,6 +86,10 @@ const props = defineProps({ type: Boolean, default: false, }, + allowBookingCompletion: { + type: Boolean, + default: false, + }, department_lane_id: { type: Number, default: null, @@ -1211,20 +1215,46 @@ const openPrimaryGatewayPage = async () => { SessionUser.functions.redirectTo.superUser("/selfserve/edge-agents", true); }; +const showCompleteOrderBookingConfirmation = async () => { + const result = await Swal.fire({ + title: t("bookings.complete_booking"), + text: t("pos.confirm_complete_order"), + icon: "warning", + showDenyButton: true, + showCancelButton: true, + confirmButtonText: t("admin.pos.settings_wheel.view_booking_new_tab"), + denyButtonText: t("tables.bookings.complete_wash_without_certificate"), + cancelButtonText: t("global.cancel"), + reverseButtons: true, + }); + + if (result.isConfirmed) { + SessionUser.functions.redirectTo.department( + props.department_id, + "modules/bookings/order/" + props.order_booking_id, + true + ); + return; + } + + if (result.isDenied) { + await SessionUser.objects.order_bookings.functions.complete(props.order_booking_id, null, () => { + props.refreshFunction(); + }); + } +}; + const flatBuiltInMenuSections = computed(() => { const sections = []; if (props.order_booking_id) { const bookingSection = buildMenuSection("booking", t("admin.pos.settings_wheel.booking"), [ - (SessionUser.canAccessAdmin() || SessionUser.canAccessSuperUser()) && !props.order_id + props.allowBookingCompletion ? buildMenuAction("booking-complete", { - icon: "fas fa-check-circle", + icon: "fas fa-check", label: t("admin.pos.settings_wheel.mark_as_completed"), template: "success", - clickAction: () => - SessionUser.objects.order_bookings.functions.showCompleteConfirmationModal(props.order_booking_id, () => { - props.refreshFunction(); - }), + clickAction: showCompleteOrderBookingConfirmation, }) : null, buildMenuAction("booking-view", { diff --git a/src/components/displays/buttons/DatePeriodSelector.vue b/src/components/displays/buttons/DatePeriodSelector.vue index 156ac78c..8c80534f 100644 --- a/src/components/displays/buttons/DatePeriodSelector.vue +++ b/src/components/displays/buttons/DatePeriodSelector.vue @@ -119,9 +119,25 @@ const formatDateInputValue = (date: Date) => { return ""; } - return date.toISOString().split("T")[0]; + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; }; +const parseDateInputValue = (value: string) => { + const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value); + if (!match) { + return new Date(value); + } + + return new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3])); +}; + +const isSameDateInputValue = (left: Date, right: Date) => ( + formatDateInputValue(left) === formatDateInputValue(right) +); + const updateSelection = () => { emits("update:selection", { startDate: startDate.value, endDate: endDate.value }); props.onSelectionChange(startDate.value, endDate.value); @@ -177,25 +193,38 @@ const isSelectionValid = computed(() => ( )); const handleStartDateChange = (event) => { - const newDate = new Date(event.target.value); + const newDate = parseDateInputValue(event.target.value); if (Number.isNaN(newDate.getTime())) { console.warn("Invalid start date:", event.target.value); return; } + if (isSameDateInputValue(newDate, startDate.value)) { + return; + } + applyDateRange(newDate, endDate.value); }; const handleEndDateChange = (event) => { - const newDate = new Date(event.target.value); + const newDate = parseDateInputValue(event.target.value); if (Number.isNaN(newDate.getTime())) { console.warn("Invalid end date:", event.target.value); return; } + if (isSameDateInputValue(newDate, endDate.value)) { + return; + } + applyDateRange(startDate.value, newDate); }; +const getFullMonthRange = (year: number, month: number): DateRange => ({ + startDate: new Date(year, month - 1, 1, 0, 0, 0, 0), + endDate: new Date(year, month, 0, 23, 59, 59, 999), +}); + const setMonth = (month: number) => { if (month < 1 || month > 12) { console.warn("Invalid month selection:", month); @@ -203,9 +232,8 @@ const setMonth = (month: number) => { } const year = startDate.value.getFullYear(); - const startOfMonth = new Date(year, month - 1, 1); - const endOfMonth = new Date(year, month, 0); - applyDateRange(startOfMonth, endOfMonth); + const range = getFullMonthRange(year, month); + applyDateRange(range.startDate, range.endDate); }; const handleMonthChange = (event) => { @@ -217,6 +245,10 @@ const handleMonthChange = (event) => { setMonth(month); }; +const setCurrentSelectionEntireMonth = () => { + setMonth(currentMonthSelection.value); +}; + const handleYearChange = (event) => { const year = parseInt(event.target.value, 10); if (Number.isNaN(year)) { @@ -307,24 +339,14 @@ const shortcuts = computed(() => { label: SessionUser.objects.global.language.text.this_month, getRange: () => { const today = new Date(); - const firstDayOfThisMonth = new Date(today.getFullYear(), today.getMonth(), 1); - const lastDayOfThisMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0); - return { - startDate: new Date(firstDayOfThisMonth.setHours(23, 59, 59, 999)), - endDate: new Date(lastDayOfThisMonth.setHours(23, 59, 59, 999)), - }; + return getFullMonthRange(today.getFullYear(), today.getMonth() + 1); }, }, { label: SessionUser.objects.global.language.text.last_month, getRange: () => { const today = new Date(); - const firstDayOfLastMonth = new Date(today.getFullYear(), today.getMonth() - 1, 1); - const lastDayOfLastMonth = new Date(today.getFullYear(), today.getMonth(), 0); - return { - startDate: new Date(firstDayOfLastMonth.setHours(23, 59, 59, 999)), - endDate: new Date(lastDayOfLastMonth.setHours(23, 59, 59, 999)), - }; + return getFullMonthRange(today.getFullYear(), today.getMonth()); }, }, { @@ -346,16 +368,7 @@ const shortcuts = computed(() => { label: SessionUser.objects.global.language.text.same_month_last_year, getRange: () => { const today = new Date(); - const firstDayOfThisMonth = new Date(today.getFullYear(), today.getMonth(), 1); - const lastDayOfThisMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0); - const firstDayOfLastYearSameMonth = new Date(firstDayOfThisMonth); - firstDayOfLastYearSameMonth.setFullYear(firstDayOfThisMonth.getFullYear() - 1); - const lastDayOfLastYearSameMonth = new Date(lastDayOfThisMonth); - lastDayOfLastYearSameMonth.setFullYear(lastDayOfThisMonth.getFullYear() - 1); - return { - startDate: new Date(firstDayOfLastYearSameMonth.setHours(23, 59, 59, 999)), - endDate: new Date(lastDayOfLastYearSameMonth.setHours(23, 59, 59, 999)), - }; + return getFullMonthRange(today.getFullYear() - 1, today.getMonth() + 1); }, }, ]; @@ -419,6 +432,7 @@ const handleShortcutSelection = (event) => { class="input" data-testid="date-period-start" :value="formatDateInputValue(startDate)" + @input="handleStartDateChange" @change="handleStartDateChange" :disabled="props.isDisabled || props.isReadonly" :class="{ 'is-danger': !isSelectionValid }" @@ -434,6 +448,7 @@ const handleShortcutSelection = (event) => { class="input" data-testid="date-period-end" :value="formatDateInputValue(endDate)" + @input="handleEndDateChange" @change="handleEndDateChange" :disabled="props.isDisabled || props.isReadonly" :class="{ 'is-danger': !isSelectionValid }" @@ -516,6 +531,7 @@ const handleShortcutSelection = (event) => { class="input" data-testid="date-period-start" :value="formatDateInputValue(startDate)" + @input="handleStartDateChange" @change="handleStartDateChange" :disabled="props.isDisabled || props.isReadonly" :class="{ 'is-danger': !isSelectionValid }" @@ -530,6 +546,7 @@ const handleShortcutSelection = (event) => { class="input" data-testid="date-period-end" :value="formatDateInputValue(endDate)" + @input="handleEndDateChange" @change="handleEndDateChange" :disabled="props.isDisabled || props.isReadonly" :class="{ 'is-danger': !isSelectionValid }" @@ -648,7 +665,7 @@ const handleShortcutSelection = (event) => { iconPack="fas" has-icon > - Selection is not an entire month Click to set entire month. + Selection is not an entire month Click to set entire month. When a partial month selection is made, it may not accurately represent the intended time period. Consider selecting an entire month for more precise results. diff --git a/src/components/displays/department/pos/steps/elements/PosDesktopOrderBookingSelectorModal.vue b/src/components/displays/department/pos/steps/elements/PosDesktopOrderBookingSelectorModal.vue index ade007b7..1f00b75d 100644 --- a/src/components/displays/department/pos/steps/elements/PosDesktopOrderBookingSelectorModal.vue +++ b/src/components/displays/department/pos/steps/elements/PosDesktopOrderBookingSelectorModal.vue @@ -413,6 +413,7 @@ const closeModal = () => { :refreshFunction="emitRefreshBookings" :icon="'fas fa-ellipsis-v'" trigger-button-variant="text" + allow-booking-completion > diff --git a/src/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobilePopupSelectOrderBooking.vue b/src/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobilePopupSelectOrderBooking.vue index 0d655f9a..3dd6f93c 100644 --- a/src/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobilePopupSelectOrderBooking.vue +++ b/src/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobilePopupSelectOrderBooking.vue @@ -355,6 +355,7 @@ const isBookingScheduledForToday = (booking: any) => { :refreshFunction="emitRefreshBookings" :icon="'fas fa-ellipsis-v'" trigger-button-variant="text" + allow-booking-completion > diff --git a/src/components/models/navigation/items/NavigationMenuItemsAdmin.vue b/src/components/models/navigation/items/NavigationMenuItemsAdmin.vue index 185e2577..b899852b 100644 --- a/src/components/models/navigation/items/NavigationMenuItemsAdmin.vue +++ b/src/components/models/navigation/items/NavigationMenuItemsAdmin.vue @@ -56,6 +56,7 @@ const department_booking_counts_loading = ref(false); const department_draft_count = ref(0); const department_draft_count_loading = ref(false); const draftTransactionCustomerNumber = computed(() => getDraftTransactionCustomerNumber()); +const canUseAdminNavigationCountsValue = computed(() => canUseAdminNavigationCounts()); let bookingCountRequestId = 0; let draftCountRequestId = 0; let bookingCountFetchInFlight = false; @@ -328,7 +329,7 @@ const isDepartmentSet = computed(() => { return department_id.value !== 'default' && parseInt(department_id.value) > 0; }); -watch(department_id, () => { +watch([department_id, canUseAdminNavigationCountsValue], () => { refreshNavigationCounts({ showBookingLoadingIndicator: true, showDraftLoadingIndicator: true, diff --git a/src/components/session/token/SessionUser.vue b/src/components/session/token/SessionUser.vue index 224b1ff8..53fd79b5 100644 --- a/src/components/session/token/SessionUser.vue +++ b/src/components/session/token/SessionUser.vue @@ -749,8 +749,10 @@ export const SessionUser = { // If // If the current path is not an admin path, redirect to the department page const newPath = currentPath.replace(/\/admin\/\d+/, '').replace(/^\/admin\/?/, '').replace(/^\//, ''); + const shouldPreserveCurrentQuery = newPath === 'modules/pos' || newPath.startsWith('modules/pos/'); + const queryString = shouldPreserveCurrentQuery ? window.location.search : ''; // Redirect to the department page - SessionUser.functions.redirectTo.department(departmentId, newPath); + SessionUser.functions.redirectTo.department(departmentId, newPath + queryString); }, /** Show confirm logout dialog */ showConfirmLogoutDialog: () => { diff --git a/src/components/session/token/SessionUser/Objects/CollectedOrderInvoices.vue b/src/components/session/token/SessionUser/Objects/CollectedOrderInvoices.vue index 85df9215..fa5116c4 100644 --- a/src/components/session/token/SessionUser/Objects/CollectedOrderInvoices.vue +++ b/src/components/session/token/SessionUser/Objects/CollectedOrderInvoices.vue @@ -440,6 +440,19 @@ export const CollectedOrderInvoices = { throw error; }); }, + split_by_month: async (dateFrom, dateTo, options = {}) => { + return authenticatedRequest('/collected-invoices/split-by-month', 'POST', { + dateFrom, + dateTo, + ...(options.preview !== undefined ? { preview: !!options.preview } : {}), + }).then((response) => { + console.log(response); + return response; + }).catch((error) => { + console.log(error); + throw error; + }); + }, showCreateNewCustom: showCreateCustomInvoiceCollectionForm, showInvoiceCollectionPickerForm: showInvoiceCollectionPickerModal, add_vehicle_subscriptions: async (id) => { diff --git a/src/components/session/token/SessionUser/Objects/OrderBookings.vue b/src/components/session/token/SessionUser/Objects/OrderBookings.vue index daaa9e01..35b0a381 100644 --- a/src/components/session/token/SessionUser/Objects/OrderBookings.vue +++ b/src/components/session/token/SessionUser/Objects/OrderBookings.vue @@ -1,5 +1,4 @@