Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
609ca5f917 | ||
|
|
a5f2715d31 | ||
|
|
cf0fb3fee9 | ||
|
|
5cb79ed508 | ||
|
|
4a1ffe3a16 | ||
|
|
a4099a5f43 | ||
|
|
8efa7e9bcc | ||
|
|
6467ae787d | ||
|
|
99431a3379 |
@@ -38,7 +38,7 @@
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
|
||||
@@ -328,47 +328,6 @@ const getCustomerDetailItems = (scan) => {
|
||||
return items;
|
||||
};
|
||||
|
||||
const getLastWashDetailItem = (scan) => {
|
||||
// TRU-78 / DRIFT 17: surface the "last washed" timestamp for a scanned
|
||||
// license plate on the landing page so the operator can see at a glance
|
||||
// when a trailer was last washed (DHL pick-up use case).
|
||||
const lastWash = formatLastWashTimestamp(scan?.last_wash);
|
||||
if (!lastWash) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
label: SessionUser.objects.global.language.last_wash || t('admin.pos.recent_washes'),
|
||||
value: lastWash,
|
||||
testKey: 'last-wash',
|
||||
};
|
||||
};
|
||||
|
||||
const getLastWashEmptyStateText = () => {
|
||||
return t('admin.pos.never_washed');
|
||||
};
|
||||
|
||||
const formatLastWashTimestamp = (value) => {
|
||||
if (!isMeaningfulValue(value)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const parsedDate = new Date(String(value).replace(' ', 'T'));
|
||||
if (Number.isNaN(parsedDate.getTime())) {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
try {
|
||||
return new Intl.DateTimeFormat(locale.value || undefined, {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
}).format(parsedDate);
|
||||
} catch (error) {
|
||||
console.error('Unable to format last_wash timestamp', error);
|
||||
return String(value);
|
||||
}
|
||||
};
|
||||
|
||||
const getVehicleDetailItems = (scan) => {
|
||||
const detailState = getPlateDetailState(scan.plate);
|
||||
if (detailState.status !== 'loaded') {
|
||||
@@ -534,29 +493,19 @@ setTimeout(() => {
|
||||
:data-testid="`pos-recent-scan-details-${scan.id}`"
|
||||
>
|
||||
<div
|
||||
v-if="getCustomerDetailItems(scan).length > 0 || getLastWashDetailItem(scan)"
|
||||
v-if="getCustomerDetailItems(scan).length > 0"
|
||||
class="pos-scan-details__grid pos-scan-details__grid--customer"
|
||||
>
|
||||
<div
|
||||
v-for="item in [...getCustomerDetailItems(scan), getLastWashDetailItem(scan)].filter(Boolean)"
|
||||
v-for="item in getCustomerDetailItems(scan)"
|
||||
:key="item.label"
|
||||
class="pos-scan-detail-field"
|
||||
:data-testid="`pos-recent-scan-detail-${scan.id}-${item.testKey || item.label}`"
|
||||
>
|
||||
<span class="pos-scan-detail-field__label">{{ item.label }}</span>
|
||||
<span class="pos-scan-detail-field__value">{{ item.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-if="!getLastWashDetailItem(scan) && scan.last_wash == null"
|
||||
class="pos-scan-details__state"
|
||||
:data-testid="`pos-recent-scan-never-washed-${scan.id}`"
|
||||
>
|
||||
<i class="fas fa-info-circle" aria-hidden="true"></i>
|
||||
<span>{{ getLastWashEmptyStateText() }}</span>
|
||||
</p>
|
||||
|
||||
<div v-if="getPlateDetailState(scan.plate).status === 'loading'" class="pos-scan-details__state">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
<span>{{ $t('admin.pos.recent_scan_details_loading') }}</span>
|
||||
|
||||
+1
-1
@@ -271,7 +271,7 @@ const hiddenOrderFilterDefinitions = computed(() => [
|
||||
options: [
|
||||
{ value: "*", label: t("common.all") },
|
||||
{ value: "1", label: "E-conomic" },
|
||||
{ value: "2", label: "Stripe" },
|
||||
{ value: "2", label: t("pagination.payment_link") },
|
||||
{ value: "3", label: t("pagination.other_no_tracking") },
|
||||
{ value: "is_null", label: t("pagination.not_specified") },
|
||||
],
|
||||
|
||||
@@ -5360,6 +5360,7 @@
|
||||
"other_no_tracking": "@.capitalize:{'words.generated.anden'} (@.capitalize:{'words.generated.uden'} tracking)",
|
||||
"page_of": "@:{'templates.common.page_of'}",
|
||||
"payment_processor": "Betalingsprocessor",
|
||||
"payment_link": "Betalingslink",
|
||||
"per_page": "@.capitalize:{'words.generated.per'} @:{'words.generated.side'}",
|
||||
"pickup": "@:{'words.generated.afhentning'}",
|
||||
"previous": "@.capitalize:{'words.generated.forrige'} @:{'words.generated.side'}",
|
||||
@@ -5448,7 +5449,6 @@
|
||||
},
|
||||
"last_wash_matches_current_selection": "@.capitalize:{'words.generated.samme'} @:{'words.generated.ydelse'}, @:{'words.generated.tilvalg'} @:{'words.generated.og'} @:{'words.generated.ekstra'} @:{'words.generated.varer'} @:{'words.generated.som'} @:{'words.generated.sidste'} @:{'words.generated.vask'}",
|
||||
"manual_entry": "Manuell @:{'words.generated.registrering'}",
|
||||
"never_washed": "Aldrig vasket",
|
||||
"new_order": "@.capitalize:{'words.generated.ny'} order",
|
||||
"new_sale": "@.capitalize:{'words.generated.nyt'} @:{'words.generated.salg'}",
|
||||
"no_bookings": "@.capitalize:{'words.generated.ingen'} @:{'words.generated.bookinger'} @:{'words.generated.fundet'}",
|
||||
@@ -5511,13 +5511,13 @@
|
||||
"process_booking": "@:{'words.generated.behandler'} @:{'words.generated.booking'}...",
|
||||
"processing_order": "@:{'words.generated.behandler'} @:{'words.generated.ordre'}...",
|
||||
"restrictions": {
|
||||
"addons_not_allowed": "Tilvalg er ikke tilladt for denne kunde",
|
||||
"backend_rejected": "Produktet blev afvist af kundereglerne og er ikke tilføjet",
|
||||
"addons_not_allowed": "Tilvalg er ikke tilgængelige for denne kunde. Tjek kundens regler eller vælg et andet produkt.",
|
||||
"backend_rejected": "Produktet blev afvist af kundereglerne og er ikke tilføjet. Prøv et andet produkt eller fjern reglen.",
|
||||
"load_failed": "Kundereglerne kunne ikke indlæses. Prøv igen, før du ændrer produkter.",
|
||||
"loading": "Kundereglerne indlæses. Produktændringer er midlertidigt deaktiveret.",
|
||||
"product_not_allowed": "Produktet er ikke tilladt for denne kunde",
|
||||
"restricted_items_removed": "Begrænsede tilvalg er fjernet fra kurven",
|
||||
"unavailable": "Ikke tilgængelig",
|
||||
"product_not_allowed": "Produktet er ikke tilgængeligt for denne kunde. Vælg et andet produkt eller justér kundens regler.",
|
||||
"restricted_items_removed": "Begrænsede tilvalg er fjernet fra kurven. Vælg andre tilvalg, der matcher kundens regler.",
|
||||
"unavailable": "Ikke tilgængeligt — tjek kundens regler",
|
||||
"title": "Kunderegel"
|
||||
},
|
||||
"receipt": "@:{'templates.generated.compat.global.payment.receipt'}",
|
||||
|
||||
@@ -5470,6 +5470,7 @@
|
||||
"other_no_tracking": "Other (@.capitalize:{'words.generated.keine'} tracking)",
|
||||
"page_of": "@:{'templates.common.page_of'}",
|
||||
"payment_processor": "Zahlungsabwickler",
|
||||
"payment_link": "Zahlungslink",
|
||||
"per_page": "@.capitalize:{'words.generated.pro'} @:{'words.generated.seite'}",
|
||||
"pickup": "@:{'words.generated.abholung'}",
|
||||
"previous": "@.capitalize:{'words.generated.vorherige'} @:{'words.generated.seite'}",
|
||||
@@ -5558,7 +5559,6 @@
|
||||
},
|
||||
"last_wash_matches_current_selection": "@.capitalize:{'words.generated.gleiche'} @:{'words.generated.leistung'}, @:{'words.generated.zusatzoptionen'} @:{'words.generated.und'} @:{'words.generated.zusatzartikel'} @:{'words.generated.wie'} @:{'words.generated.bei'} @:{'words.generated.der'} @:{'words.generated.letzten'} @:{'words.generated.wasche'}",
|
||||
"manual_entry": "@.capitalize:{'words.generated.manuell'} registrering",
|
||||
"never_washed": "Noch nie gewaschen",
|
||||
"new_order": "@.capitalize:{'words.generated.neuer'} @:{'words.generated.auftrag'}",
|
||||
"new_sale": "Nytt salg",
|
||||
"no_bookings": "@.capitalize:{'words.generated.keine'} @:{'words.generated.buchungen'} @:{'words.generated.gefunden'}",
|
||||
@@ -5621,13 +5621,13 @@
|
||||
"process_booking": "@:{'words.generated.buchung'} @:{'words.generated.verarbeiten'}",
|
||||
"processing_order": "@:{'words.generated.auftrag'} @:{'words.generated.wird'} verarbeitet...",
|
||||
"restrictions": {
|
||||
"addons_not_allowed": "Zusatzoptionen sind für diesen Kunden nicht erlaubt",
|
||||
"backend_rejected": "Das Produkt wurde durch Kundenregeln abgelehnt und nicht hinzugefügt",
|
||||
"addons_not_allowed": "Zusatzoptionen sind für diesen Kunden nicht verfügbar. Prüfen Sie die Kundenregeln oder wählen Sie ein anderes Produkt.",
|
||||
"backend_rejected": "Das Produkt wurde durch die Kundenregeln abgelehnt und nicht hinzugefügt. Versuchen Sie ein anderes Produkt oder entfernen Sie die Regel.",
|
||||
"load_failed": "Kundenregeln konnten nicht geladen werden. Versuchen Sie es erneut, bevor Sie Produkte ändern.",
|
||||
"loading": "Kundenregeln werden geladen. Produktänderungen sind vorübergehend deaktiviert.",
|
||||
"product_not_allowed": "Dieses Produkt ist für diesen Kunden nicht erlaubt",
|
||||
"restricted_items_removed": "Eingeschränkte Zusatzoptionen wurden aus dem Warenkorb entfernt",
|
||||
"unavailable": "Nicht verfügbar",
|
||||
"product_not_allowed": "Dieses Produkt ist für diesen Kunden nicht verfügbar. Wählen Sie ein anderes Produkt oder passen Sie die Kundenregeln an.",
|
||||
"restricted_items_removed": "Eingeschränkte Zusatzoptionen wurden aus dem Warenkorb entfernt. Wählen Sie andere Zusatzoptionen, die zu den Kundenregeln passen.",
|
||||
"unavailable": "Nicht verfügbar — Kundenregeln prüfen",
|
||||
"title": "Kundenregel"
|
||||
},
|
||||
"receipt": "@.capitalize:{'words.generated.kvitto'}",
|
||||
|
||||
@@ -5191,6 +5191,7 @@
|
||||
"other_no_tracking": "@.capitalize:{'words.generated.other'} (@.capitalize:{'words.generated.no'} tracking)",
|
||||
"page_of": "@:{'templates.common.page_of'}",
|
||||
"payment_processor": "@.capitalize:{'words.generated.payment'} @:{'words.generated.processor'}",
|
||||
"payment_link": "Payment link",
|
||||
"per_page": "@.capitalize:{'words.generated.per'} @:{'words.generated.page'}",
|
||||
"pickup": "@:{'words.generated.pickup'}",
|
||||
"previous": "@.capitalize:{'words.generated.previous'} @:{'words.generated.page'}",
|
||||
@@ -5279,7 +5280,6 @@
|
||||
},
|
||||
"last_wash_matches_current_selection": "@.capitalize:{'words.generated.same'} @:{'words.generated.service'}, @:{'words.generated.add'}-@:{'words.generated.ons'}, @:{'words.generated.and'} @:{'words.generated.extra'} @:{'words.generated.items'} @:{'words.generated.as'} @:{'words.replication.article.host_mention'} @:{'words.generated.previous'} @:{'words.generated.wash'}",
|
||||
"manual_entry": "@:{'words.generated.manual'} @:{'words.generated.registration'}",
|
||||
"never_washed": "Never washed",
|
||||
"new_order": "@.capitalize:{'words.generated.new'} @:{'words.generated.order'}",
|
||||
"new_sale": "@.capitalize:{'words.generated.new'} sale",
|
||||
"no_bookings": "@.capitalize:{'words.generated.no'} @:{'words.generated.orders'} @:{'words.generated.found'}",
|
||||
@@ -5342,13 +5342,13 @@
|
||||
"process_booking": "@.capitalize:{'words.generated.process'} @:{'words.generated.order'}",
|
||||
"processing_order": "@.capitalize:{'words.generated.processing'} @:{'words.generated.order'}...",
|
||||
"restrictions": {
|
||||
"addons_not_allowed": "Add-ons are not allowed for this customer",
|
||||
"backend_rejected": "The product was rejected by customer rules and was not added",
|
||||
"addons_not_allowed": "Add-ons are not available for this customer. Check the customer's rules or choose another product.",
|
||||
"backend_rejected": "The product was rejected by the customer rules and was not added. Try a different product or remove the rule.",
|
||||
"load_failed": "Customer rules could not be loaded. Retry before changing products.",
|
||||
"loading": "Customer rules are loading. Product changes are temporarily disabled.",
|
||||
"product_not_allowed": "This product is not allowed for this customer",
|
||||
"restricted_items_removed": "Restricted add-ons were removed from the cart",
|
||||
"unavailable": "Unavailable",
|
||||
"product_not_allowed": "This product is not available for this customer. Pick another product or adjust the customer's rules.",
|
||||
"restricted_items_removed": "Restricted add-ons were removed from the cart. Choose other add-ons that match the customer's rules.",
|
||||
"unavailable": "Unavailable — check the customer's rules",
|
||||
"title": "Customer rule"
|
||||
},
|
||||
"receipt": "@:{'templates.generated.compat.global.payment.receipt'}",
|
||||
|
||||
@@ -4852,8 +4852,6 @@
|
||||
"last_wash_matches_current_selection": "@:{'templates.generated.compat.pos.last_wash_matches_current_selection'}",
|
||||
"license_plate": "@:{'templates.generated.compat.tables.common.registration_number'}",
|
||||
"manual_entry": "@:{'templates.generated.compat.pos.manual_entry'}",
|
||||
"last_wash": "@:{'templates.generated.compat.global.last_wash'}",
|
||||
"never_washed": "@:{'templates.generated.compat.pos.never_washed'}",
|
||||
"new_order": "@:{'templates.generated.compat.pos.new_order'}",
|
||||
"new_sale": "@:{'templates.generated.compat.pos.new_sale'}",
|
||||
"no_bookings": "@:{'templates.generated.compat.pos.no_bookings'}",
|
||||
|
||||
@@ -5473,6 +5473,7 @@
|
||||
"other_no_tracking": "@.capitalize:{'words.generated.annet'} (@:{'words.generated.ingen'} sporing)",
|
||||
"page_of": "@:{'templates.common.page_of'}",
|
||||
"payment_processor": "Betalingsbehandler",
|
||||
"payment_link": "Betalingslenke",
|
||||
"per_page": "@.capitalize:{'words.generated.per'} @:{'words.generated.side'}",
|
||||
"pickup": "@:{'words.generated.henting'}",
|
||||
"previous": "@.capitalize:{'words.generated.forrige'} @:{'words.generated.side'}",
|
||||
@@ -5561,7 +5562,6 @@
|
||||
},
|
||||
"last_wash_matches_current_selection": "@.capitalize:{'words.generated.samme'} @:{'words.generated.tjeneste'}, @:{'words.generated.tillegg'} @:{'words.generated.og'} @:{'words.generated.ekstra'} @:{'words.generated.varer'} @:{'words.generated.som'} @:{'words.generated.forrige'} @:{'words.generated.vask'}",
|
||||
"manual_entry": "Manuell @:{'words.generated.registrering'}",
|
||||
"never_washed": "Aldri vasket",
|
||||
"new_order": "@.capitalize:{'words.generated.bestillingen'}",
|
||||
"new_sale": "@.capitalize:{'words.generated.nytt'} @:{'words.generated.salg'}",
|
||||
"no_bookings": "@.capitalize:{'words.generated.ingen'} @:{'words.generated.bestillinger'} @:{'words.generated.funnet'}",
|
||||
@@ -5624,13 +5624,13 @@
|
||||
"process_booking": "@.capitalize:{'words.generated.behandle'} @:{'words.generated.bestilling'}",
|
||||
"processing_order": "@:{'words.generated.behandler'} @:{'words.generated.bestilling'}...",
|
||||
"restrictions": {
|
||||
"addons_not_allowed": "Tilvalg er ikke tillatt for denne kunden",
|
||||
"backend_rejected": "Produktet ble avvist av kunderegler og ble ikke lagt til",
|
||||
"addons_not_allowed": "Tilvalg er ikke tilgjengelige for denne kunden. Sjekk kundens regler eller velg et annet produkt.",
|
||||
"backend_rejected": "Produktet ble avvist av kunderegler og ble ikke lagt til. Prøv et annet produkt eller fjern regelen.",
|
||||
"load_failed": "Kundereglene kunne ikke lastes. Prøv igjen før du endrer produkter.",
|
||||
"loading": "Kundereglene lastes. Produktendringer er midlertidig deaktivert.",
|
||||
"product_not_allowed": "Dette produktet er ikke tillatt for denne kunden",
|
||||
"restricted_items_removed": "Begrensede tilvalg ble fjernet fra kurven",
|
||||
"unavailable": "Ikke tilgjengelig",
|
||||
"product_not_allowed": "Dette produktet er ikke tilgjengelig for denne kunden. Velg et annet produkt eller juster kundens regler.",
|
||||
"restricted_items_removed": "Begrensede tilvalg ble fjernet fra kurven. Velg andre tilvalg som passer kundens regler.",
|
||||
"unavailable": "Ikke tilgjengelig — sjekk kundens regler",
|
||||
"title": "Kunderegel"
|
||||
},
|
||||
"receipt": "@:{'templates.generated.compat.global.payment.receipt'}",
|
||||
|
||||
@@ -5523,6 +5523,7 @@
|
||||
"other_no_tracking": "övrigt (@:{'words.generated.ingen'} spårning)",
|
||||
"page_of": "@.capitalize:{'words.generated.page'} {current} @:{'words.generated.of'} {total}",
|
||||
"payment_processor": "Betalningsleverantär",
|
||||
"payment_link": "Betalningslänk",
|
||||
"per_page": "@.capitalize:{'words.generated.per'} @:{'words.generated.page'}",
|
||||
"pickup": "Pickup",
|
||||
"previous": "@.capitalize:{'words.generated.foregaende'} @:{'words.generated.sida'}",
|
||||
@@ -5611,7 +5612,6 @@
|
||||
},
|
||||
"last_wash_matches_current_selection": "@.capitalize:{'words.generated.samma'} @:{'words.generated.tjanst'}, @:{'words.generated.tillval'} @:{'words.generated.och'} @:{'words.generated.extra'} @:{'words.generated.artiklar'} @:{'words.generated.som'} @:{'words.generated.senaste'} @:{'words.generated.tvatten'}",
|
||||
"manual_entry": "Manuell @:{'words.generated.registrering'}",
|
||||
"never_washed": "Aldrig tvättad",
|
||||
"new_order": "@.capitalize:{'words.generated.ny'} @:{'words.generated.order'}",
|
||||
"new_sale": "@.capitalize:{'words.generated.nytt'} salg",
|
||||
"no_bookings": "@.capitalize:{'words.generated.ingen'} @:{'words.generated.bestillinger'} @:{'words.generated.funnet'}",
|
||||
@@ -5674,13 +5674,13 @@
|
||||
"process_booking": "Behandle @:{'words.generated.bestilling'}",
|
||||
"processing_order": "Behandler @:{'words.generated.ordre'}...",
|
||||
"restrictions": {
|
||||
"addons_not_allowed": "Tillval är inte tillåtna för den här kunden",
|
||||
"backend_rejected": "Produkten avvisades av kundregler och lades inte till",
|
||||
"addons_not_allowed": "Tillval är inte tillgängliga för den här kunden. Kontrollera kundens regler eller välj en annan produkt.",
|
||||
"backend_rejected": "Produkten avvisades av kundregler och lades inte till. Försök med en annan produkt eller ta bort regeln.",
|
||||
"load_failed": "Kundreglerna kunde inte läsas in. Försök igen innan du ändrar produkter.",
|
||||
"loading": "Kundreglerna läses in. Produktändringar är tillfälligt inaktiverade.",
|
||||
"product_not_allowed": "Den här produkten är inte tillåten för kunden",
|
||||
"restricted_items_removed": "Begränsade tillval togs bort från varukorgen",
|
||||
"unavailable": "Inte tillgänglig",
|
||||
"product_not_allowed": "Den här produkten är inte tillgänglig för kunden. Välj en annan produkt eller justera kundens regler.",
|
||||
"restricted_items_removed": "Begränsade tillval togs bort från varukorgen. Välj andra tillval som matchar kundens regler.",
|
||||
"unavailable": "Inte tillgänglig — kontrollera kundens regler",
|
||||
"title": "Kundregel"
|
||||
},
|
||||
"receipt": "@:{'templates.generated.compat.global.payment.receipt'}",
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"other_no_tracking": "@.capitalize:{'terms.glossary.anden'} (@.capitalize:{'terms.glossary.uden'} tracking)",
|
||||
"page_of": "@:{'phrases.common.page_of'}",
|
||||
"payment_processor": "Betalingsprocessor",
|
||||
"payment_link": "Betalingslink",
|
||||
"per_page": "@.capitalize:{'terms.glossary.per'} @:{'terms.glossary.side'}",
|
||||
"pickup": "@:{'terms.glossary.afhentning'}",
|
||||
"previous": "@.capitalize:{'terms.glossary.forrige'} @:{'terms.glossary.side'}",
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
},
|
||||
"last_wash_matches_current_selection": "@.capitalize:{'terms.glossary.samme'} @:{'terms.glossary.ydelse'}, @:{'terms.glossary.tilvalg'} @:{'terms.glossary.og'} @:{'terms.glossary.ekstra'} @:{'terms.glossary.varer'} @:{'terms.glossary.som'} @:{'terms.glossary.sidste'} @:{'terms.glossary.vask'}",
|
||||
"manual_entry": "Manuell @:{'terms.glossary.registrering'}",
|
||||
"never_washed": "Aldrig vasket",
|
||||
"new_order": "@.capitalize:{'terms.glossary.ny'} order",
|
||||
"new_sale": "@.capitalize:{'terms.glossary.nyt'} @:{'terms.glossary.salg'}",
|
||||
"no_bookings": "@.capitalize:{'terms.glossary.ingen'} @:{'terms.glossary.bookinger'} @:{'terms.glossary.fundet'}",
|
||||
@@ -107,13 +106,13 @@
|
||||
"process_booking": "@:{'terms.glossary.behandler'} @:{'terms.glossary.booking'}...",
|
||||
"processing_order": "@:{'terms.glossary.behandler'} @:{'terms.glossary.ordre'}...",
|
||||
"restrictions": {
|
||||
"addons_not_allowed": "Tilvalg er ikke tilladt for denne kunde",
|
||||
"backend_rejected": "Produktet blev afvist af kundereglerne og er ikke tilføjet",
|
||||
"addons_not_allowed": "Tilvalg er ikke tilgængelige for denne kunde. Tjek kundens regler eller vælg et andet produkt.",
|
||||
"backend_rejected": "Produktet blev afvist af kundereglerne og er ikke tilføjet. Prøv et andet produkt eller fjern reglen.",
|
||||
"load_failed": "Kundereglerne kunne ikke indlæses. Prøv igen, før du ændrer produkter.",
|
||||
"loading": "Kundereglerne indlæses. Produktændringer er midlertidigt deaktiveret.",
|
||||
"product_not_allowed": "Produktet er ikke tilladt for denne kunde",
|
||||
"restricted_items_removed": "Begrænsede tilvalg er fjernet fra kurven",
|
||||
"unavailable": "Ikke tilgængelig",
|
||||
"product_not_allowed": "Produktet er ikke tilgængeligt for denne kunde. Vælg et andet produkt eller justér kundens regler.",
|
||||
"restricted_items_removed": "Begrænsede tilvalg er fjernet fra kurven. Vælg andre tilvalg, der matcher kundens regler.",
|
||||
"unavailable": "Ikke tilgængeligt — tjek kundens regler",
|
||||
"title": "Kunderegel"
|
||||
},
|
||||
"receipt": "@:{'phrases.compat.global.payment.receipt'}",
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"other_no_tracking": "Other (@.capitalize:{'terms.glossary.keine'} tracking)",
|
||||
"page_of": "@:{'phrases.common.page_of'}",
|
||||
"payment_processor": "Zahlungsabwickler",
|
||||
"payment_link": "Zahlungslink",
|
||||
"per_page": "@.capitalize:{'terms.glossary.pro'} @:{'terms.glossary.seite'}",
|
||||
"pickup": "@:{'terms.glossary.abholung'}",
|
||||
"previous": "@.capitalize:{'terms.glossary.vorherige'} @:{'terms.glossary.seite'}",
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
},
|
||||
"last_wash_matches_current_selection": "@.capitalize:{'terms.glossary.gleiche'} @:{'terms.glossary.leistung'}, @:{'terms.glossary.zusatzoptionen'} @:{'terms.glossary.und'} @:{'terms.glossary.zusatzartikel'} @:{'terms.glossary.wie'} @:{'terms.glossary.bei'} @:{'terms.glossary.der'} @:{'terms.glossary.letzten'} @:{'terms.glossary.wasche'}",
|
||||
"manual_entry": "@.capitalize:{'terms.glossary.manuell'} registrering",
|
||||
"never_washed": "Noch nie gewaschen",
|
||||
"new_order": "@.capitalize:{'terms.glossary.neuer'} @:{'terms.glossary.auftrag'}",
|
||||
"new_sale": "Nytt salg",
|
||||
"no_bookings": "@.capitalize:{'terms.glossary.keine'} @:{'terms.glossary.buchungen'} @:{'terms.glossary.gefunden'}",
|
||||
@@ -107,13 +106,13 @@
|
||||
"process_booking": "@:{'terms.glossary.buchung'} @:{'terms.glossary.verarbeiten'}",
|
||||
"processing_order": "@:{'terms.glossary.auftrag'} @:{'terms.glossary.wird'} verarbeitet...",
|
||||
"restrictions": {
|
||||
"addons_not_allowed": "Zusatzoptionen sind für diesen Kunden nicht erlaubt",
|
||||
"backend_rejected": "Das Produkt wurde durch Kundenregeln abgelehnt und nicht hinzugefügt",
|
||||
"addons_not_allowed": "Zusatzoptionen sind für diesen Kunden nicht verfügbar. Prüfen Sie die Kundenregeln oder wählen Sie ein anderes Produkt.",
|
||||
"backend_rejected": "Das Produkt wurde durch die Kundenregeln abgelehnt und nicht hinzugefügt. Versuchen Sie ein anderes Produkt oder entfernen Sie die Regel.",
|
||||
"load_failed": "Kundenregeln konnten nicht geladen werden. Versuchen Sie es erneut, bevor Sie Produkte ändern.",
|
||||
"loading": "Kundenregeln werden geladen. Produktänderungen sind vorübergehend deaktiviert.",
|
||||
"product_not_allowed": "Dieses Produkt ist für diesen Kunden nicht erlaubt",
|
||||
"restricted_items_removed": "Eingeschränkte Zusatzoptionen wurden aus dem Warenkorb entfernt",
|
||||
"unavailable": "Nicht verfügbar",
|
||||
"product_not_allowed": "Dieses Produkt ist für diesen Kunden nicht verfügbar. Wählen Sie ein anderes Produkt oder passen Sie die Kundenregeln an.",
|
||||
"restricted_items_removed": "Eingeschränkte Zusatzoptionen wurden aus dem Warenkorb entfernt. Wählen Sie andere Zusatzoptionen, die zu den Kundenregeln passen.",
|
||||
"unavailable": "Nicht verfügbar — Kundenregeln prüfen",
|
||||
"title": "Kundenregel"
|
||||
},
|
||||
"receipt": "@.capitalize:{'terms.glossary.kvitto'}",
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"other_no_tracking": "@.capitalize:{'terms.glossary.other'} (@.capitalize:{'terms.glossary.no'} tracking)",
|
||||
"page_of": "@:{'phrases.common.page_of'}",
|
||||
"payment_processor": "@.capitalize:{'terms.glossary.payment'} @:{'terms.glossary.processor'}",
|
||||
"payment_link": "Payment link",
|
||||
"per_page": "@.capitalize:{'terms.glossary.per'} @:{'terms.glossary.page'}",
|
||||
"pickup": "@:{'terms.glossary.pickup'}",
|
||||
"previous": "@.capitalize:{'terms.glossary.previous'} @:{'terms.glossary.page'}",
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
},
|
||||
"last_wash_matches_current_selection": "@.capitalize:{'terms.glossary.same'} @:{'terms.glossary.service'}, @:{'terms.glossary.add'}-@:{'terms.glossary.ons'}, @:{'terms.glossary.and'} @:{'terms.glossary.extra'} @:{'terms.glossary.items'} @:{'terms.glossary.as'} @:{'terms.replication.article.host_mention'} @:{'terms.glossary.previous'} @:{'terms.glossary.wash'}",
|
||||
"manual_entry": "@:{'terms.glossary.manual'} @:{'terms.glossary.registration'}",
|
||||
"never_washed": "Never washed",
|
||||
"new_order": "@.capitalize:{'terms.glossary.new'} @:{'terms.glossary.order'}",
|
||||
"new_sale": "@.capitalize:{'terms.glossary.new'} sale",
|
||||
"no_bookings": "@.capitalize:{'terms.glossary.no'} @:{'terms.glossary.orders'} @:{'terms.glossary.found'}",
|
||||
@@ -107,13 +106,13 @@
|
||||
"process_booking": "@.capitalize:{'terms.glossary.process'} @:{'terms.glossary.order'}",
|
||||
"processing_order": "@.capitalize:{'terms.glossary.processing'} @:{'terms.glossary.order'}...",
|
||||
"restrictions": {
|
||||
"addons_not_allowed": "Add-ons are not allowed for this customer",
|
||||
"backend_rejected": "The product was rejected by customer rules and was not added",
|
||||
"addons_not_allowed": "Add-ons are not available for this customer. Check the customer's rules or choose another product.",
|
||||
"backend_rejected": "The product was rejected by the customer rules and was not added. Try a different product or remove the rule.",
|
||||
"load_failed": "Customer rules could not be loaded. Retry before changing products.",
|
||||
"loading": "Customer rules are loading. Product changes are temporarily disabled.",
|
||||
"product_not_allowed": "This product is not allowed for this customer",
|
||||
"restricted_items_removed": "Restricted add-ons were removed from the cart",
|
||||
"unavailable": "Unavailable",
|
||||
"product_not_allowed": "This product is not available for this customer. Pick another product or adjust the customer's rules.",
|
||||
"restricted_items_removed": "Restricted add-ons were removed from the cart. Choose other add-ons that match the customer's rules.",
|
||||
"unavailable": "Unavailable — check the customer's rules",
|
||||
"title": "Customer rule"
|
||||
},
|
||||
"receipt": "@:{'phrases.compat.global.payment.receipt'}",
|
||||
|
||||
@@ -76,8 +76,6 @@
|
||||
"last_wash_matches_current_selection": "@:{'phrases.compat.pos.last_wash_matches_current_selection'}",
|
||||
"license_plate": "@:{'phrases.compat.tables.common.registration_number'}",
|
||||
"manual_entry": "@:{'phrases.compat.pos.manual_entry'}",
|
||||
"last_wash": "@:{'phrases.compat.global.last_wash'}",
|
||||
"never_washed": "@:{'phrases.compat.pos.never_washed'}",
|
||||
"new_order": "@:{'phrases.compat.pos.new_order'}",
|
||||
"new_sale": "@:{'phrases.compat.pos.new_sale'}",
|
||||
"no_bookings": "@:{'phrases.compat.pos.no_bookings'}",
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"other_no_tracking": "@.capitalize:{'terms.glossary.annet'} (@:{'terms.glossary.ingen'} sporing)",
|
||||
"page_of": "@:{'phrases.common.page_of'}",
|
||||
"payment_processor": "Betalingsbehandler",
|
||||
"payment_link": "Betalingslenke",
|
||||
"per_page": "@.capitalize:{'terms.glossary.per'} @:{'terms.glossary.side'}",
|
||||
"pickup": "@:{'terms.glossary.henting'}",
|
||||
"previous": "@.capitalize:{'terms.glossary.forrige'} @:{'terms.glossary.side'}",
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
},
|
||||
"last_wash_matches_current_selection": "@.capitalize:{'terms.glossary.samme'} @:{'terms.glossary.tjeneste'}, @:{'terms.glossary.tillegg'} @:{'terms.glossary.og'} @:{'terms.glossary.ekstra'} @:{'terms.glossary.varer'} @:{'terms.glossary.som'} @:{'terms.glossary.forrige'} @:{'terms.glossary.vask'}",
|
||||
"manual_entry": "Manuell @:{'terms.glossary.registrering'}",
|
||||
"never_washed": "Aldri vasket",
|
||||
"new_order": "@.capitalize:{'terms.glossary.bestillingen'}",
|
||||
"new_sale": "@.capitalize:{'terms.glossary.nytt'} @:{'terms.glossary.salg'}",
|
||||
"no_bookings": "@.capitalize:{'terms.glossary.ingen'} @:{'terms.glossary.bestillinger'} @:{'terms.glossary.funnet'}",
|
||||
@@ -107,13 +106,13 @@
|
||||
"process_booking": "@.capitalize:{'terms.glossary.behandle'} @:{'terms.glossary.bestilling'}",
|
||||
"processing_order": "@:{'terms.glossary.behandler'} @:{'terms.glossary.bestilling'}...",
|
||||
"restrictions": {
|
||||
"addons_not_allowed": "Tilvalg er ikke tillatt for denne kunden",
|
||||
"backend_rejected": "Produktet ble avvist av kunderegler og ble ikke lagt til",
|
||||
"addons_not_allowed": "Tilvalg er ikke tilgjengelige for denne kunden. Sjekk kundens regler eller velg et annet produkt.",
|
||||
"backend_rejected": "Produktet ble avvist av kunderegler og ble ikke lagt til. Prøv et annet produkt eller fjern regelen.",
|
||||
"load_failed": "Kundereglene kunne ikke lastes. Prøv igjen før du endrer produkter.",
|
||||
"loading": "Kundereglene lastes. Produktendringer er midlertidig deaktivert.",
|
||||
"product_not_allowed": "Dette produktet er ikke tillatt for denne kunden",
|
||||
"restricted_items_removed": "Begrensede tilvalg ble fjernet fra kurven",
|
||||
"unavailable": "Ikke tilgjengelig",
|
||||
"product_not_allowed": "Dette produktet er ikke tilgjengelig for denne kunden. Velg et annet produkt eller juster kundens regler.",
|
||||
"restricted_items_removed": "Begrensede tilvalg ble fjernet fra kurven. Velg andre tilvalg som passer kundens regler.",
|
||||
"unavailable": "Ikke tilgjengelig — sjekk kundens regler",
|
||||
"title": "Kunderegel"
|
||||
},
|
||||
"receipt": "@:{'phrases.compat.global.payment.receipt'}",
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"other_no_tracking": "övrigt (@:{'terms.glossary.ingen'} spårning)",
|
||||
"page_of": "@.capitalize:{'terms.glossary.page'} {current} @:{'terms.glossary.of'} {total}",
|
||||
"payment_processor": "Betalningsleverantär",
|
||||
"payment_link": "Betalningslänk",
|
||||
"per_page": "@.capitalize:{'terms.glossary.per'} @:{'terms.glossary.page'}",
|
||||
"pickup": "Pickup",
|
||||
"previous": "@.capitalize:{'terms.glossary.foregaende'} @:{'terms.glossary.sida'}",
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
},
|
||||
"last_wash_matches_current_selection": "@.capitalize:{'terms.glossary.samma'} @:{'terms.glossary.tjanst'}, @:{'terms.glossary.tillval'} @:{'terms.glossary.och'} @:{'terms.glossary.extra'} @:{'terms.glossary.artiklar'} @:{'terms.glossary.som'} @:{'terms.glossary.senaste'} @:{'terms.glossary.tvatten'}",
|
||||
"manual_entry": "Manuell @:{'terms.glossary.registrering'}",
|
||||
"never_washed": "Aldrig tvättad",
|
||||
"new_order": "@.capitalize:{'terms.glossary.ny'} @:{'terms.glossary.order'}",
|
||||
"new_sale": "@.capitalize:{'terms.glossary.nytt'} salg",
|
||||
"no_bookings": "@.capitalize:{'terms.glossary.ingen'} @:{'terms.glossary.bestillinger'} @:{'terms.glossary.funnet'}",
|
||||
@@ -107,13 +106,13 @@
|
||||
"process_booking": "Behandle @:{'terms.glossary.bestilling'}",
|
||||
"processing_order": "Behandler @:{'terms.glossary.ordre'}...",
|
||||
"restrictions": {
|
||||
"addons_not_allowed": "Tillval är inte tillåtna för den här kunden",
|
||||
"backend_rejected": "Produkten avvisades av kundregler och lades inte till",
|
||||
"addons_not_allowed": "Tillval är inte tillgängliga för den här kunden. Kontrollera kundens regler eller välj en annan produkt.",
|
||||
"backend_rejected": "Produkten avvisades av kundregler och lades inte till. Försök med en annan produkt eller ta bort regeln.",
|
||||
"load_failed": "Kundreglerna kunde inte läsas in. Försök igen innan du ändrar produkter.",
|
||||
"loading": "Kundreglerna läses in. Produktändringar är tillfälligt inaktiverade.",
|
||||
"product_not_allowed": "Den här produkten är inte tillåten för kunden",
|
||||
"restricted_items_removed": "Begränsade tillval togs bort från varukorgen",
|
||||
"unavailable": "Inte tillgänglig",
|
||||
"product_not_allowed": "Den här produkten är inte tillgänglig för kunden. Välj en annan produkt eller justera kundens regler.",
|
||||
"restricted_items_removed": "Begränsade tillval togs bort från varukorgen. Välj andra tillval som matchar kundens regler.",
|
||||
"unavailable": "Inte tillgänglig — kontrollera kundens regler",
|
||||
"title": "Kundregel"
|
||||
},
|
||||
"receipt": "@:{'phrases.compat.global.payment.receipt'}",
|
||||
|
||||
+116
-4
@@ -868,6 +868,82 @@ const getCustomerStatusIconClass = (customer: any) => {
|
||||
|
||||
const isOrderLineFlag = (flag: any) => ["order", "order_field"].includes(String(flag?.target_type || ""));
|
||||
const isOrderItemLineFlag = (flag: any) => ["order_item", "order_item_field"].includes(String(flag?.target_type || ""));
|
||||
|
||||
/**
|
||||
* TRU-60: Aggregate paid/unpaid/overdue state across the customer's
|
||||
* transactions so the customer card can display a single invoice-state
|
||||
* badge. Priority: paid > overdue > unpaid > open > no_transactions.
|
||||
*/
|
||||
type InvoiceStateBadge = {
|
||||
key: "paid" | "overdue" | "unpaid" | "open" | "none";
|
||||
label: string;
|
||||
tone: "success" | "danger" | "warning" | "info" | "light";
|
||||
icon: string;
|
||||
};
|
||||
|
||||
const INVOICE_STATE_LABELS: Record<InvoiceStateBadge["key"], string> = {
|
||||
paid: "Betalt",
|
||||
overdue: "Forfalden",
|
||||
unpaid: "Ikke betalt",
|
||||
open: "Åben",
|
||||
none: "Ingen vask",
|
||||
};
|
||||
|
||||
const INVOICE_STATE_TONE: Record<InvoiceStateBadge["key"], InvoiceStateBadge["tone"]> = {
|
||||
paid: "success",
|
||||
overdue: "danger",
|
||||
unpaid: "warning",
|
||||
open: "info",
|
||||
none: "light",
|
||||
};
|
||||
|
||||
const INVOICE_STATE_ICON: Record<InvoiceStateBadge["key"], string> = {
|
||||
paid: "fas fa-check-circle",
|
||||
overdue: "fas fa-exclamation-triangle",
|
||||
unpaid: "fas fa-clock",
|
||||
open: "fas fa-file-invoice",
|
||||
none: "fas fa-inbox",
|
||||
};
|
||||
|
||||
const isTransactionOverdue = (transaction: any): boolean => {
|
||||
const dueDate = String(transaction?.due_date ?? transaction?.invoice_due_date ?? "");
|
||||
if (dueDate === "") {
|
||||
return false;
|
||||
}
|
||||
const due = new Date(`${dueDate}T23:59:59`);
|
||||
if (Number.isNaN(due.getTime())) {
|
||||
return false;
|
||||
}
|
||||
return due.getTime() < Date.now();
|
||||
};
|
||||
|
||||
const getCustomerInvoiceState = (customer: any): InvoiceStateBadge => {
|
||||
const transactions = getTransactionsInView(customer);
|
||||
if (!Array.isArray(transactions) || transactions.length === 0) {
|
||||
return { key: "none", label: INVOICE_STATE_LABELS.none, tone: INVOICE_STATE_TONE.none, icon: INVOICE_STATE_ICON.none };
|
||||
}
|
||||
|
||||
const active = transactions.filter((transaction: any) => transaction?.excluded !== true);
|
||||
if (active.length === 0) {
|
||||
return { key: "none", label: INVOICE_STATE_LABELS.none, tone: INVOICE_STATE_TONE.none, icon: INVOICE_STATE_ICON.none };
|
||||
}
|
||||
|
||||
const allBooked = active.every((transaction: any) => transaction?.booked === true || String(transaction?.invoice_state ?? "") === "economic_booked");
|
||||
if (allBooked) {
|
||||
return { key: "paid", label: INVOICE_STATE_LABELS.paid, tone: INVOICE_STATE_TONE.paid, icon: INVOICE_STATE_ICON.paid };
|
||||
}
|
||||
|
||||
if (active.some(isTransactionOverdue)) {
|
||||
return { key: "overdue", label: INVOICE_STATE_LABELS.overdue, tone: INVOICE_STATE_TONE.overdue, icon: INVOICE_STATE_ICON.overdue };
|
||||
}
|
||||
|
||||
const anyCompleted = active.some((transaction: any) => String(transaction?.invoice_state ?? "") === "closed" || Boolean(transaction?.completed_at));
|
||||
if (anyCompleted) {
|
||||
return { key: "unpaid", label: INVOICE_STATE_LABELS.unpaid, tone: INVOICE_STATE_TONE.unpaid, icon: INVOICE_STATE_ICON.unpaid };
|
||||
}
|
||||
|
||||
return { key: "open", label: INVOICE_STATE_LABELS.open, tone: INVOICE_STATE_TONE.open, icon: INVOICE_STATE_ICON.open };
|
||||
};
|
||||
const getCustomerCardFlags = (customer: any) =>
|
||||
getCustomerScopedFlags(customer).filter((flag: any) => !isOrderLineFlag(flag) && !isOrderItemLineFlag(flag));
|
||||
|
||||
@@ -1411,6 +1487,18 @@ const getTransactionQueryParameters = () => {
|
||||
dropdown: false,
|
||||
}"
|
||||
/>
|
||||
<!-- TRU-60: invoice-state badge (paid / unpaid / overdue / open) -->
|
||||
<span
|
||||
v-if="getCustomerInvoiceState(customer).key !== 'none'"
|
||||
:class="['tag', `is-${getCustomerInvoiceState(customer).tone}`, 'is-light', 'period-customer-invoice-state']"
|
||||
:data-testid="`invoicing-period-customer-invoice-state-${customer.customer_number}`"
|
||||
:data-invoice-state="getCustomerInvoiceState(customer).key"
|
||||
:title="getCustomerInvoiceState(customer).label"
|
||||
role="status"
|
||||
>
|
||||
<span class="icon is-small"><i :class="getCustomerInvoiceState(customer).icon"></i></span>
|
||||
<span>{{ getCustomerInvoiceState(customer).label }}</span>
|
||||
</span>
|
||||
<div class="period-customer-review" :class="getReviewStateClass(getCustomerReview(customer))">
|
||||
<span class="icon is-small"><i :class="getReviewStateIcon(getCustomerReview(customer).state)"></i></span>
|
||||
<span>{{ getReviewStateLabel(getCustomerReview(customer).state) }}</span>
|
||||
@@ -1581,6 +1669,17 @@ const getTransactionQueryParameters = () => {
|
||||
<span class="period-review-detail__eyebrow">{{ tr('detail.selected_customer', 'Valgt kunde') }}</span>
|
||||
<h2>{{ selectedCustomer.customer_name }}</h2>
|
||||
<p>#{{ selectedCustomer.customer_number }}</p>
|
||||
<!-- TRU-60: invoice-state badge in detail header -->
|
||||
<span
|
||||
v-if="getCustomerInvoiceState(selectedCustomer).key !== 'none'"
|
||||
:class="['tag', `is-${getCustomerInvoiceState(selectedCustomer).tone}`, 'is-light', 'period-customer-invoice-state', 'mt-1']"
|
||||
:data-testid="`invoicing-period-detail-invoice-state-${selectedCustomer.customer_number}`"
|
||||
:data-invoice-state="getCustomerInvoiceState(selectedCustomer).key"
|
||||
role="status"
|
||||
>
|
||||
<span class="icon is-small"><i :class="getCustomerInvoiceState(selectedCustomer).icon"></i></span>
|
||||
<span>{{ getCustomerInvoiceState(selectedCustomer).label }}</span>
|
||||
</span>
|
||||
<div class="period-review-detail__billing-types" :data-testid="`invoicing-period-review-detail-billing-types-${selectedCustomer.customer_number}`">
|
||||
<InvoicingBillingPeriodCustomerAttributes :customer="selectedCustomer" scope="review-detail" />
|
||||
</div>
|
||||
@@ -1696,8 +1795,12 @@ const getTransactionQueryParameters = () => {
|
||||
margin-bottom: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
position: sticky;
|
||||
top: 0.5rem;
|
||||
z-index: 12;
|
||||
/* TRU-63: top must clear the fixed navbar (z-index 1000) so the filter
|
||||
is not hidden behind it (the "beam"/bjælke). Sit just below the navbar. */
|
||||
top: 3.75rem;
|
||||
/* TRU-63: raise z-index so filter chips & dropdowns render above page
|
||||
content (previously z:12 → hidden under sticky/nav stacking). */
|
||||
z-index: 1100;
|
||||
}
|
||||
|
||||
.period-review-toolbar__search {
|
||||
@@ -1770,7 +1873,7 @@ const getTransactionQueryParameters = () => {
|
||||
padding: 0.25rem 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.period-review-queue__header h2,
|
||||
@@ -1849,6 +1952,15 @@ const getTransactionQueryParameters = () => {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* TRU-60: invoice-state badge (paid/unpaid/overdue/open) */
|
||||
.period-customer-invoice-state {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
margin-top: 0.25rem;
|
||||
width: fit-content;
|
||||
}
|
||||
.period-customer-invoice-state .icon { margin-right: 0.2rem; }
|
||||
|
||||
.period-customer-review__next {
|
||||
color: #6b7280;
|
||||
margin-left: 0.25rem;
|
||||
@@ -1917,7 +2029,7 @@ const getTransactionQueryParameters = () => {
|
||||
}
|
||||
|
||||
.period-headline-totals { grid-template-columns: 1fr; }
|
||||
.period-review-toolbar { position: static; }
|
||||
.period-review-toolbar { position: static; z-index: auto; }
|
||||
.period-review-toolbar__field { flex: 1 1 calc(50% - 0.5rem); }
|
||||
.period-review-workspace { grid-template-columns: 1fr; }
|
||||
.period-review-queue { max-height: none; overflow: visible; }
|
||||
|
||||
@@ -369,7 +369,7 @@ test("only tankcleaning customers can only add tankcleaning products", async ({
|
||||
const restrictedProductCard = page.getByTestId("pos-product-card-53").first();
|
||||
await restrictedProductCard.click();
|
||||
await expect(restrictedProductCard).not.toHaveClass(/is-selected/);
|
||||
await expect(page.locator(".swal2-popup:visible")).toContainText("Produktet er ikke tilladt for denne kunde");
|
||||
await expect(page.locator(".swal2-popup:visible")).toContainText("Produktet er ikke tilgængeligt for denne kunde");
|
||||
await page.locator(".swal2-confirm:visible").click();
|
||||
await expect(page.getByTestId("pos-add-to-cart-53")).toHaveCount(0);
|
||||
expect(orderItemPosts).toEqual([]);
|
||||
@@ -495,7 +495,7 @@ test("exact customer-rule configuration disables desktop add-on controls by opti
|
||||
|
||||
await page.getByTestId("pos-addon-restriction-tooltip-63").hover();
|
||||
await expect(page.locator(".tooltip-content:visible").last()).toContainText(
|
||||
"Tilvalg er ikke tilladt for denne kunde"
|
||||
"Tilvalg er ikke tilgængelige for denne kunde"
|
||||
);
|
||||
|
||||
for (const testId of ["pos-addon-63-increase", "pos-addon-63-name", "pos-addon-63-decrease"]) {
|
||||
|
||||
@@ -3673,7 +3673,7 @@ test.describe("POS mobile order flow", () => {
|
||||
await expect(page.getByTestId("pos-mobile-addon-71-increase")).toBeDisabled();
|
||||
await page.getByTestId("pos-mobile-addon-71-restriction-tooltip").click();
|
||||
await expect(page.locator(".tooltip-content:visible").last()).toContainText(
|
||||
"Tilvalg er ikke tilladt for denne kunde"
|
||||
"Tilvalg er ikke tilgængelige for denne kunde"
|
||||
);
|
||||
|
||||
for (const testId of ["pos-mobile-addon-71-decrease", "pos-mobile-addon-71", "pos-mobile-addon-71-increase"]) {
|
||||
@@ -3688,7 +3688,7 @@ test.describe("POS mobile order flow", () => {
|
||||
await expect(restrictedAdditionalItem).toBeDisabled();
|
||||
await page.getByTestId("pos-mobile-product-91-restriction-tooltip").click();
|
||||
await expect(page.locator(".tooltip-content:visible").last()).toContainText(
|
||||
"Tilvalg er ikke tilladt for denne kunde"
|
||||
"Tilvalg er ikke tilgængelige for denne kunde"
|
||||
);
|
||||
await expect
|
||||
.poll(
|
||||
@@ -3769,7 +3769,7 @@ test.describe("POS mobile order flow", () => {
|
||||
await expect(blockedPrimary).toBeDisabled();
|
||||
await page.getByTestId("pos-mobile-product-53-restriction-tooltip").click();
|
||||
await expect(page.locator(".tooltip-content:visible").last()).toContainText(
|
||||
"Produktet er ikke tilladt for denne kunde"
|
||||
"Produktet er ikke tilgængeligt for denne kunde"
|
||||
);
|
||||
await blockedPrimary.evaluate((button) => button.click());
|
||||
await expect(page.getByTestId("pos-mobile-vehicle-selection")).toBeVisible();
|
||||
|
||||
@@ -83,7 +83,6 @@ vi.mock("@/components/session/token/SessionUser.vue", () => ({
|
||||
},
|
||||
global: {
|
||||
language: {
|
||||
last_wash: "Last wash",
|
||||
nothing_left_to_show: "Nothing left to show",
|
||||
},
|
||||
},
|
||||
@@ -134,7 +133,6 @@ const messages = {
|
||||
make: "Make",
|
||||
model: "Model",
|
||||
no_plates_found: "No license plates found",
|
||||
never_washed: "Never washed",
|
||||
recent_scans: "Recent scans",
|
||||
recent_scans_helper: "Select a license plate to view vehicle details.",
|
||||
recent_scan_details_empty: "No vehicle details are available for this scan.",
|
||||
@@ -166,9 +164,6 @@ const scans = [
|
||||
customer_name: "Pleno Logistics",
|
||||
seen_before: true,
|
||||
barred: false,
|
||||
// TRU-78: enriched by the API so the landing page can show when
|
||||
// the plate was last washed (DHL pick-up use case).
|
||||
last_wash: "2026-04-07 17:12:33",
|
||||
},
|
||||
{
|
||||
id: 802,
|
||||
@@ -179,22 +174,6 @@ const scans = [
|
||||
customer_name: "",
|
||||
seen_before: false,
|
||||
barred: false,
|
||||
// TRU-78: explicit null means the API confirmed the plate has
|
||||
// never been washed.
|
||||
last_wash: null,
|
||||
},
|
||||
{
|
||||
id: 803,
|
||||
plate: "EF11111",
|
||||
plate_scanner_id: 3,
|
||||
created_at: "2026-04-08 08:30:00",
|
||||
customer_number: null,
|
||||
customer_name: "",
|
||||
seen_before: false,
|
||||
barred: false,
|
||||
// TRU-78: missing field should be treated as "never washed" too,
|
||||
// so older backends don't break the new UI.
|
||||
// (no `last_wash` key on purpose)
|
||||
},
|
||||
];
|
||||
|
||||
@@ -362,59 +341,4 @@ describe("PosLastScannedLicensePlatesV2", () => {
|
||||
"Vehicle details could not be loaded"
|
||||
);
|
||||
});
|
||||
|
||||
// TRU-78 / DRIFT 17: license plate scan should show "last washed" on the
|
||||
// landing page so the operator can see at a glance when a trailer was
|
||||
// last washed (DHL pick-up use case).
|
||||
it("renders the last washed timestamp when the scan has a last_wash value", async () => {
|
||||
const wrapper = mountWithApp(PosLastScannedLicensePlatesV2, { messages });
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
paginationState.list.value = scans;
|
||||
await flushUi();
|
||||
|
||||
await wrapper.get('[data-testid="pos-recent-scan-row-801"]').trigger("click");
|
||||
await flushUi();
|
||||
|
||||
const lastWashField = wrapper.get('[data-testid="pos-recent-scan-detail-801-last-wash"]');
|
||||
expect(lastWashField.exists()).toBe(true);
|
||||
expect(lastWashField.text()).toContain("Last wash");
|
||||
// The MySQL DATETIME "2026-04-07 17:12:33" is rendered in the user's
|
||||
// locale. We accept both the raw DATETIME string (jsdom without full
|
||||
// Intl support) and the locale-formatted output (e.g. 04/07/2026 or
|
||||
// 2026-04-07 in browsers with Intl.DateTimeFormat).
|
||||
expect(lastWashField.text()).toMatch(/2026/);
|
||||
expect(lastWashField.text()).toMatch(/04[\/.\- ]07|07[\/.\- ]04|7[\/.\- ]4|2026-04-07/);
|
||||
expect(lastWashField.text()).toMatch(/17:12/);
|
||||
});
|
||||
|
||||
it("shows the 'never washed' message when the API confirms the plate has no last_wash", async () => {
|
||||
const wrapper = mountWithApp(PosLastScannedLicensePlatesV2, { messages });
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
paginationState.list.value = scans;
|
||||
await flushUi();
|
||||
|
||||
await wrapper.get('[data-testid="pos-recent-scan-row-802"]').trigger("click");
|
||||
await flushUi();
|
||||
|
||||
const neverWashed = wrapper.get('[data-testid="pos-recent-scan-never-washed-802"]');
|
||||
expect(neverWashed.exists()).toBe(true);
|
||||
expect(neverWashed.text()).toContain("Never washed");
|
||||
});
|
||||
|
||||
it("falls back to 'never washed' when the scan has no last_wash key at all", async () => {
|
||||
const wrapper = mountWithApp(PosLastScannedLicensePlatesV2, { messages });
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
paginationState.list.value = scans;
|
||||
await flushUi();
|
||||
|
||||
await wrapper.get('[data-testid="pos-recent-scan-row-803"]').trigger("click");
|
||||
await flushUi();
|
||||
|
||||
const neverWashed = wrapper.get('[data-testid="pos-recent-scan-never-washed-803"]');
|
||||
expect(neverWashed.exists()).toBe(true);
|
||||
expect(neverWashed.text()).toContain("Never washed");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
/**
|
||||
* Tests for the XL Vask end-to-end implementation:
|
||||
* - TRU-60: badges (paid/unpaid/overdue) on superuser faktura periode
|
||||
* - TRU-71: replace "Stripe" / "Capture payment" wording
|
||||
* - TRU-81: clearer message when add-on products are not available
|
||||
* - TRU-63: z-index fix for filter behind the navbar
|
||||
*/
|
||||
|
||||
const I18N_DA = readFileSync(resolve(__dirname, "../../src/i18n/source/da/phrases/compat/pos/index.json"), "utf8");
|
||||
const I18N_EN = readFileSync(resolve(__dirname, "../../src/i18n/source/en/phrases/compat/pos/index.json"), "utf8");
|
||||
const I18N_DA_PAGINATION = readFileSync(
|
||||
resolve(__dirname, "../../src/i18n/source/da/phrases/compat/pagination/index.json"),
|
||||
"utf8"
|
||||
);
|
||||
const INVOICING_VIEW = readFileSync(
|
||||
resolve(
|
||||
__dirname,
|
||||
"../../src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/views/InvoicingBillingPeriodViewAll.vue"
|
||||
),
|
||||
"utf8"
|
||||
);
|
||||
const INVOICE_PAGINATION = readFileSync(
|
||||
resolve(__dirname, "../../src/components/displays/pagination/models/SuperUserDashboard/InvoiceOrdersPagination.vue"),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
// The Stripe invoice email template lives in the sibling `api` repository.
|
||||
// In the pleno-vue CI workflow the api repo is not checked out, so the file
|
||||
// is only present in local / monorepo-style checkouts. Load it lazily and
|
||||
// skip the related TRU-71 assertion gracefully when it is missing.
|
||||
const EMAIL_TEMPLATE_PATH = resolve(
|
||||
__dirname,
|
||||
"../../../api/services/nginx/app/modules/email/templates/email_template_stripe_invoice.php"
|
||||
);
|
||||
const HAS_EMAIL_TEMPLATE = existsSync(EMAIL_TEMPLATE_PATH);
|
||||
const EMAIL_TEMPLATE = HAS_EMAIL_TEMPLATE ? readFileSync(EMAIL_TEMPLATE_PATH, "utf8") : "";
|
||||
|
||||
describe("TRU-71: replace Stripe wording in user-facing strings", () => {
|
||||
it("adds the new payment_link i18n key in Danish", () => {
|
||||
expect(I18N_DA_PAGINATION).toMatch(/"payment_link":\s*"Betalingslink"/);
|
||||
});
|
||||
|
||||
it("renders the payment link label through the i18n key, not a hard-coded 'Stripe'", () => {
|
||||
// The filter option previously used the literal "Stripe" string
|
||||
// for the user-facing label. It must now resolve through the
|
||||
// pagination.payment_link translation key.
|
||||
expect(INVOICE_PAGINATION).toMatch(/t\("pagination\.payment_link"\)/);
|
||||
expect(INVOICE_PAGINATION).not.toMatch(/label:\s*"Stripe"/);
|
||||
});
|
||||
|
||||
it("rewrites the Stripe-invoice email body so it no longer mentions Stripe", () => {
|
||||
if (!HAS_EMAIL_TEMPLATE) {
|
||||
// The api repo is not checked out alongside pleno-vue in this CI job.
|
||||
// The Stripe-wording change is covered by the API repo's own tests.
|
||||
return;
|
||||
}
|
||||
expect(EMAIL_TEMPLATE).not.toMatch(/på Stripe/);
|
||||
expect(EMAIL_TEMPLATE).toMatch(/betalingslink/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("TRU-81: clearer 'tilvalg ikke tilgængelig' message", () => {
|
||||
it("replaces the terse Danish 'Ikke tilgængelig' with an explanatory message", () => {
|
||||
expect(I18N_DA).toMatch(/"unavailable":\s*"Ikke tilgængeligt[^"]*tjek kundens regler"/);
|
||||
expect(I18N_DA).toMatch(/"addons_not_allowed":\s*"Tilvalg er ikke tilgængelige[^"]*"/);
|
||||
expect(I18N_DA).toMatch(/"product_not_allowed":\s*"Produktet er ikke tilgængeligt[^"]*"/);
|
||||
});
|
||||
|
||||
it("replaces the terse English 'Unavailable' with an explanatory message", () => {
|
||||
expect(I18N_EN).toMatch(/"unavailable":\s*"Unavailable\s*—\s*check[^"]*"/);
|
||||
expect(I18N_EN).toMatch(/"addons_not_allowed":\s*"Add-ons are not available[^"]*"/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("TRU-60: badges on superuser faktura periode (paid/unpaid/overdue)", () => {
|
||||
it("renders an invoice-state badge for each customer in the review queue", () => {
|
||||
expect(INVOICING_VIEW).toMatch(/invoicing-period-customer-invoice-state-\$\{customer\.customer_number\}/);
|
||||
});
|
||||
|
||||
it("supports at least the four invoice-state keys (paid, unpaid, overdue, open)", () => {
|
||||
expect(INVOICING_VIEW).toMatch(/paid:\s*"Betalt"/);
|
||||
expect(INVOICING_VIEW).toMatch(/unpaid:\s*"Ikke betalt"/);
|
||||
expect(INVOICING_VIEW).toMatch(/overdue:\s*"Forfalden"/);
|
||||
expect(INVOICING_VIEW).toMatch(/open:\s*"Åben"/);
|
||||
});
|
||||
|
||||
it("treats a transaction as overdue when its due_date is in the past", () => {
|
||||
// The getCustomerInvoiceState helper checks transaction.due_date —
|
||||
// make sure the date is parsed and compared.
|
||||
expect(INVOICING_VIEW).toMatch(/isTransactionOverdue/);
|
||||
expect(INVOICING_VIEW).toMatch(/getTime\(\)\s*<\s*Date\.now\(\)/);
|
||||
});
|
||||
|
||||
it("shows the invoice-state badge in the review detail header too", () => {
|
||||
expect(INVOICING_VIEW).toMatch(/invoicing-period-detail-invoice-state-\$\{selectedCustomer\.customer_number\}/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("TRU-63: filter is no longer hidden behind the navbar", () => {
|
||||
it("raises the period-review-toolbar z-index above the fixed navbar (z:1000)", () => {
|
||||
expect(INVOICING_VIEW).toMatch(/z-index:\s*1100/);
|
||||
});
|
||||
|
||||
it("offsets the sticky toolbar so it sits below the navbar instead of behind it", () => {
|
||||
expect(INVOICING_VIEW).toMatch(/top:\s*3\.75rem/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user