Add unit tests and components for invoicing period flag handling and self-wash view rendering
This commit is contained in:
@@ -178,6 +178,233 @@ function normalizePositiveIntegerValue(value) {
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
||||
}
|
||||
|
||||
function normalizeReferenceKey(value) {
|
||||
return String(value ?? "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
function normalizeReferencePlate(value) {
|
||||
return String(value ?? "")
|
||||
.trim()
|
||||
.toUpperCase()
|
||||
.replace(/\s+/g, "");
|
||||
}
|
||||
|
||||
function referenceMatchScore(reference, search) {
|
||||
const referenceKey = normalizeReferenceKey(reference);
|
||||
const searchKey = normalizeReferenceKey(search);
|
||||
|
||||
if (!searchKey) {
|
||||
return 0;
|
||||
}
|
||||
if (referenceKey === searchKey) {
|
||||
return 1000;
|
||||
}
|
||||
if (referenceKey.startsWith(searchKey)) {
|
||||
return 600;
|
||||
}
|
||||
if (referenceKey.includes(searchKey)) {
|
||||
return 300;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function referenceSourceScore(source) {
|
||||
if (source === "booking") {
|
||||
return 30;
|
||||
}
|
||||
if (source === "order") {
|
||||
return 20;
|
||||
}
|
||||
if (source === "vehicle") {
|
||||
return 10;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function referenceContextBoost(row, customerId, plates) {
|
||||
let score = 0;
|
||||
if (customerId && Number(row.customer_id || 0) === Number(customerId)) {
|
||||
score += 80;
|
||||
}
|
||||
|
||||
if (referenceMatchesAnyPlate(row, plates)) {
|
||||
score += 90;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
function referenceMatchesAnyPlate(row, plates) {
|
||||
const rowPlates = [row.reg_1, row.reg_2, row.reg_3].map(normalizeReferencePlate).filter(Boolean);
|
||||
return plates.some((plate) => rowPlates.includes(plate));
|
||||
}
|
||||
|
||||
function referenceSection(row, customerId, plates) {
|
||||
if (referenceMatchesAnyPlate(row, plates)) {
|
||||
return "this_vehicle";
|
||||
}
|
||||
if (customerId && Number(row.customer_id || 0) === Number(customerId)) {
|
||||
return "other_customer_vehicle";
|
||||
}
|
||||
return "other";
|
||||
}
|
||||
|
||||
function referenceSectionScore(section) {
|
||||
if (section === "this_vehicle") {
|
||||
return 40;
|
||||
}
|
||||
if (section === "other_customer_vehicle") {
|
||||
return 20;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function buildReferenceSuggestions(
|
||||
posFixture,
|
||||
{ search = "", departmentId = null, customerId = null, plates = [], limit = 10 }
|
||||
) {
|
||||
const normalizedSearch = String(search || "").trim();
|
||||
const normalizedDepartmentId = Number(departmentId || 0);
|
||||
const normalizedCustomerId = Number(customerId || 0);
|
||||
const normalizedPlates = plates.map(normalizeReferencePlate).filter(Boolean);
|
||||
const rows = [];
|
||||
|
||||
for (const booking of posFixture.orderBookings || []) {
|
||||
const reference = String(booking.reference ?? booking.reference_number ?? "").trim();
|
||||
if (
|
||||
!reference ||
|
||||
booking.deleted_at ||
|
||||
Number(booking.department ?? booking.department_id ?? 0) !== normalizedDepartmentId
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (normalizedSearch && !normalizeReferenceKey(reference).includes(normalizeReferenceKey(normalizedSearch))) {
|
||||
continue;
|
||||
}
|
||||
rows.push({
|
||||
source: "booking",
|
||||
origin_id: Number(booking.id || 0),
|
||||
reference,
|
||||
source_created_at: booking.datetime || booking.created_at || "",
|
||||
used_at: booking.datetime || booking.created_at || "",
|
||||
customer_id: Number(booking.customer_number || booking.customer_id || 0),
|
||||
reg_1: booking.reg_1 || booking.regNr || "",
|
||||
reg_2: booking.reg_2 || booking.regNrTrailer || "",
|
||||
reg_3: booking.reg_3 || "",
|
||||
});
|
||||
}
|
||||
|
||||
for (const order of Object.values(posFixture.ordersById || {})) {
|
||||
const reference = String(order.reference || "").trim();
|
||||
if (!reference || order.deleted_at || Number(order.department_id || 0) !== normalizedDepartmentId) {
|
||||
continue;
|
||||
}
|
||||
if (normalizedSearch && !normalizeReferenceKey(reference).includes(normalizeReferenceKey(normalizedSearch))) {
|
||||
continue;
|
||||
}
|
||||
rows.push({
|
||||
source: "order",
|
||||
origin_id: Number(order.id || 0),
|
||||
reference,
|
||||
source_created_at: order.created_at || "",
|
||||
used_at: order.created_at || "",
|
||||
customer_id: Number(order.customer_id || 0),
|
||||
reg_1: order.reg_1 || "",
|
||||
reg_2: order.reg_2 || "",
|
||||
reg_3: order.reg_3 || "",
|
||||
});
|
||||
}
|
||||
|
||||
for (const vehicle of posFixture.vehicles || []) {
|
||||
const reference = String(vehicle.reference || "").trim();
|
||||
const vehiclePlate = normalizeReferencePlate(vehicle.reg);
|
||||
const matchesContext =
|
||||
(normalizedCustomerId && Number(vehicle.customer_id || 0) === normalizedCustomerId) ||
|
||||
(vehiclePlate && normalizedPlates.includes(vehiclePlate));
|
||||
if (!reference || vehicle.deleted_at || !matchesContext) {
|
||||
continue;
|
||||
}
|
||||
if (normalizedSearch && !normalizeReferenceKey(reference).includes(normalizeReferenceKey(normalizedSearch))) {
|
||||
continue;
|
||||
}
|
||||
rows.push({
|
||||
source: "vehicle",
|
||||
origin_id: Number(vehicle.id || 0),
|
||||
reference,
|
||||
source_created_at: vehicle.created_at || "",
|
||||
used_at: vehicle.created_at || "",
|
||||
customer_id: Number(vehicle.customer_id || 0),
|
||||
reg_1: vehicle.reg || "",
|
||||
reg_2: "",
|
||||
reg_3: "",
|
||||
});
|
||||
}
|
||||
|
||||
const groups = new Map();
|
||||
for (const row of rows) {
|
||||
const key = normalizeReferenceKey(row.reference);
|
||||
const existing = groups.get(key) || {
|
||||
reference: row.reference,
|
||||
rows: [],
|
||||
usage_count: 0,
|
||||
last_used_at: "",
|
||||
context_boost: 0,
|
||||
section: "other",
|
||||
};
|
||||
const rowSection = referenceSection(row, normalizedCustomerId, normalizedPlates);
|
||||
existing.rows.push(row);
|
||||
existing.usage_count += 1;
|
||||
existing.last_used_at =
|
||||
String(row.used_at || "") > String(existing.last_used_at || "") ? row.used_at || "" : existing.last_used_at;
|
||||
existing.context_boost = Math.max(
|
||||
existing.context_boost,
|
||||
referenceContextBoost(row, normalizedCustomerId, normalizedPlates)
|
||||
);
|
||||
existing.section =
|
||||
referenceSectionScore(rowSection) > referenceSectionScore(existing.section) ? rowSection : existing.section;
|
||||
groups.set(key, existing);
|
||||
}
|
||||
|
||||
return Array.from(groups.values())
|
||||
.map((group) => {
|
||||
const bestRow = [...group.rows].sort((left, right) => {
|
||||
return (
|
||||
referenceSourceScore(right.source) - referenceSourceScore(left.source) ||
|
||||
String(right.source_created_at || "").localeCompare(String(left.source_created_at || "")) ||
|
||||
Number(right.origin_id || 0) - Number(left.origin_id || 0)
|
||||
);
|
||||
})[0];
|
||||
const usageCount = Number(group.usage_count || 1);
|
||||
return {
|
||||
source: bestRow.source,
|
||||
section: group.section,
|
||||
reference: group.reference,
|
||||
source_created_at: bestRow.source_created_at || "",
|
||||
last_used_at: group.last_used_at || bestRow.used_at || "",
|
||||
usage_count: usageCount,
|
||||
origin_id: Number(bestRow.origin_id || 0),
|
||||
score:
|
||||
referenceMatchScore(group.reference, normalizedSearch) +
|
||||
Number(group.context_boost || 0) +
|
||||
referenceSectionScore(group.section) +
|
||||
referenceSourceScore(bestRow.source) +
|
||||
Math.min(usageCount, 20) * 5,
|
||||
};
|
||||
})
|
||||
.sort((left, right) => {
|
||||
return (
|
||||
Number(right.score || 0) - Number(left.score || 0) ||
|
||||
Number(right.usage_count || 0) - Number(left.usage_count || 0) ||
|
||||
referenceSectionScore(right.section) - referenceSectionScore(left.section) ||
|
||||
String(right.last_used_at || "").localeCompare(String(left.last_used_at || "")) ||
|
||||
String(left.reference || "").localeCompare(String(right.reference || ""))
|
||||
);
|
||||
})
|
||||
.slice(0, Math.max(1, Math.min(Number(limit || 10), 25)));
|
||||
}
|
||||
|
||||
function resolveCustomerNumberFromAttributeTarget(posFixture, target = {}) {
|
||||
const customerNumber = normalizePositiveIntegerValue(target.customer_number ?? target.customerNumber);
|
||||
if (customerNumber) {
|
||||
@@ -3149,6 +3376,22 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
|
||||
return true;
|
||||
}
|
||||
|
||||
if (pathname.endsWith("/orders/reference-suggestions") && method === "GET") {
|
||||
const suggestions = buildReferenceSuggestions(posFixture, {
|
||||
search: parsedUrl.searchParams.get("search") || "",
|
||||
departmentId: parsedUrl.searchParams.get("department_id") || null,
|
||||
customerId: parsedUrl.searchParams.get("customer_id") || null,
|
||||
plates: [
|
||||
parsedUrl.searchParams.get("reg_1") || "",
|
||||
parsedUrl.searchParams.get("reg_2") || "",
|
||||
parsedUrl.searchParams.get("reg_3") || "",
|
||||
],
|
||||
limit: parsedUrl.searchParams.get("limit") || 10,
|
||||
});
|
||||
await route.fulfill(json({ success: true, data: suggestions }));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (pathname.endsWith("/orders") && method === "GET") {
|
||||
const filters = String(parsedUrl.searchParams.get("filters") || "");
|
||||
const orders = filterPosOrders(posFixture, filters)
|
||||
|
||||
Reference in New Issue
Block a user