diff --git a/src/components/displays/buttons/ActionSettingsWheelButton.vue b/src/components/displays/buttons/ActionSettingsWheelButton.vue index 02b93c3c..0cfca432 100644 --- a/src/components/displays/buttons/ActionSettingsWheelButton.vue +++ b/src/components/displays/buttons/ActionSettingsWheelButton.vue @@ -63,6 +63,10 @@ const props = defineProps({ type: String, default: null, }, + reg_3: { + type: String, + default: null, + }, order_booking_id: { type: Number, default: null, @@ -917,6 +921,15 @@ const getAttachmentLabel = (attachment) => { ); }; +const isWashCertificateAttachment = (attachment) => { + const marker = String(getAttachmentOtherPayload(attachment) || "").trim().toUpperCase(); + if (marker === "WASH_CERTIFICATE") { + return true; + } + + return /(?:^|[/\\])wash[_-]?certificate.*\.pdf$/i.test(getAttachmentLabel(attachment)); +}; + const getAttachmentExtension = (attachment) => { const match = String(getAttachmentLabel(attachment)) .toLowerCase() @@ -1045,6 +1058,10 @@ const activeAttachmentPreviewSource = computed(() => { return previewSourcesById.value[activeAttachment.value.id] ?? null; }); +const hasWashCertificateAttachment = computed(() => + attachmentsFromOrder.value.some((attachment) => isWashCertificateAttachment(attachment)) +); + const hasCachedPreviewSource = (attachmentId) => { return Object.prototype.hasOwnProperty.call(previewSourcesById.value, attachmentId); }; @@ -1609,6 +1626,48 @@ const showCompleteOrderBookingConfirmation = async () => { } }; +const showEmailNotificationActionResult = async (requestAction, successKey, errorKey) => { + try { + await requestAction(); + await Swal.fire({ + title: t(successKey), + icon: "success", + showConfirmButton: false, + timer: 2000, + heightAuto: false, + }); + } catch (error) { + console.error(error); + await Swal.fire({ + title: t("common.error"), + text: [t(errorKey), SessionUser.functions.parseErrorMessage?.(error)].filter(Boolean).join(": "), + icon: "error", + heightAuto: false, + }); + } +}; + +const resendBookingConfirmation = () => + showEmailNotificationActionResult( + () => SessionUser.objects.order_bookings.functions.resendBookingConfirmation(props.order_booking_id), + "admin.pos.settings_wheel.resend_booking_confirmation_success", + "admin.pos.settings_wheel.resend_booking_confirmation_error" + ); + +const resendBookingCompletionConfirmation = () => + showEmailNotificationActionResult( + () => SessionUser.objects.order_bookings.functions.resendBookingCompletionConfirmation(props.order_booking_id), + "admin.pos.settings_wheel.resend_booking_completion_confirmation_success", + "admin.pos.settings_wheel.resend_booking_completion_confirmation_error" + ); + +const resendWashCertificate = () => + showEmailNotificationActionResult( + () => SessionUser.objects.orders.functions.resendWashCertificate(props.order_id), + "admin.pos.settings_wheel.resend_wash_certificate_success", + "admin.pos.settings_wheel.resend_wash_certificate_error" + ); + const getAvailableInvoiceCollectionsForCustomer = async (customerNumber) => { const normalizedCustomerNumber = normalizePositiveInteger(customerNumber); if (!normalizedCustomerNumber) { @@ -1835,6 +1894,40 @@ const flatBuiltInMenuSections = computed(() => { } } + if (props.order_booking_id || hasWashCertificateAttachment.value) { + const emailNotificationsSection = buildMenuSection( + "email-notifications", + t("admin.pos.settings_wheel.email_notifications_section"), + [ + props.order_booking_id + ? buildMenuAction("email-notifications-resend-booking-confirmation", { + icon: "fas fa-envelope", + label: t("admin.pos.settings_wheel.resend_booking_confirmation"), + clickAction: resendBookingConfirmation, + }) + : null, + props.order_booking_id && hasWashCertificateAttachment.value + ? buildMenuAction("email-notifications-resend-booking-completion-confirmation", { + icon: "fas fa-envelope-open-text", + label: t("admin.pos.settings_wheel.resend_booking_completion_confirmation"), + clickAction: resendBookingCompletionConfirmation, + }) + : null, + !props.order_booking_id && hasWashCertificateAttachment.value + ? buildMenuAction("email-notifications-resend-wash-certificate", { + icon: "fas fa-file-pdf", + label: t("admin.pos.settings_wheel.resend_wash_certificate"), + clickAction: resendWashCertificate, + }) + : null, + ] + ); + + if (emailNotificationsSection) { + sections.push(emailNotificationsSection); + } + } + if (props.invoice_collection_id && SessionUser.canAccessSuperUser()) { const invoiceCollectionLinkSection = buildMenuSection( "invoice-collection-link", @@ -2136,10 +2229,10 @@ const flatBuiltInMenuSections = computed(() => { } } - if (props.reg_1 || props.reg_2) { + if (props.reg_1 || props.reg_2 || props.reg_3) { const vehicleSection = buildMenuSection( "vehicle", - props.reg_1 && props.reg_2 + [props.reg_1, props.reg_2, props.reg_3].filter(Boolean).length > 1 ? SessionUser.objects.vehicles.meta.labels.multiple : SessionUser.objects.vehicles.meta.labels.single, [ @@ -2157,6 +2250,13 @@ const flatBuiltInMenuSections = computed(() => { clickAction: () => SessionUser.functions.redirectTo.superUser("/vehicles/" + props.reg_2, true), }) : null, + props.reg_3 && SessionUser.canAccessSuperUser() + ? buildMenuAction("vehicle-reg-3", { + icon: "fas fa-car", + label: t("admin.pos.settings_wheel.view_vehicle_new_tab", { reg: props.reg_3 }), + clickAction: () => SessionUser.functions.redirectTo.superUser("/vehicles/" + props.reg_3, true), + }) + : null, ] ); diff --git a/src/components/displays/department/pos/orders/ordersTable.vue b/src/components/displays/department/pos/orders/ordersTable.vue index 2cf3c2b6..1c828a4f 100644 --- a/src/components/displays/department/pos/orders/ordersTable.vue +++ b/src/components/displays/department/pos/orders/ordersTable.vue @@ -1263,6 +1263,9 @@ const formatCashierName = (order) => { v-bind:customer_number="order.customer_id" v-bind:department_id="order.department_id" v-bind:reg_1="order.reg_1" + v-bind:reg_2="order.reg_2" + v-bind:reg_3="order.reg_3" + v-bind:order_booking_id="order.booking_id" :refreshFunction="loadList" @deleted="loadList()" @flag-created="emitFlagCreated" @@ -1506,6 +1509,9 @@ const formatCashierName = (order) => { v-bind:customer_number="order.customer_id" v-bind:department_id="order.department_id" v-bind:reg_1="order.reg_1" + v-bind:reg_2="order.reg_2" + v-bind:reg_3="order.reg_3" + v-bind:order_booking_id="order.booking_id" :refreshFunction="loadList" @deleted="loadList()" @flag-created="emitFlagCreated" diff --git a/src/components/session/token/SessionUser/Objects/OrderBookings.vue b/src/components/session/token/SessionUser/Objects/OrderBookings.vue index 57ffd5fd..25b5ad8e 100644 --- a/src/components/session/token/SessionUser/Objects/OrderBookings.vue +++ b/src/components/session/token/SessionUser/Objects/OrderBookings.vue @@ -356,6 +356,20 @@ export const OrderBookings = { { safety_seal: normalizeCompletionSafetySeal(safetySeal) }, onAfterComplete ); + }, + resendBookingConfirmation: async (id) => { + return SessionUser.request( + OrderBookings.meta.endpoint + "/booking-confirmation/resend", + "POST", + { id: parseInt(id, 10) } + ).then((response) => response.data.data); + }, + resendBookingCompletionConfirmation: async (id) => { + return SessionUser.request( + OrderBookings.meta.endpoint + "/completion-confirmation/resend", + "POST", + { id: parseInt(id, 10) } + ).then((response) => response.data.data); } }, /** diff --git a/src/components/session/token/SessionUser/Objects/Orders.vue b/src/components/session/token/SessionUser/Objects/Orders.vue index d7b3ec34..1300b51c 100644 --- a/src/components/session/token/SessionUser/Objects/Orders.vue +++ b/src/components/session/token/SessionUser/Objects/Orders.vue @@ -924,6 +924,13 @@ const assignDraftOrderCustomer = async ({ console.error(error); }); }, + resendWashCertificate: async (id) => { + return SessionUser.request( + Orders.meta.endpoint + "/wash-certificate/resend", + "POST", + { id: parseInt(id, 10) } + ).then((response) => response.data.data); + }, showAttachWashCertificateForm(order_id, onAfterSubmit = null) { const normalizedOrderId = Number.parseInt(order_id, 10); if (!Number.isInteger(normalizedOrderId) || normalizedOrderId <= 0) { diff --git a/src/i18n/locales/da.json b/src/i18n/locales/da.json index a9bd1352..64b11d6b 100644 --- a/src/i18n/locales/da.json +++ b/src/i18n/locales/da.json @@ -600,10 +600,20 @@ "delete_user": "Slet bruger", "download_invoice": "Download faktura", "edit_permissions": "Rediger tilladelser", + "email_notifications_section": "E-mail notifikationer", "enter_password": "Indtast adgangskode", "error": "Fejl", "error_changing_password": "Fejl ved ændring af adgangskode", "error_downloading_attachment": "Fejl ved download af vedhæftet fil", + "resend_booking_confirmation": "(Gen)send bookingbekræftelse", + "resend_booking_confirmation_error": "Bookingbekræftelsen kunne ikke sendes", + "resend_booking_confirmation_success": "Bookingbekræftelsen er sendt", + "resend_booking_completion_confirmation": "(Gen)send bekræftelse for færdig booking", + "resend_booking_completion_confirmation_error": "Bekræftelsen for færdig booking kunne ikke sendes", + "resend_booking_completion_confirmation_success": "Bekræftelsen for færdig booking er sendt", + "resend_wash_certificate": "(Gen)send vaskecertifikat", + "resend_wash_certificate_error": "Vaskecertifikatet kunne ikke sendes", + "resend_wash_certificate_success": "Vaskecertifikatet er sendt", "link_copied": "Link kopieret", "login_as_customer": "Log ind som kunde", "login_as_user": "Log ind som bruger", diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index f7284de8..867baf74 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -600,10 +600,20 @@ "delete_user": "Benutzer l?schen", "download_invoice": "Rechnung herunterladen", "edit_permissions": "Berechtigungen bearbeiten", + "email_notifications_section": "E-Mail-Benachrichtigungen", "enter_password": "Passwort eingeben", "error": "Fehler", "error_changing_password": "Beim ?ndern des Passworts ist ein Fehler aufgetreten.", "error_downloading_attachment": "Beim Herunterladen der angeh?ngten Datei ist ein Fehler aufgetreten.", + "resend_booking_confirmation": "(Re)send booking confirmation", + "resend_booking_confirmation_error": "The booking confirmation could not be sent", + "resend_booking_confirmation_success": "The booking confirmation has been sent", + "resend_booking_completion_confirmation": "(Re)send booking completion confirmation", + "resend_booking_completion_confirmation_error": "The booking completion confirmation could not be sent", + "resend_booking_completion_confirmation_success": "The booking completion confirmation has been sent", + "resend_wash_certificate": "(Re)send wash certificate", + "resend_wash_certificate_error": "The wash certificate could not be sent", + "resend_wash_certificate_success": "The wash certificate has been sent", "link_copied": "Link in die Zwischenablage kopiert", "login_as_customer": "Als Kunde anmelden", "login_as_user": "Als Benutzer anmelden", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 1b9c5ecb..289c6ebc 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -600,10 +600,20 @@ "delete_user": "Delete user", "download_invoice": "Download invoice", "edit_permissions": "Edit permissions", + "email_notifications_section": "Email notifications", "enter_password": "Enter password", "error": "Error", "error_changing_password": "An error occurred while changing the password.", "error_downloading_attachment": "An error occurred while downloading the attached file.", + "resend_booking_confirmation": "(Re)send booking confirmation", + "resend_booking_confirmation_error": "The booking confirmation could not be sent", + "resend_booking_confirmation_success": "The booking confirmation has been sent", + "resend_booking_completion_confirmation": "(Re)send booking completion confirmation", + "resend_booking_completion_confirmation_error": "The booking completion confirmation could not be sent", + "resend_booking_completion_confirmation_success": "The booking completion confirmation has been sent", + "resend_wash_certificate": "(Re)send wash certificate", + "resend_wash_certificate_error": "The wash certificate could not be sent", + "resend_wash_certificate_success": "The wash certificate has been sent", "link_copied": "Link copied to clipboard", "login_as_customer": "Log in as customer", "login_as_user": "Log in as user", diff --git a/src/i18n/locales/no.json b/src/i18n/locales/no.json index 877595b1..a6930add 100644 --- a/src/i18n/locales/no.json +++ b/src/i18n/locales/no.json @@ -600,10 +600,20 @@ "delete_user": "Delete user", "download_invoice": "Last ned faktura", "edit_permissions": "Rediger tillatelser", + "email_notifications_section": "E-postvarsler", "enter_password": "Skriv inn passord", "error": "Feil", "error_changing_password": "Det oppstod en feil under endring av passord.", "error_downloading_attachment": "Det oppsto en feil under nedlasting av den vedlagte filen.", + "resend_booking_confirmation": "(Re)send booking confirmation", + "resend_booking_confirmation_error": "The booking confirmation could not be sent", + "resend_booking_confirmation_success": "The booking confirmation has been sent", + "resend_booking_completion_confirmation": "(Re)send booking completion confirmation", + "resend_booking_completion_confirmation_error": "The booking completion confirmation could not be sent", + "resend_booking_completion_confirmation_success": "The booking completion confirmation has been sent", + "resend_wash_certificate": "(Re)send wash certificate", + "resend_wash_certificate_error": "The wash certificate could not be sent", + "resend_wash_certificate_success": "The wash certificate has been sent", "link_copied": "Linken er kopiert til utklippstavlen", "login_as_customer": "Logg inn som kunde", "login_as_user": "Logg inn som bruker", diff --git a/src/i18n/locales/sv.json b/src/i18n/locales/sv.json index beb32311..976f04de 100644 --- a/src/i18n/locales/sv.json +++ b/src/i18n/locales/sv.json @@ -600,10 +600,20 @@ "delete_user": "Delete user", "download_invoice": "Ladda ner faktura", "edit_permissions": "Edit permissions", + "email_notifications_section": "E-postaviseringar", "enter_password": "Enter password", "error": "Fel", "error_changing_password": "Ett fel uppstod när lösenordet ändrades.", "error_downloading_attachment": "Ett fel uppstod när den bifogade filen laddades ner.", + "resend_booking_confirmation": "(Re)send booking confirmation", + "resend_booking_confirmation_error": "The booking confirmation could not be sent", + "resend_booking_confirmation_success": "The booking confirmation has been sent", + "resend_booking_completion_confirmation": "(Re)send booking completion confirmation", + "resend_booking_completion_confirmation_error": "The booking completion confirmation could not be sent", + "resend_booking_completion_confirmation_success": "The booking completion confirmation has been sent", + "resend_wash_certificate": "(Re)send wash certificate", + "resend_wash_certificate_error": "The wash certificate could not be sent", + "resend_wash_certificate_success": "The wash certificate has been sent", "link_copied": "Link copied to clipboard", "login_as_customer": "Log in as customer", "login_as_user": "Log in as user", diff --git a/tests/e2e/admin-pos-orders.spec.ts b/tests/e2e/admin-pos-orders.spec.ts index 44291fd2..cd79cb87 100644 --- a/tests/e2e/admin-pos-orders.spec.ts +++ b/tests/e2e/admin-pos-orders.spec.ts @@ -2747,6 +2747,72 @@ test.describe("Admin POS Orders - desktop action menu layout", () => { await expect(attachmentsSection).toBeVisible(); }); + test("shows email notification actions and resends booking completion confirmation", async ({ page }) => { + const baseFixture = createPosFixture(); + await mockApi(page, { + authenticated: true, + permissions: SUPERUSER_POS_PERMISSIONS, + edgeGateways: false, + pos: createPosFixture({ + ordersById: { + 54518: { + ...baseFixture.ordersById[54518], + user_id: 77, + booking_id: 9101, + customer_name: "(TEST) Pleno Vognmandsforretning", + invoice_collection_id: 101, + }, + }, + attachmentsByOrderId: { + 54518: [ + { + id: 7401, + object_type: "orders", + object_id: 54518, + content: { + image: null, + document: "wash_certificate_54518.pdf", + relation: null, + other: "WASH_CERTIFICATE", + src: null, + }, + created_at: "2026-04-21 08:44:07", + updated_at: "2026-04-21 08:44:07", + deleted_at: null, + }, + ], + }, + }), + }); + await primeOperatorSession(page, "pos-orders-email-notifications-token", SUPERUSER_POS_PERMISSIONS); + + await page.setViewportSize({ width: 1900, height: 900 }); + await page.goto("/admin/12/modules/pos/orders"); + await expect(page.locator("table")).toBeVisible(); + + const dropdownRoot = await openLowestVisibleOrderActionDropdown(page); + const dropdownContent = dropdownRoot.locator(".dropdown-content").first(); + const emailSection = dropdownContent.getByTestId("action-settings-wheel-section-email-notifications"); + + await expect(emailSection).toBeVisible(); + await emailSection.hover(); + + const emailSubmenu = dropdownContent.getByTestId("action-settings-wheel-submenu-email-notifications"); + await expect(emailSubmenu).toBeVisible(); + await expect(emailSubmenu).toContainText("(Gen)send bookingbekræftelse"); + await expect(emailSubmenu).toContainText("(Gen)send bekræftelse for færdig booking"); + await expect(emailSubmenu).not.toContainText("(Gen)send vaskecertifikat"); + + const resendRequestPromise = page.waitForRequest((request) => { + return request.method() === "POST" && request.url().includes("/order-bookings/completion-confirmation/resend"); + }); + + await emailSubmenu.getByRole("button", { name: /\(Gen\)send bekræftelse for færdig booking/i }).click(); + const resendRequest = await resendRequestPromise; + + expect(resendRequest.postDataJSON()).toEqual({ id: 9101 }); + }); + test("keeps the first action reachable while async menu sections load", async ({ page }) => { const posFixture = createAsyncActionMenuPosFixture(); diff --git a/tests/e2e/support/network.js b/tests/e2e/support/network.js index 85e257a8..9e074499 100644 --- a/tests/e2e/support/network.js +++ b/tests/e2e/support/network.js @@ -3203,6 +3203,16 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos return true; } + if ( + (pathname.endsWith("/order-bookings/booking-confirmation/resend") || + pathname.endsWith("/order-bookings/completion-confirmation/resend")) && + method === "POST" + ) { + const body = request.postDataJSON?.() || {}; + await route.fulfill(json({ success: true, data: { id: Number(body.id || 0), sent: true } })); + return true; + } + if (pathname.endsWith("/department/numberplatescanners") && method === "GET") { await route.fulfill(json({ success: true, data: posFixture.numberPlateScanners || [] })); return true; @@ -3802,6 +3812,12 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos return true; } + if (pathname.endsWith("/orders/wash-certificate/resend") && method === "POST") { + const body = request.postDataJSON?.() || {}; + await route.fulfill(json({ success: true, data: { id: Number(body.id || 0), sent: true } })); + return true; + } + if (pathname.endsWith("/departments/order/recommended") && method === "GET") { await route.fulfill( json({ diff --git a/tests/unit/action-settings-wheel-button.spec.js b/tests/unit/action-settings-wheel-button.spec.js index 53693fc5..83bd69ce 100644 --- a/tests/unit/action-settings-wheel-button.spec.js +++ b/tests/unit/action-settings-wheel-button.spec.js @@ -89,6 +89,8 @@ vi.mock("@/components/session/token/SessionUser.vue", () => ({ }, showEditObjectFieldForm: vi.fn(() => Promise.resolve()), functions: { + resendBookingCompletionConfirmation: vi.fn(() => Promise.resolve()), + resendBookingConfirmation: vi.fn(() => Promise.resolve()), showDeleteConfirmationModal: vi.fn(() => Promise.resolve()), }, }, @@ -120,6 +122,7 @@ vi.mock("@/components/session/token/SessionUser.vue", () => ({ assignDraftCustomer: vi.fn(() => Promise.resolve()), get_department_id: vi.fn(() => Promise.resolve(1)), removeAttachment: vi.fn(() => Promise.resolve({ success: true })), + resendWashCertificate: vi.fn(() => Promise.resolve()), showAttachWashCertificateForm: vi.fn(() => Promise.resolve()), showChangeCustomerForm: vi.fn(() => Promise.resolve()), showChangeInvoiceCollectionForm: vi.fn(() => Promise.resolve()), @@ -184,6 +187,7 @@ const SETTINGS_WHEEL_TRANSLATION_KEYS = [ "admin.pos.settings_wheel.delete_user", "admin.pos.settings_wheel.download_invoice", "admin.pos.settings_wheel.edit_permissions", + "admin.pos.settings_wheel.email_notifications_section", "admin.pos.settings_wheel.enter_password", "admin.pos.settings_wheel.error", "admin.pos.settings_wheel.error_changing_password", @@ -212,6 +216,15 @@ const SETTINGS_WHEEL_TRANSLATION_KEYS = [ "admin.pos.settings_wheel.password_changed_text", "admin.pos.settings_wheel.please_enter_password", "admin.pos.settings_wheel.relays_section", + "admin.pos.settings_wheel.resend_booking_completion_confirmation", + "admin.pos.settings_wheel.resend_booking_completion_confirmation_error", + "admin.pos.settings_wheel.resend_booking_completion_confirmation_success", + "admin.pos.settings_wheel.resend_booking_confirmation", + "admin.pos.settings_wheel.resend_booking_confirmation_error", + "admin.pos.settings_wheel.resend_booking_confirmation_success", + "admin.pos.settings_wheel.resend_wash_certificate", + "admin.pos.settings_wheel.resend_wash_certificate_error", + "admin.pos.settings_wheel.resend_wash_certificate_success", "admin.pos.settings_wheel.scan_qr_to_login", "admin.pos.settings_wheel.self_serve_customer", "admin.pos.settings_wheel.self_serve_driver", @@ -461,6 +474,12 @@ describe("ActionSettingsWheelButton", () => { SessionUser.objects.orders.functions.downloadAttachment.mockResolvedValue("https://cdn.example.test/attachment"); SessionUser.objects.orders.functions.removeAttachment.mockReset(); SessionUser.objects.orders.functions.removeAttachment.mockResolvedValue({ success: true }); + SessionUser.objects.orders.functions.resendWashCertificate.mockReset(); + SessionUser.objects.orders.functions.resendWashCertificate.mockResolvedValue({}); + SessionUser.objects.order_bookings.functions.resendBookingConfirmation.mockReset(); + SessionUser.objects.order_bookings.functions.resendBookingConfirmation.mockResolvedValue({}); + SessionUser.objects.order_bookings.functions.resendBookingCompletionConfirmation.mockReset(); + SessionUser.objects.order_bookings.functions.resendBookingCompletionConfirmation.mockResolvedValue({}); SessionUser.objects.department_gates.showCreateObjectForm.mockClear(); SessionUser.objects.department_relays.showCreateObjectForm.mockClear(); SessionUser.objects.collectedOrderInvoices.add.mockReset(); @@ -749,6 +768,141 @@ describe("ActionSettingsWheelButton", () => { expect(wrapper.text()).not.toContain("admin.pos.settings_wheel.mark_as_completed"); }); + it("hides email notifications when the order has no booking or wash certificate", async () => { + const wrapper = mountFlatDropdownButton({ + order_id: 42, + order_booking_id: null, + }); + + await flushMicrotasks(); + await wrapper.find(".dropdown-trigger button").trigger("click"); + await flushMicrotasks(); + + expect(wrapper.text()).not.toContain("admin.pos.settings_wheel.email_notifications_section"); + expect(wrapper.text()).not.toContain("admin.pos.settings_wheel.resend_booking_confirmation"); + expect(wrapper.text()).not.toContain("admin.pos.settings_wheel.resend_wash_certificate"); + }); + + it("shows booking email notification actions for booking-only orders", async () => { + const wrapper = mountFlatDropdownButton({ + order_id: 42, + order_booking_id: 123, + department_id: 1, + }); + + await flushMicrotasks(); + await wrapper.find(".dropdown-trigger button").trigger("click"); + await flushMicrotasks(); + + expect(wrapper.text()).toContain("admin.pos.settings_wheel.email_notifications_section"); + expect(wrapper.text()).toContain("admin.pos.settings_wheel.resend_booking_confirmation"); + expect(wrapper.text()).not.toContain("admin.pos.settings_wheel.resend_booking_completion_confirmation"); + expect(wrapper.text()).not.toContain("admin.pos.settings_wheel.resend_wash_certificate"); + + const button = wrapper + .findAll("button.dropdown-item-action") + .find((buttonWrapper) => buttonWrapper.text().includes("admin.pos.settings_wheel.resend_booking_confirmation")); + + expect(button).toBeTruthy(); + await button.trigger("click"); + await flushMicrotasks(); + + expect(SessionUser.objects.order_bookings.functions.resendBookingConfirmation).toHaveBeenCalledWith(123); + expect(swalFireMock).toHaveBeenCalledWith( + expect.objectContaining({ + title: "admin.pos.settings_wheel.resend_booking_confirmation_success", + icon: "success", + }) + ); + }); + + it("shows booking completion email action when a booking has a wash certificate", async () => { + SessionUser.objects.orders.functions.fetchAttachments.mockResolvedValue([ + { + id: 301, + content: { + document: "wash_certificate_42.pdf", + other: "WASH_CERTIFICATE", + }, + }, + ]); + + const wrapper = mountFlatDropdownButton({ + order_id: 42, + order_booking_id: 123, + department_id: 1, + }); + + await flushMicrotasks(); + await wrapper.find(".dropdown-trigger button").trigger("click"); + await flushMicrotasks(); + + expect(wrapper.text()).toContain("admin.pos.settings_wheel.email_notifications_section"); + expect(wrapper.text()).toContain("admin.pos.settings_wheel.resend_booking_confirmation"); + expect(wrapper.text()).toContain("admin.pos.settings_wheel.resend_booking_completion_confirmation"); + expect(wrapper.text()).not.toContain("admin.pos.settings_wheel.resend_wash_certificate"); + + const button = wrapper + .findAll("button.dropdown-item-action") + .find((buttonWrapper) => + buttonWrapper.text().includes("admin.pos.settings_wheel.resend_booking_completion_confirmation") + ); + + expect(button).toBeTruthy(); + await button.trigger("click"); + await flushMicrotasks(); + + expect(SessionUser.objects.order_bookings.functions.resendBookingCompletionConfirmation).toHaveBeenCalledWith(123); + expect(swalFireMock).toHaveBeenCalledWith( + expect.objectContaining({ + title: "admin.pos.settings_wheel.resend_booking_completion_confirmation_success", + icon: "success", + }) + ); + }); + + it("shows wash certificate email action when an order has a certificate without booking", async () => { + SessionUser.objects.orders.functions.fetchAttachments.mockResolvedValue([ + { + id: 302, + content: { + document: "wash_certificate_42_302.pdf", + other: "wash_certificate_42_302.pdf", + }, + }, + ]); + + const wrapper = mountFlatDropdownButton({ + order_id: 42, + order_booking_id: null, + }); + + await flushMicrotasks(); + await wrapper.find(".dropdown-trigger button").trigger("click"); + await flushMicrotasks(); + + expect(wrapper.text()).toContain("admin.pos.settings_wheel.email_notifications_section"); + expect(wrapper.text()).not.toContain("admin.pos.settings_wheel.resend_booking_confirmation"); + expect(wrapper.text()).not.toContain("admin.pos.settings_wheel.resend_booking_completion_confirmation"); + expect(wrapper.text()).toContain("admin.pos.settings_wheel.resend_wash_certificate"); + + const button = wrapper + .findAll("button.dropdown-item-action") + .find((buttonWrapper) => buttonWrapper.text().includes("admin.pos.settings_wheel.resend_wash_certificate")); + + expect(button).toBeTruthy(); + await button.trigger("click"); + await flushMicrotasks(); + + expect(SessionUser.objects.orders.functions.resendWashCertificate).toHaveBeenCalledWith(42); + expect(swalFireMock).toHaveBeenCalledWith( + expect.objectContaining({ + title: "admin.pos.settings_wheel.resend_wash_certificate_success", + icon: "success", + }) + ); + }); + it("routes 'change invoice collection' action without opening a new tab", async () => { const windowOpenSpy = vi.spyOn(window, "open").mockImplementation(() => null);