2268 lines
67 KiB
Vue
2268 lines
67 KiB
Vue
<script lang="ts">
|
|
import { ref, watch, computed, defineComponent } from "vue";
|
|
import type { PosVehicle } from "./PosVehicle.vue";
|
|
import type { PosProduct } from "./PosProduct.vue";
|
|
import type { PosCategory } from "./PosCategory.vue";
|
|
import type { PosPopup } from "./PosPopup.vue";
|
|
import type { PosOrder } from "./PosOrder.vue";
|
|
import type { Addon } from "@/components/displays/department/pos/steps/mobile/objects/PosAddon.vue";
|
|
import type { PosLocation } from "./PosLocation.vue";
|
|
import type { PosSoundEffect } from "./PosSoundEffect.vue";
|
|
import type { PosActionButton } from "./PosActionButton.vue";
|
|
import {
|
|
doesTransactionContainWashCertificateProduct,
|
|
WASH_CERTIFICATE_PRODUCT_ID,
|
|
} from "@/components/displays/department/pos/utils/washCertificate.js";
|
|
import { determineStatusKey, type VehicleStatusKey } from "./PosVehicleStatus.vue";
|
|
import i18n from "@/i18n";
|
|
|
|
/** Views */
|
|
// Step 1
|
|
const manualInput = ref(false);
|
|
const attachmentView = ref(false);
|
|
// Step 2
|
|
const vehicleSelection = ref(false);
|
|
const additionalItemSelection = ref(false);
|
|
// Transaction history view
|
|
const transactionHistoryView = ref(false);
|
|
// Is any view active?
|
|
const isAnyViewActive = computed(() => {
|
|
return manualInput.value || vehicleSelection.value || additionalItemSelection.value || transactionHistoryView.value;
|
|
});
|
|
const views = {
|
|
// Step 1: Manual Input of registration numbers.
|
|
manualInput,
|
|
attachmentView,
|
|
// Step 2: Vehicle Selection from a list.
|
|
vehicleSelection, // Used to select the "Primary" product for the transaction.
|
|
additionalItemSelection, // Used to select additional items for the transaction.
|
|
transactionHistoryView, // View the last transactions made on this device.
|
|
isAnyActive: isAnyViewActive,
|
|
};
|
|
/** Sound effects */
|
|
const soundEffects = ref<PosSoundEffect[]>([]);
|
|
const soundEffectsEnabled = ref<boolean>(true);
|
|
const playSoundEffect = (soundEffect: PosSoundEffect) => {
|
|
if (!soundEffectsEnabled.value) return;
|
|
const audio = new Audio(soundEffect.src);
|
|
audio.volume = soundEffect.volume ?? 1.0;
|
|
audio.play().catch((error) => {
|
|
console.error("Error playing sound effect:", error);
|
|
});
|
|
};
|
|
const toggleSoundEffects = () => {
|
|
soundEffectsEnabled.value = !soundEffectsEnabled.value;
|
|
};
|
|
const setSoundEffects = (enabled: boolean) => {
|
|
soundEffectsEnabled.value = enabled;
|
|
};
|
|
const getSoundEffects = () => {
|
|
return soundEffects.value;
|
|
};
|
|
const addSoundEffect = (soundEffect: PosSoundEffect) => {
|
|
soundEffects.value.push(soundEffect);
|
|
};
|
|
const clearSoundEffects = () => {
|
|
soundEffects.value = [];
|
|
};
|
|
const defaultSoundEffects = ref<{ [key: string]: PosSoundEffect }>({
|
|
onAfterSuccessfulScan: {
|
|
id: "success",
|
|
src: "/sounds/success.mp3",
|
|
volume: 0.5,
|
|
pitch: 1.0,
|
|
delay: 0,
|
|
loop: false,
|
|
},
|
|
});
|
|
const listDefaultSoundEffects = computed(() => defaultSoundEffects.value);
|
|
const sounds = {
|
|
soundEffects,
|
|
enabled: soundEffectsEnabled,
|
|
play: playSoundEffect, // example: sounds.play(sounds.list.value.onAfterSuccessfulScan)
|
|
toggle: toggleSoundEffects,
|
|
set: setSoundEffects,
|
|
get: getSoundEffects,
|
|
add: addSoundEffect,
|
|
clear: clearSoundEffects,
|
|
list: listDefaultSoundEffects,
|
|
};
|
|
/** Device location */
|
|
const location = ref<PosLocation | null>(null);
|
|
const locationTimeout = ref<number>(5000); // Timeout for location retrieval in milliseconds
|
|
|
|
const setLocation = (newLocation: PosLocation | null) => {
|
|
location.value = newLocation;
|
|
};
|
|
const getLocation = () => {
|
|
return location.value;
|
|
};
|
|
const clearLocation = () => {
|
|
location.value = null;
|
|
};
|
|
type CoordinatePair = {
|
|
latitude?: number | string | null;
|
|
longitude?: number | string | null;
|
|
};
|
|
|
|
export const normalizeCoordinatePair = (
|
|
coordinates: CoordinatePair | null | undefined,
|
|
{ allowZeroPair = true }: { allowZeroPair?: boolean } = {}
|
|
): { latitude: number; longitude: number } | null => {
|
|
const latitude = Number(coordinates?.latitude);
|
|
const longitude = Number(coordinates?.longitude);
|
|
|
|
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
|
|
return null;
|
|
}
|
|
|
|
if (latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180) {
|
|
return null;
|
|
}
|
|
|
|
if (!allowZeroPair && latitude === 0 && longitude === 0) {
|
|
return null;
|
|
}
|
|
|
|
return { latitude, longitude };
|
|
};
|
|
|
|
export const hasValidCoordinatePair = (
|
|
coordinates: CoordinatePair | null | undefined,
|
|
options: { allowZeroPair?: boolean } = {}
|
|
): boolean => normalizeCoordinatePair(coordinates, options) !== null;
|
|
|
|
// Get distance in kilometers between two locations
|
|
const getDistance = (
|
|
from: CoordinatePair,
|
|
to: CoordinatePair
|
|
): number => {
|
|
const normalizedFrom = normalizeCoordinatePair(from);
|
|
const normalizedTo = normalizeCoordinatePair(to);
|
|
|
|
if (!normalizedFrom || !normalizedTo) {
|
|
return Number.POSITIVE_INFINITY;
|
|
}
|
|
|
|
const toRad = (value: number) => (value * Math.PI) / 180;
|
|
|
|
const R = 6371; // Radius of the Earth in kilometers
|
|
const dLat = toRad(normalizedTo.latitude - normalizedFrom.latitude);
|
|
const dLon = toRad(normalizedTo.longitude - normalizedFrom.longitude);
|
|
const lat1 = toRad(normalizedFrom.latitude);
|
|
const lat2 = toRad(normalizedTo.latitude);
|
|
|
|
const a =
|
|
Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2);
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
return R * c; // Distance in kilometers
|
|
};
|
|
|
|
const locations = {
|
|
location,
|
|
set: setLocation,
|
|
get: getLocation,
|
|
clear: clearLocation,
|
|
normalizeCoordinatePair,
|
|
hasValidCoordinatePair,
|
|
getDistance,
|
|
defaultTimeout: locationTimeout,
|
|
};
|
|
/** Action buttons */
|
|
const defaultActionButtons = computed<{ [key: string]: PosActionButton }>(() => {
|
|
const t = i18n.global.t;
|
|
return {
|
|
/** General purpose action buttons (used by PosAddons)*/
|
|
confirm: {
|
|
label: t("common.confirm"),
|
|
description: t("admin.pos.action_buttons.confirm_desc"),
|
|
onClick: () => {
|
|
clearPopup(); // Close the popup
|
|
},
|
|
color: "primary",
|
|
},
|
|
cancel: {
|
|
label: t("common.cancel"),
|
|
description: t("admin.pos.action_buttons.cancel_desc"),
|
|
onClick: () => {
|
|
clearPopup(); // Close the popup
|
|
},
|
|
color: "light",
|
|
},
|
|
close: {
|
|
label: t("common.close"),
|
|
description: t("admin.pos.action_buttons.close_desc"),
|
|
onClick: () => {
|
|
clearPopup(); // Close the popup
|
|
},
|
|
color: "light",
|
|
},
|
|
/** Customer selection action buttons */
|
|
selectCustomer: {
|
|
label: t("admin.pos.action_buttons.select_customer"),
|
|
description: t("admin.pos.action_buttons.select_customer_desc"),
|
|
onClick: () => {
|
|
// Open the select customer popup
|
|
popups.select("select_customer");
|
|
},
|
|
color: "primary",
|
|
},
|
|
addCustomer: {
|
|
label: t("admin.pos.action_buttons.add_customer"),
|
|
description: t("admin.pos.action_buttons.add_customer_desc"),
|
|
onClick: () => {
|
|
// Open the add customer popup
|
|
popups.select("add_customer", { props: { error: null, canCreate: true } });
|
|
},
|
|
color: "primary",
|
|
},
|
|
editReference: {
|
|
label: t("admin.pos.action_buttons.edit_reference"),
|
|
description: t("admin.pos.action_buttons.edit_reference_desc"),
|
|
onClick: () => {
|
|
// Open the edit reference popup
|
|
//popups.select('edit_reference');
|
|
},
|
|
color: "primary",
|
|
},
|
|
addNote: {
|
|
label: t("admin.pos.action_buttons.add_note"),
|
|
description: t("admin.pos.action_buttons.add_note_desc"),
|
|
onClick: () => {
|
|
// Open the add note popup
|
|
popups.select("customer_notes", { props: { showInput: true, showNotes: false } });
|
|
},
|
|
color: "primary",
|
|
},
|
|
};
|
|
});
|
|
// Call action buttons object for use in other components
|
|
const actionButtons = {
|
|
default: defaultActionButtons,
|
|
};
|
|
/** Popups */
|
|
const popup = ref<PosPopup | null>(null);
|
|
// Function to check if a popup is currently set
|
|
const isPopupSet = computed(() => {
|
|
return popup.value !== null;
|
|
});
|
|
// Function to get the current popup
|
|
const getPopup = () => {
|
|
if (popup.value) {
|
|
return popup.value;
|
|
}
|
|
return null;
|
|
};
|
|
// Function to set the popup
|
|
const setPopup = (newPopup: PosPopup | null) => {
|
|
popup.value = newPopup;
|
|
};
|
|
// Function to clear the popup
|
|
const clearPopup = () => {
|
|
popup.value = null;
|
|
};
|
|
// Default list of popups
|
|
/** Popups list */
|
|
const popupsList = ref<{ [key: string]: { value: PosPopup } }>({});
|
|
// Function to add a new popup to the list
|
|
const addPopup = (popup: PosPopup) => {
|
|
if (!popup.id) {
|
|
console.warn("Popup must have an id to be added to the list.");
|
|
return;
|
|
}
|
|
popupsList.value[popup.id] = { value: popup };
|
|
};
|
|
// Function to add default popups to the list
|
|
const addDefaultPopups = () => {
|
|
// Select customer
|
|
addPopup({
|
|
id: "select_customer",
|
|
title: "Vælg Kunde",
|
|
message: "Bekræft venligst valget af kunde til denne transaktion",
|
|
component: "select_customer",
|
|
showHeaderClose: true,
|
|
headerCloseTestId: "pos-mobile-select-customer-header-close",
|
|
style: { maxHeight: "72dvh" },
|
|
actionButtons: [
|
|
{ ...defaultActionButtons.value.cancel },
|
|
{ ...defaultActionButtons.value.addCustomer, label: "Ny kunde" },
|
|
],
|
|
});
|
|
addPopup({
|
|
id: "select_order_booking",
|
|
title: i18n.global.t("admin.pos.order_booking_selector.title"),
|
|
message: i18n.global.t("admin.pos.order_booking_selector.help_text"),
|
|
component: "select_order_booking",
|
|
showHeaderClose: true,
|
|
headerCloseTestId: "pos-mobile-order-booking-header-close",
|
|
style: { maxHeight: "72dvh" },
|
|
actionButtons: [],
|
|
});
|
|
// Completed
|
|
addPopup({
|
|
id: "completed_transaction",
|
|
title: "Gennemført",
|
|
message: "Transaktionen er gennemført!",
|
|
component: "completed_transaction",
|
|
hideHeader: true,
|
|
});
|
|
// Completed
|
|
addPopup({
|
|
id: "complete_booking",
|
|
title: "Fuldført",
|
|
message: "Bookingen er nu fuldført!",
|
|
component: "complete_booking",
|
|
hideHeader: true,
|
|
actionButtons: [],
|
|
});
|
|
// Error
|
|
addPopup({
|
|
id: "error",
|
|
title: "Fejl",
|
|
message: "Der opstod en fejl under behandlingen af din anmodning. Prøv venligst igen.",
|
|
component: "error",
|
|
hideHeader: true,
|
|
actionButtons: [{ ...defaultActionButtons.value.close }],
|
|
});
|
|
// Add product note
|
|
addPopup({
|
|
id: "add_product_note",
|
|
title: "Tilføj note",
|
|
message: "Tilføj venligst en note for produktet",
|
|
component: "add_product_note", // This component should handle input and return the notes
|
|
hideHeader: true,
|
|
actionButtons: [{ ...defaultActionButtons.value.cancel }],
|
|
});
|
|
// Select vehicle
|
|
addPopup({
|
|
id: "select_vehicle",
|
|
title: "Vælg køretøj",
|
|
message: "Vælg venligst et køretøj til denne transaktion",
|
|
component: "select_vehicle", // This component should handle vehicle selection
|
|
showHeaderClose: true,
|
|
headerCloseTestId: "pos-mobile-select-vehicle-header-close",
|
|
style: { maxHeight: "72dvh" },
|
|
actionButtons: [{ ...defaultActionButtons.value.confirm }, { ...defaultActionButtons.value.close }],
|
|
});
|
|
// Change reference
|
|
addPopup({
|
|
id: "change_reference",
|
|
title: "Indtast reference",
|
|
message: "Indtast venligst en reference for denne transaktion",
|
|
component: "change_reference",
|
|
showHeaderClose: true,
|
|
headerCloseTestId: "pos-mobile-change-reference-header-close",
|
|
style: { maxHeight: "40vh" },
|
|
actionButtons: [{ ...defaultActionButtons.value.confirm }, { ...defaultActionButtons.value.close }],
|
|
});
|
|
// Customer notes
|
|
addPopup({
|
|
id: "customer_notes",
|
|
title: "Kundebemærkninger",
|
|
message: "Læs venligst kundens bemærkninger",
|
|
component: "customer_notes",
|
|
showHeaderClose: true,
|
|
headerCloseTestId: "pos-mobile-customer-notes-header-close",
|
|
style: { maxHeight: "50vh" },
|
|
actionButtons: [{ ...defaultActionButtons.value.addNote }, { ...defaultActionButtons.value.close }],
|
|
props: { showInput: false, showNotes: true },
|
|
});
|
|
// Image viewer
|
|
addPopup({
|
|
id: "image",
|
|
title: "Billede",
|
|
message: "",
|
|
component: "image_viewer",
|
|
showHeaderClose: true,
|
|
headerCloseTestId: "pos-mobile-image-viewer-header-close",
|
|
style: { maxHeight: "50vh" },
|
|
actionButtons: [{ ...defaultActionButtons.value.close }],
|
|
props: { base64String: null, filename: null },
|
|
});
|
|
// Add customer
|
|
addPopup({
|
|
id: "add_customer",
|
|
title: "Tilføj kunde",
|
|
message: "Indtast venligst oplysninger for den nye kunde",
|
|
component: "add_customer",
|
|
showHeaderClose: true,
|
|
headerCloseTestId: "pos-mobile-add-customer-header-close",
|
|
style: { maxHeight: "60vh" },
|
|
actionButtons: [{ ...defaultActionButtons.value.cancel }],
|
|
props: {
|
|
cvr: null,
|
|
contactEmail: null,
|
|
contactPhone: null,
|
|
companyPhone: null,
|
|
invoiceEmail: null,
|
|
searchResult: null,
|
|
canCreate: false,
|
|
error: null,
|
|
},
|
|
});
|
|
};
|
|
// Function to check if a popup is defined in the list
|
|
const isPopupDefined = (id: string): boolean => {
|
|
return !!popupsList.value[id];
|
|
};
|
|
// Function to get a popup by its key
|
|
const getPopupByKey = (key: string): PosPopup | null => {
|
|
const popupItem = popupsList.value[key];
|
|
if (popupItem) {
|
|
return popupItem.value;
|
|
}
|
|
console.warn(`Popup with key "${key}" not found.`);
|
|
return null;
|
|
};
|
|
// Function to remove a popup by its key
|
|
const removePopupByKey = (key: string): boolean => {
|
|
if (popupsList.value[key]) {
|
|
delete popupsList.value[key];
|
|
return true;
|
|
}
|
|
console.warn(`Popup with key "${key}" not found.`);
|
|
return false;
|
|
};
|
|
// Function to clear the popups list
|
|
const clearPopupsList = () => {
|
|
popupsList.value = {};
|
|
};
|
|
// Function to select a popup by its ID
|
|
const selectPopupById = (id: string, modifications?: Partial<PosPopup>): PosPopup | null => {
|
|
const popupItem = popupsList.value[id];
|
|
if (popupItem) {
|
|
setPopup({
|
|
...popupItem.value,
|
|
...modifications, // Apply any modifications if provided
|
|
});
|
|
// Optionally, you can also return the popup
|
|
return <PosPopup>{
|
|
...popupItem.value,
|
|
...modifications, // Apply any modifications if provided
|
|
};
|
|
}
|
|
console.warn(`Popup with id "${id}" not found.`);
|
|
return null;
|
|
};
|
|
|
|
// Add default popups to the list on module load
|
|
addDefaultPopups();
|
|
|
|
// Exporting the popups object for use in other components
|
|
const popups = {
|
|
// Popup-related functions
|
|
popup,
|
|
isSet: isPopupSet,
|
|
get: getPopup,
|
|
set: setPopup,
|
|
clear: clearPopup,
|
|
select: selectPopupById,
|
|
// Popups list-related functions
|
|
list: popupsList,
|
|
exists: isPopupDefined,
|
|
getByKey: getPopupByKey,
|
|
removeByKey: removePopupByKey,
|
|
clearList: clearPopupsList,
|
|
add: addPopup,
|
|
};
|
|
/** Vehicles */
|
|
// Define reactive references for vehicle registrations
|
|
const vehicle_1 = ref<PosVehicle | null>(null);
|
|
const vehicle_2 = ref<PosVehicle | null>(null);
|
|
const vehicle_3 = ref<PosVehicle | null>(null);
|
|
// Active vehicle reference to track the currently selected vehicle
|
|
const activeVehicleIndex = ref<number>(1);
|
|
// Function to set the active vehicle
|
|
const setActiveVehicleIndex = (index: number): void => {
|
|
activeVehicleIndex.value = index;
|
|
};
|
|
|
|
const normalizeVehicleStatus = (vehicle: PosVehicle): VehicleStatusKey => {
|
|
if (typeof vehicle.status === "string" && vehicle.status.length > 0) {
|
|
return vehicle.status as VehicleStatusKey;
|
|
}
|
|
|
|
return determineStatusKey({
|
|
hasBooking: Array.isArray(vehicle.booking_matches) ? vehicle.booking_matches.length > 0 : !!vehicle.booking_id,
|
|
customer_id: vehicle.customer_id,
|
|
customer_name: vehicle.customer_name,
|
|
barred: vehicle.barred,
|
|
wash_subscription: vehicle.wash_subscription,
|
|
});
|
|
};
|
|
|
|
const normalizeVehicleSelection = (vehicle: PosVehicle | null): PosVehicle | null => {
|
|
if (!vehicle) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
...vehicle,
|
|
reg: String(vehicle.reg ?? ""),
|
|
status: normalizeVehicleStatus(vehicle),
|
|
};
|
|
};
|
|
|
|
// Function to select a vehicle
|
|
const selectVehicle = (index: number, vehicle: PosVehicle | null) => {
|
|
const normalizedVehicle = normalizeVehicleSelection(vehicle);
|
|
switch (index) {
|
|
case 1:
|
|
vehicle_1.value = normalizedVehicle;
|
|
// If the vehicle has a reference, and it is currently not set, set it
|
|
if (normalizedVehicle?.reference && !metadata.getReference()) {
|
|
metadata.setReference(normalizedVehicle.reference);
|
|
}
|
|
break;
|
|
case 2:
|
|
vehicle_2.value = normalizedVehicle;
|
|
break;
|
|
case 3:
|
|
vehicle_3.value = normalizedVehicle;
|
|
break;
|
|
default:
|
|
console.warn(`Invalid vehicle index: ${index}. Please use 1, 2, or 3.`);
|
|
return;
|
|
}
|
|
};
|
|
|
|
const getActiveVehicle = () => {
|
|
switch (activeVehicleIndex.value) {
|
|
default:
|
|
setActiveVehicleIndex(1);
|
|
return vehicle_1.value;
|
|
case 2:
|
|
return vehicle_2.value;
|
|
case 3:
|
|
return vehicle_3.value;
|
|
}
|
|
};
|
|
|
|
const getVehicleByIndex = (index: number): PosVehicle | null => {
|
|
switch (index) {
|
|
case 1:
|
|
return vehicle_1.value;
|
|
case 2:
|
|
return vehicle_2.value;
|
|
case 3:
|
|
return vehicle_3.value;
|
|
default:
|
|
console.warn(`Invalid vehicle index: ${index}. Please use 1, 2, or 3.`);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const vehicles = {
|
|
select: selectVehicle,
|
|
vehicle_1,
|
|
vehicle_2,
|
|
vehicle_3,
|
|
// Active vehicle (Used by scanner, to determine what the next response should apply to.)
|
|
activeVehicleIndex,
|
|
setActiveVehicleIndex,
|
|
getActiveVehicle,
|
|
get: getVehicleByIndex,
|
|
};
|
|
|
|
/** Transaction history */
|
|
// Store the last transactions created on this device
|
|
const lastTransactions = ref<PosOrder[]>([]);
|
|
const timeoutToClearLastTransactions = ref<number>(1000 * 60 * 60 * 24); // Default timeout to clear a transaction from history (24 hours)
|
|
const limitToLastTransactions = ref<number>(100); // Limit to the last X transactions in history, older transactions will be removed
|
|
// Function to check if a transaction is outdated based on the timeout
|
|
const isTransactionOutdated = (order: PosOrder): boolean => {
|
|
const now = Date.now();
|
|
const orderTime = new Date(order.created_at).getTime();
|
|
return now - orderTime > timeoutToClearLastTransactions.value;
|
|
};
|
|
// Function to clear outdated transactions from history
|
|
const clearOutdatedTransactions = () => {
|
|
// Check if there are more than the limit of transactions
|
|
const excessTransactions = lastTransactions.value.length - limitToLastTransactions.value;
|
|
if (excessTransactions > 0) {
|
|
lastTransactions.value.splice(0, excessTransactions);
|
|
}
|
|
// Remove transactions that are older than the timeout
|
|
lastTransactions.value = lastTransactions.value.filter((order) => !isTransactionOutdated(order));
|
|
};
|
|
// Watcher to automatically clear outdated transactions every 10 minutes
|
|
setInterval(() => {
|
|
clearOutdatedTransactions();
|
|
}, 1000 * 60 * 10); // Every 10 minutes
|
|
// Function to add a transaction to the history
|
|
const addTransactionToHistory = (order: PosOrder) => {
|
|
lastTransactions.value.push(order);
|
|
clearOutdatedTransactions();
|
|
};
|
|
// Function to get the last transactions
|
|
const getLastTransactions = () => {
|
|
return lastTransactions.value;
|
|
};
|
|
// Function to get pending transactions (not completed)
|
|
const getPendingTransactions = () => {
|
|
return lastTransactions.value.filter((order) => !order.completed_at);
|
|
};
|
|
// Function to clear the last transactions
|
|
const clearLastTransactions = () => {
|
|
lastTransactions.value = [];
|
|
};
|
|
const transactionHistory = {
|
|
list: lastTransactions,
|
|
add: addTransactionToHistory,
|
|
get: getLastTransactions,
|
|
getPending: getPendingTransactions,
|
|
clear: clearLastTransactions,
|
|
timeout: timeoutToClearLastTransactions,
|
|
limit: limitToLastTransactions,
|
|
clearOutdated: clearOutdatedTransactions,
|
|
};
|
|
|
|
/** Transaction items */
|
|
// The primary item is the main product or service being purchased
|
|
const primaryItem = ref<PosProduct | null>(null);
|
|
// Additional items are any extra products or services added to the transaction, not to be confused with the primary item addons.
|
|
// This could include things like additional products, services, or fees.
|
|
// It is a separate list to allow for flexibility in the transaction.
|
|
const additionalItems = ref<PosProduct[]>([]);
|
|
const normalizeAdditionalItem = (item: PosProduct): PosProduct => ({
|
|
...item,
|
|
quantity: Number(item?.quantity ?? 0) > 0 ? Number(item.quantity) : 1,
|
|
});
|
|
|
|
// Function to add another item to the transaction
|
|
const addAdditionalItem = (item: PosProduct) => {
|
|
// If the additional item requires a note, prompt for it before adding
|
|
if (item.requires_note) {
|
|
promptForNotesIfRequired(item, (notes: string) => {
|
|
additionalItems.value.push(
|
|
normalizeAdditionalItem({
|
|
...item,
|
|
notes,
|
|
})
|
|
);
|
|
});
|
|
return;
|
|
}
|
|
additionalItems.value.push(normalizeAdditionalItem(item));
|
|
};
|
|
// Function to remove an additional item from the transaction
|
|
const removeAdditionalItem = (item: PosProduct) => {
|
|
const index = additionalItems.value.indexOf(item);
|
|
if (index > -1) {
|
|
additionalItems.value.splice(index, 1);
|
|
}
|
|
};
|
|
// Function to clear all additional items from the transaction
|
|
const clearAdditionalItems = () => {
|
|
additionalItems.value = [];
|
|
};
|
|
// Function to set the additional items for the transaction (replaces the current list)
|
|
const setAdditionalItems = (items: PosProduct[]) => {
|
|
additionalItems.value = items;
|
|
};
|
|
// Function to get the total price of all additional items
|
|
const getAdditionalItemsTotal = () => {
|
|
let total = 0;
|
|
additionalItems.value.forEach((item) => {
|
|
total += item.price * (item.quantity || 1);
|
|
// Add the addons prices if they exist
|
|
if (item.addons && item.addons.length > 0) {
|
|
// Make sure that there is a quantity for each addon, if not, assume 0 quantity
|
|
total += item.addons.reduce((sum, addon) => sum + addon.price * (addon.quantity || 0), 0);
|
|
}
|
|
});
|
|
return total;
|
|
};
|
|
// Function to convert a product to an addon:
|
|
const convertProductToAddon = (
|
|
product: PosProduct,
|
|
options: { quantity?: number; min?: number; max?: number } = {}
|
|
): Addon => {
|
|
return {
|
|
id: product.id,
|
|
name: product.name,
|
|
price: product.price,
|
|
product: product,
|
|
quantity: options.quantity || 0,
|
|
min: options.min || -1,
|
|
max: options.max || -1,
|
|
};
|
|
};
|
|
// Function to apply wash certificate addon if present
|
|
const applyWashCertificateIfPresent = (item: PosProduct | null): PosProduct | null => {
|
|
if (!item) return null;
|
|
if (!Array.isArray(item.addons) || item.addons.length === 0) {
|
|
console.warn("The item requires a wash certificate, but the primary service does not support it.");
|
|
return item;
|
|
}
|
|
|
|
const washCertificateAddon = item.addons.find(
|
|
(addon) => addon?.product?.id === WASH_CERTIFICATE_PRODUCT_ID || addon?.id === WASH_CERTIFICATE_PRODUCT_ID
|
|
);
|
|
if (!washCertificateAddon) {
|
|
console.warn("The item requires a wash certificate, but the primary service does not support it.");
|
|
return item;
|
|
}
|
|
|
|
washCertificateAddon.quantity = 1;
|
|
if (washCertificateAddon.product) {
|
|
washCertificateAddon.product.quantity = 1;
|
|
}
|
|
return item;
|
|
};
|
|
|
|
const removeWashCertificateIfPresent = (item: PosProduct | null): PosProduct | null => {
|
|
if (!item || !Array.isArray(item.addons) || item.addons.length === 0) return item;
|
|
|
|
const washCertificateAddon = item.addons.find(
|
|
(addon) => addon?.product?.id === WASH_CERTIFICATE_PRODUCT_ID || addon?.id === WASH_CERTIFICATE_PRODUCT_ID
|
|
);
|
|
if (!washCertificateAddon) return item;
|
|
|
|
washCertificateAddon.quantity = 0;
|
|
if (washCertificateAddon.product) {
|
|
washCertificateAddon.product.quantity = 0;
|
|
}
|
|
|
|
return item;
|
|
};
|
|
// Function to set the primary item for the transaction
|
|
const setPrimaryItem = (item: PosProduct | null) => {
|
|
// Check if the attachments include a wash certificate and if the item requires it
|
|
if (hasWashCertificate()) {
|
|
item = applyWashCertificateIfPresent(item);
|
|
} else {
|
|
item = removeWashCertificateIfPresent(item);
|
|
}
|
|
primaryItem.value = item;
|
|
// Set the vehicle type to the new primary item id (This is done to prevent overriding the vehicle type when changing the primary item)
|
|
if (item && vehicles.vehicle_1.value) {
|
|
vehicles.vehicle_1.value.type = item.id;
|
|
}
|
|
};
|
|
// Function to clear the primary item from the transaction
|
|
const clearPrimaryItem = () => {
|
|
primaryItem.value = null;
|
|
};
|
|
const getPrimaryItemTotal = () => {
|
|
let total = 0;
|
|
// Add the primary item price
|
|
total += primaryItem.value ? primaryItem.value.price : 0;
|
|
// Add the addons prices
|
|
if (primaryItem.value && primaryItem.value.addons && primaryItem.value.addons.length > 0) {
|
|
total += primaryItem.value.addons.reduce((sum, addon) => sum + addon.price * (addon.quantity || 0), 0);
|
|
}
|
|
return total;
|
|
};
|
|
// Function to get the total price of the transaction, including primary and additional items
|
|
const getTransactionTotal = () => {
|
|
return getPrimaryItemTotal() + getAdditionalItemsTotal();
|
|
};
|
|
// Function to update product prices if needed (e.g., if an item is already in the cart, update its price)
|
|
const updateTransactionProduct = (updatedProduct: PosProduct) => {
|
|
// Update primary item price if it matches the updated product
|
|
if (primaryItem.value && primaryItem.value.id === updatedProduct.id) {
|
|
primaryItem.value.price = updatedProduct.price;
|
|
// Update the addons prices if they match the updated product (while maintaining their quantity)
|
|
if (primaryItem.value.addons && primaryItem.value.addons.length > 0) {
|
|
primaryItem.value.addons = primaryItem.value.addons.map((addon) => {
|
|
if (addon.id === updatedProduct.id) {
|
|
return { ...addon, price: updatedProduct.price };
|
|
}
|
|
return addon;
|
|
});
|
|
}
|
|
}
|
|
// Update additional items prices if they match the updated product
|
|
additionalItems.value = additionalItems.value.map((item) => {
|
|
if (item.id === updatedProduct.id) {
|
|
return { ...item, price: updatedProduct.price };
|
|
}
|
|
return item;
|
|
});
|
|
// Update last vehicle orders if they match the updated product
|
|
for (const vehicleKey in lastVehicleOrders.value) {
|
|
const order = lastVehicleOrders.value[vehicleKey as keyof typeof lastVehicleOrders.value];
|
|
if (order && order.items && order.items.length > 0) {
|
|
order.items = order.items.map((item) => {
|
|
if (item.product.id === updatedProduct.id) {
|
|
return {
|
|
...item,
|
|
price: updatedProduct.price, // Update the item price
|
|
product: {
|
|
...item.product, // Keep other product details (quantity, etc.)
|
|
price: updatedProduct.price, // Update the product price
|
|
},
|
|
};
|
|
}
|
|
return item;
|
|
});
|
|
}
|
|
}
|
|
};
|
|
// Function to update product prices in additional items if needed (e.g., if an item is already in the cart, update its price)
|
|
const updateTransactionPrices = (updatedProducts: PosProduct[] | null = null) => {
|
|
// If no updated products are provided, take the current product list
|
|
if (!updatedProducts) {
|
|
//console.warn('No updated products provided, using current product list.');
|
|
updatedProducts = productList.list.value;
|
|
}
|
|
// Update prices for each product in the transaction
|
|
updatedProducts.forEach((product) => {
|
|
//console.warn(`Updating transaction prices for product with id: ${product.id}`);
|
|
updateTransactionProduct(product);
|
|
});
|
|
};
|
|
|
|
// Function to prompt for notes if required (input is a PosProduct)
|
|
const promptForNotesIfRequired = (product: PosProduct, callback: (notes: string) => void) => {
|
|
if (product.requires_note) {
|
|
// Open a popup to prompt for notes
|
|
popups.select("add_product_note", {
|
|
title: "Tilføj note",
|
|
message: `Tilføj venligst en note for produktet: ${product.name}`,
|
|
component: "add_product_note", // This component should handle input and return the notes
|
|
style: { maxHeight: "40vh" },
|
|
props: { product },
|
|
actionButtons: [
|
|
{
|
|
label: "Bekræft",
|
|
description: "Bekræft noten og fortsæt",
|
|
onClick: () => {
|
|
const activePopup = popups.get();
|
|
const note = String(activePopup?.props?.product?.notes || "").trim();
|
|
if (!note) {
|
|
if (activePopup?.props) {
|
|
activePopup.props.validationMessage = "Note er påkrævet for dette produkt";
|
|
}
|
|
return;
|
|
}
|
|
callback(note);
|
|
clearPopup();
|
|
},
|
|
color: "primary",
|
|
},
|
|
{ ...defaultActionButtons.value.cancel },
|
|
],
|
|
});
|
|
} else {
|
|
// If no notes are required, call the callback with an empty string
|
|
callback("");
|
|
}
|
|
};
|
|
|
|
const containsWashCertificate = (): boolean => {
|
|
return doesTransactionContainWashCertificateProduct({
|
|
primaryItem: primaryItem.value,
|
|
additionalItems: additionalItems.value,
|
|
attachmentsHasWashCertificate: attachments.hasWashCertificate(),
|
|
});
|
|
};
|
|
|
|
const transactionItems = {
|
|
// Additional items are separate from the primary item
|
|
additionalItems,
|
|
addAdditionalItem,
|
|
removeAdditionalItem,
|
|
clearAdditionalItems,
|
|
setAdditionalItems,
|
|
// Primary item related functions
|
|
primaryItem,
|
|
setPrimaryItem,
|
|
clearPrimaryItem,
|
|
getPrimaryItemTotal,
|
|
getAdditionalItemsTotal,
|
|
// Transaction total function
|
|
getTransactionTotal,
|
|
// Update product prices if needed (e.g., if an item is already in the cart, update its price)
|
|
updateTransactionPrices,
|
|
// Convert a product to an addon
|
|
convertProductToAddon,
|
|
containsWashCertificate,
|
|
};
|
|
|
|
/** Categories */
|
|
// Categories available for the transaction
|
|
const categoriesList = ref<PosCategory[]>([]);
|
|
const categoriesLoading = ref<boolean>(false);
|
|
// Function to add a new category
|
|
const addCategory = (category: PosCategory) => {
|
|
categoriesList.value.push(category);
|
|
};
|
|
// Function to remove a category
|
|
const removeCategory = (category: PosCategory) => {
|
|
const index = categoriesList.value.indexOf(category);
|
|
if (index > -1) {
|
|
categoriesList.value.splice(index, 1);
|
|
}
|
|
};
|
|
// Function to clear all categories
|
|
const clearCategories = () => {
|
|
categoriesList.value = [];
|
|
};
|
|
// Function to get the list of categories
|
|
const getCategories = () => {
|
|
return categoriesList.value;
|
|
};
|
|
// Current category for the transaction
|
|
const currentCategory = ref<PosCategory | null>(null);
|
|
// Function to set the current category
|
|
const setCurrentCategory = (category: PosCategory | null) => {
|
|
currentCategory.value = category;
|
|
};
|
|
// Function to clear the current category
|
|
const clearCurrentCategory = () => {
|
|
currentCategory.value = null;
|
|
};
|
|
// Function to get the current category
|
|
const getCurrentCategory = () => {
|
|
return currentCategory.value;
|
|
};
|
|
// Function to check if a category is selected
|
|
const isCurrentCategorySelected = () => {
|
|
return currentCategory.value !== null;
|
|
};
|
|
// Function to determine if a category is the current category
|
|
const isCategorySelected = (category: PosCategory) => {
|
|
return currentCategory.value === category;
|
|
};
|
|
const setCategoriesLoading = (state: boolean) => {
|
|
categoriesLoading.value = state;
|
|
};
|
|
|
|
// Exporting the categories object for use in other components
|
|
const categories = {
|
|
// Categories related functions
|
|
list: categoriesList,
|
|
loading: categoriesLoading,
|
|
add: addCategory,
|
|
remove: removeCategory,
|
|
clear: clearCategories,
|
|
get: getCategories,
|
|
setLoading: setCategoriesLoading,
|
|
// Current category-related functions
|
|
selected: currentCategory,
|
|
select: setCurrentCategory,
|
|
unselect: clearCurrentCategory,
|
|
selection: getCurrentCategory,
|
|
isSelected: isCurrentCategorySelected,
|
|
isCategorySelected,
|
|
};
|
|
|
|
/** Products */
|
|
const products = ref<PosProduct[]>([]);
|
|
const productsLoading = ref<boolean>(false);
|
|
// Function to add a product
|
|
const addProduct = (product: PosProduct) => {
|
|
products.value.push(product);
|
|
};
|
|
// Function to remove a product
|
|
const removeProduct = (product: PosProduct) => {
|
|
const index = products.value.indexOf(product);
|
|
if (index > -1) {
|
|
products.value.splice(index, 1);
|
|
}
|
|
};
|
|
// Function to clear all products
|
|
const clearProducts = () => {
|
|
products.value = [];
|
|
};
|
|
// Function to get the list of products
|
|
const getProducts = () => {
|
|
return products.value;
|
|
};
|
|
// Function to check if a product is in the list
|
|
const isProductInList = (product: PosProduct) => {
|
|
return products.value.includes(product);
|
|
};
|
|
const setProductsLoading = (state: boolean) => {
|
|
productsLoading.value = state;
|
|
};
|
|
|
|
// Exporting the products object for use in other components
|
|
const productList = {
|
|
list: products,
|
|
loading: productsLoading,
|
|
add: addProduct,
|
|
remove: removeProduct,
|
|
clear: clearProducts,
|
|
get: getProducts,
|
|
setLoading: setProductsLoading,
|
|
isInList: isProductInList,
|
|
};
|
|
|
|
/** Attachments */
|
|
// Define the reactive properties
|
|
const attachments_files = ref<File[]>([]);
|
|
// Base64 representation must conform to { filename: string; base64String: string }[]
|
|
const attachments_base64 = ref<{ filename: string; base64String: string }[]>([]);
|
|
// Wash certificate?
|
|
const attachments_wash_certificate = ref<boolean>(false);
|
|
// Function to set the wash certificate flag
|
|
const setWashCertificate = (hasCertificate: boolean) => {
|
|
attachments_wash_certificate.value = hasCertificate;
|
|
};
|
|
// Function to get the wash certificate flag
|
|
const hasWashCertificate = () => {
|
|
return attachments_wash_certificate.value;
|
|
};
|
|
// Function to clear the wash certificate flag
|
|
const clearWashCertificate = () => {
|
|
attachments_wash_certificate.value = false;
|
|
};
|
|
// Function to add a wash certificate
|
|
const addWashCertificate = () => {
|
|
attachments_wash_certificate.value = true;
|
|
};
|
|
// Function to add a file attachment
|
|
const addAttachmentFile = (file: File) => {
|
|
attachments_files.value.push(file);
|
|
};
|
|
// Function to remove a file attachment
|
|
const removeAttachmentFile = (file: File) => {
|
|
const index = attachments_files.value.indexOf(file);
|
|
if (index > -1) {
|
|
attachments_files.value.splice(index, 1);
|
|
}
|
|
};
|
|
// Function to clear all file attachments
|
|
const clearAttachmentFiles = () => {
|
|
attachments_files.value = [];
|
|
};
|
|
// Function to get the list of file attachments
|
|
const getAttachmentFiles = () => {
|
|
return attachments_files.value;
|
|
};
|
|
// Function to add a base64 attachment
|
|
const addAttachmentBase64 = (attachment: { filename: string; base64String: string }) => {
|
|
attachments_base64.value.push(attachment);
|
|
};
|
|
// Function to remove a base64 attachment
|
|
const removeAttachmentBase64 = (attachment: { filename: string; base64String: string }) => {
|
|
const index = attachments_base64.value.indexOf(attachment);
|
|
if (index > -1) {
|
|
attachments_base64.value.splice(index, 1);
|
|
}
|
|
};
|
|
// Function to clear all base64 attachments
|
|
const clearAttachmentBase64 = () => {
|
|
attachments_base64.value = [];
|
|
};
|
|
// Function to get the list of base64 attachments
|
|
const getAttachmentBase64 = () => {
|
|
return attachments_base64.value;
|
|
};
|
|
// Function to clear all attachments (both files and base64)
|
|
const clearAllAttachments = () => {
|
|
clearAttachmentFiles();
|
|
clearAttachmentBase64();
|
|
clearWashCertificate();
|
|
};
|
|
// Function to get the total count of all attachments
|
|
const getTotalAttachmentsCount = () => {
|
|
return (
|
|
attachments_files.value.length + attachments_base64.value.length + (attachments_wash_certificate.value ? 1 : 0)
|
|
);
|
|
};
|
|
// Function to take a picture as a base64 attachment
|
|
const takePicture = () => {
|
|
// Save the last picture to the base64 attachments
|
|
const lastPicture = latestImage.value;
|
|
if (lastPicture) {
|
|
addAttachmentBase64({
|
|
filename: "last_picture.jpg",
|
|
base64String: lastPicture,
|
|
});
|
|
}
|
|
};
|
|
// Exporting the attachments object for use in other components
|
|
const attachments = {
|
|
// File attachments
|
|
files: attachments_files,
|
|
addFile: addAttachmentFile,
|
|
removeFile: removeAttachmentFile,
|
|
clearFiles: clearAttachmentFiles,
|
|
getFiles: getAttachmentFiles,
|
|
// Base64 attachments
|
|
base64: attachments_base64,
|
|
addBase64: addAttachmentBase64,
|
|
removeBase64: removeAttachmentBase64,
|
|
clearBase64: clearAttachmentBase64,
|
|
getBase64: getAttachmentBase64,
|
|
// Clear all attachments
|
|
clearAll: clearAllAttachments,
|
|
// Get total count of all attachments
|
|
count: getTotalAttachmentsCount,
|
|
// Wash certificate
|
|
hasWashCertificate,
|
|
setWashCertificate,
|
|
addWashCertificate,
|
|
clearWashCertificate,
|
|
// Take a picture as a base64 attachment
|
|
takePicture,
|
|
};
|
|
|
|
watch(
|
|
() => [attachments_wash_certificate.value, primaryItem.value?.id],
|
|
([hasCertificate]) => {
|
|
if (!primaryItem.value) return;
|
|
if (hasCertificate) {
|
|
applyWashCertificateIfPresent(primaryItem.value);
|
|
} else {
|
|
removeWashCertificateIfPresent(primaryItem.value);
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
/** Camera */
|
|
// Define the reactive properties
|
|
const latestImage = ref<string | null>(null);
|
|
const isCameraMounted = ref<boolean>(false);
|
|
const cameraImageCaptureDelayInitial = ref<number>(500); // Initial delay for camera image capture in milliseconds (First capture)
|
|
const cameraImageCaptureDelaySubsequent = ref<number>(1005); // Further captures delay in milliseconds (Every capture after the first one)
|
|
const cameraImageCaptureDelayAfterSuccess = ref<number>(1005); // Delay after a successful capture in milliseconds (After a successful capture, before the next one)
|
|
const cameraImageCaptureLastSuccess = ref<number | null>(null); // Timestamp of the last successful capture
|
|
const cameraImageZoom = ref<number>(2); // Camera zoom level (1 = no zoom, 2 = 2x zoom, etc.)
|
|
// Function to get the camera zoom level
|
|
const getCameraZoom = () => {
|
|
return cameraImageZoom.value;
|
|
};
|
|
// Function to set the camera zoom level
|
|
const setCameraZoom = (zoom: number) => {
|
|
cameraImageZoom.value = zoom;
|
|
};
|
|
// Function to clear the camera zoom level (reset to default)
|
|
const clearCameraZoom = () => {
|
|
cameraImageZoom.value = 2;
|
|
};
|
|
|
|
// Function to set the timestamp of the last successful capture
|
|
const setCameraImageCaptureLastSuccess = (timestamp?: number | null) => {
|
|
cameraImageCaptureLastSuccess.value = timestamp !== undefined ? timestamp : Date.now();
|
|
};
|
|
// Function to get the timestamp of the last successful capture
|
|
const getCameraImageCaptureLastSuccess = () => {
|
|
return cameraImageCaptureLastSuccess.value;
|
|
};
|
|
// Function to get the delay after a successful capture
|
|
const getCameraImageCaptureDelayAfterSuccess = () => {
|
|
return cameraImageCaptureDelayAfterSuccess.value;
|
|
};
|
|
// Function to set the delay after a successful capture
|
|
const setCameraImageCaptureDelayAfterSuccess = (delay: number) => {
|
|
cameraImageCaptureDelayAfterSuccess.value = delay;
|
|
};
|
|
// Function to check if the delay after a successful capture has passed
|
|
const hasCameraImageCaptureDelayAfterSuccessPassed = (): boolean => {
|
|
if (cameraImageCaptureLastSuccess.value === null) return true; // No successful capture yet
|
|
const currentTime = Date.now();
|
|
return currentTime - cameraImageCaptureLastSuccess.value > cameraImageCaptureDelayAfterSuccess.value;
|
|
};
|
|
// Function to retrieve the latest image frame.
|
|
const getLatestImage = async () => {
|
|
return latestImage.value;
|
|
};
|
|
// Function to set the latest image frame.
|
|
const setLatestImage = (image: string | null) => {
|
|
latestImage.value = image;
|
|
};
|
|
// Function to set the camera mounted state.
|
|
const setCameraMounted = (mounted: boolean) => {
|
|
isCameraMounted.value = mounted;
|
|
};
|
|
// Function to clear the latest image.
|
|
const clearLatestImage = () => {
|
|
latestImage.value = null;
|
|
};
|
|
// Function to clear the camera mounted state.
|
|
const clearCameraMounted = () => {
|
|
isCameraMounted.value = false;
|
|
};
|
|
// Function to get the camera image capture delay.
|
|
const getCameraImageCaptureDelay = (isFirstCapture: boolean): number => {
|
|
return isFirstCapture ? cameraImageCaptureDelayInitial.value : cameraImageCaptureDelaySubsequent.value;
|
|
};
|
|
// Function to set the camera image capture delay.
|
|
const setCameraImageCaptureDelay = (isFirstCapture: boolean, delay: number) => {
|
|
if (isFirstCapture) {
|
|
cameraImageCaptureDelayInitial.value = delay;
|
|
} else {
|
|
cameraImageCaptureDelaySubsequent.value = delay;
|
|
}
|
|
};
|
|
|
|
const camera = {
|
|
latestImage,
|
|
get: getLatestImage,
|
|
mounted: isCameraMounted,
|
|
setMounted: setCameraMounted,
|
|
setLatestImage,
|
|
clearLatestImage,
|
|
clearMounted: clearCameraMounted,
|
|
getImageCaptureDelay: getCameraImageCaptureDelay,
|
|
setImageCaptureDelay: setCameraImageCaptureDelay,
|
|
// Capture delay after success functions
|
|
getImageCaptureDelayAfterSuccess: getCameraImageCaptureDelayAfterSuccess,
|
|
setImageCaptureDelayAfterSuccess: setCameraImageCaptureDelayAfterSuccess,
|
|
setLastSuccess: setCameraImageCaptureLastSuccess,
|
|
getLastSuccess: getCameraImageCaptureLastSuccess,
|
|
hasDelayAfterSuccessPassed: hasCameraImageCaptureDelayAfterSuccessPassed,
|
|
// Camera zoom functions
|
|
getZoom: getCameraZoom,
|
|
setZoom: setCameraZoom,
|
|
clearZoom: clearCameraZoom,
|
|
};
|
|
|
|
/** Metadata */
|
|
const customerId = ref<number | null>(null);
|
|
const notes = ref<string>("");
|
|
const reference = ref<string>("");
|
|
const safetySeal = ref<string>("");
|
|
const washId = ref<string | null>(null); // The Wash ID is used to track the related wash event
|
|
const bookingId = ref<number | null>(null); // The Booking ID is used to track the related booking event
|
|
const bookingSelectionSkippedPlate = ref<string>("");
|
|
const laneId = ref<number | null>(null); // The Lane ID is used to track the related lane event
|
|
const loadingState = ref<boolean>(false); // Loading state for the transaction
|
|
const loadingMessage = ref<string>("Indlæser..."); // Loading message for the transaction
|
|
// Function to set the loading state
|
|
const setLoadingState = (state: boolean) => {
|
|
loadingState.value = state;
|
|
};
|
|
// Function to get the loading state
|
|
const getLoadingState = () => {
|
|
return loadingState.value;
|
|
};
|
|
// Function to set the loading message
|
|
const setLoadingMessage = (message: string) => {
|
|
loadingMessage.value = message;
|
|
};
|
|
// Function to get the loading message
|
|
const getLoadingMessage = () => {
|
|
return loadingMessage.value;
|
|
};
|
|
// Function to clear the loading state
|
|
const clearLoadingState = () => {
|
|
loadingState.value = false;
|
|
loadingMessage.value = "Indlæser...";
|
|
};
|
|
// Function to set the customer ID
|
|
const setCustomerId = (id: number | null) => {
|
|
customerId.value = id;
|
|
};
|
|
// Function to get the customer ID
|
|
const getCustomerId = () => {
|
|
return customerId.value;
|
|
};
|
|
// Function to set notes
|
|
const setNotes = (newNotes: string | null | undefined) => {
|
|
notes.value = newNotes ?? "";
|
|
};
|
|
// Function to get notes
|
|
const getNotes = () => {
|
|
return notes.value;
|
|
};
|
|
// Function to set reference
|
|
const setReference = (newReference: string | null | undefined) => {
|
|
reference.value = newReference ?? "";
|
|
};
|
|
// Function to get reference
|
|
const getReference = () => {
|
|
return reference.value;
|
|
};
|
|
// Function to set safety seal
|
|
const setSafetySeal = (newSafetySeal: string | null | undefined) => {
|
|
safetySeal.value = newSafetySeal ?? "";
|
|
};
|
|
// Function to get safety seal
|
|
const getSafetySeal = () => {
|
|
return safetySeal.value;
|
|
};
|
|
// Function to set the wash ID
|
|
const setWashId = (id: string | null) => {
|
|
washId.value = id;
|
|
};
|
|
// Function to get the wash ID
|
|
const getWashId = () => {
|
|
return washId.value;
|
|
};
|
|
// Function to set the booking ID
|
|
const setBookingId = (id: number | null) => {
|
|
bookingId.value = id;
|
|
};
|
|
// Function to get the booking ID
|
|
const getBookingId = () => {
|
|
return bookingId.value;
|
|
};
|
|
const setBookingSelectionSkippedPlate = (plate: string | null | undefined) => {
|
|
bookingSelectionSkippedPlate.value = String(plate ?? "")
|
|
.replace(/\s/g, "")
|
|
.toUpperCase();
|
|
};
|
|
const getBookingSelectionSkippedPlate = () => {
|
|
return bookingSelectionSkippedPlate.value;
|
|
};
|
|
const clearBookingSelectionSkippedPlate = () => {
|
|
bookingSelectionSkippedPlate.value = "";
|
|
};
|
|
// Function to set the lane ID
|
|
const setLaneId = (id: number | null) => {
|
|
laneId.value = id;
|
|
};
|
|
// Function to get the lane ID
|
|
const getLaneId = () => {
|
|
return laneId.value;
|
|
};
|
|
// Function to clear the lane ID
|
|
const clearLaneId = () => {
|
|
laneId.value = null;
|
|
};
|
|
|
|
const metadata = {
|
|
// Customer ID
|
|
customerId,
|
|
setCustomerId,
|
|
getCustomerId,
|
|
// Notes
|
|
notes,
|
|
setNotes,
|
|
getNotes,
|
|
// Reference
|
|
reference,
|
|
setReference,
|
|
getReference,
|
|
// Safety seal
|
|
safetySeal,
|
|
setSafetySeal,
|
|
getSafetySeal,
|
|
// Wash ID
|
|
washId,
|
|
setWashId,
|
|
getWashId,
|
|
// Booking ID
|
|
bookingId,
|
|
setBookingId,
|
|
getBookingId,
|
|
bookingSelectionSkippedPlate,
|
|
setBookingSelectionSkippedPlate,
|
|
getBookingSelectionSkippedPlate,
|
|
clearBookingSelectionSkippedPlate,
|
|
// Lane ID
|
|
laneId,
|
|
setLaneId,
|
|
getLaneId,
|
|
clearLaneId,
|
|
// Additional metadata can be added here as needed
|
|
setLoadingState,
|
|
setLoadingMessage,
|
|
loadingMessage,
|
|
clearLoadingState,
|
|
loadingState,
|
|
getLoadingState,
|
|
getLoadingMessage,
|
|
};
|
|
|
|
/** Historical data */
|
|
// This section is used to store the last order for the vehicles.
|
|
const lastVehicleOrders = ref<{
|
|
vehicle_1: PosOrder | null;
|
|
vehicle_2: PosOrder | null;
|
|
vehicle_3: PosOrder | null;
|
|
}>({
|
|
vehicle_1: null,
|
|
vehicle_2: null,
|
|
vehicle_3: null,
|
|
});
|
|
/** Last vehicle order */
|
|
const setLastVehicleOrder = (vehicleIndex: number, order: PosOrder | null) => {
|
|
switch (vehicleIndex) {
|
|
case 1:
|
|
lastVehicleOrders.value.vehicle_1 = order;
|
|
break;
|
|
case 2:
|
|
lastVehicleOrders.value.vehicle_2 = order;
|
|
break;
|
|
case 3:
|
|
lastVehicleOrders.value.vehicle_3 = order;
|
|
break;
|
|
default:
|
|
console.warn(`Invalid vehicle index: ${vehicleIndex}. Please use 1, 2, or 3.`);
|
|
return;
|
|
}
|
|
};
|
|
const getLastVehicleOrder = (vehicleIndex: number): PosOrder | null => {
|
|
switch (vehicleIndex) {
|
|
case 1:
|
|
return lastVehicleOrders.value.vehicle_1;
|
|
case 2:
|
|
return lastVehicleOrders.value.vehicle_2;
|
|
case 3:
|
|
return lastVehicleOrders.value.vehicle_3;
|
|
default:
|
|
console.warn(`Invalid vehicle index: ${vehicleIndex}. Please use 1, 2, or 3.`);
|
|
return null;
|
|
}
|
|
};
|
|
const selectLastVehicleOrder = (vehicleIndex: number) => {
|
|
// This function is used to use the last vehicle order for the selected vehicle.
|
|
const lastOrder = getLastVehicleOrder(vehicleIndex);
|
|
if (lastOrder) {
|
|
if (!lastOrder.items || lastOrder.items.length === 0) {
|
|
console.warn(`Last vehicle order for vehicle ${vehicleIndex} has no items. aborting selection.`);
|
|
return;
|
|
}
|
|
|
|
const orderItems = lastOrder.items.filter(Boolean);
|
|
const primaryOrderItem =
|
|
orderItems.find((item) => item?.related_item_id === null || item?.related_item_id === undefined) || orderItems[0];
|
|
|
|
if (!primaryOrderItem) {
|
|
console.warn(`Last vehicle order for vehicle ${vehicleIndex} has no usable primary item.`);
|
|
return;
|
|
}
|
|
|
|
const normalizeOrderItemProduct = (item: any, defaultQuantity = 1): PosProduct => {
|
|
const productId = Number(item?.product?.id ?? item?.product_id ?? item?.id ?? 0);
|
|
const quantity = Number(item?.quantity ?? defaultQuantity);
|
|
|
|
return {
|
|
...(item?.product || {}),
|
|
id: productId,
|
|
name: item?.product?.name ?? item?.name ?? "",
|
|
price: Number(item?.price ?? item?.product?.price ?? 0),
|
|
quantity,
|
|
notes: item?.notes ?? item?.product?.notes ?? "",
|
|
addons: Array.isArray(item?.product?.addons) ? item.product.addons : [],
|
|
subscription_allowed: Boolean(item?.product?.subscription_allowed ?? false),
|
|
} as PosProduct;
|
|
};
|
|
|
|
const primaryOrderItemId = Number(primaryOrderItem?.id ?? 0);
|
|
const addonItems = primaryOrderItemId
|
|
? orderItems.filter((item) => Number(item?.related_item_id ?? 0) === primaryOrderItemId)
|
|
: [];
|
|
const addonIds = new Set(addonItems.map((item) => Number(item?.id ?? 0)));
|
|
|
|
const addons: Addon[] = addonItems.map((item) => {
|
|
const addonProduct = normalizeOrderItemProduct(item, 1);
|
|
return transactionItems.convertProductToAddon(addonProduct, {
|
|
quantity: Number(item?.quantity ?? 1),
|
|
min: 0,
|
|
max: -1,
|
|
});
|
|
});
|
|
|
|
const additionalItems = orderItems
|
|
.filter((item) => {
|
|
const itemId = Number(item?.id ?? 0);
|
|
if (itemId && itemId === Number(primaryOrderItem?.id ?? 0)) {
|
|
return false;
|
|
}
|
|
|
|
if (itemId && addonIds.has(itemId)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
})
|
|
.map((item) => {
|
|
return {
|
|
...normalizeOrderItemProduct(item, 1),
|
|
related_item_id: null,
|
|
} as PosProduct;
|
|
});
|
|
|
|
const primaryItem = <PosProduct>{
|
|
...normalizeOrderItemProduct(primaryOrderItem, 1),
|
|
quantity: 1,
|
|
addons,
|
|
};
|
|
|
|
vehicles.select(vehicleIndex, { ...vehicles.get(vehicleIndex), type: primaryItem.id });
|
|
transactionItems.setAdditionalItems(additionalItems);
|
|
transactionItems.setPrimaryItem({ ...primaryItem });
|
|
} else {
|
|
console.warn(`No last vehicle order found for vehicle ${vehicleIndex}.`);
|
|
}
|
|
};
|
|
|
|
/** Last vehicle order functions */
|
|
const lastOrders = {
|
|
set: setLastVehicleOrder,
|
|
get: getLastVehicleOrder,
|
|
select: selectLastVehicleOrder,
|
|
lastOrders: lastVehicleOrders,
|
|
};
|
|
|
|
/** Reset function */
|
|
// Function to reset vehicles
|
|
const resetVehicles = () => {
|
|
vehicles.vehicle_1.value = null;
|
|
vehicles.vehicle_2.value = null;
|
|
vehicles.vehicle_3.value = null;
|
|
vehicles.activeVehicleIndex.value = 1;
|
|
};
|
|
// Function to reset views
|
|
const resetViews = () => {
|
|
views.manualInput.value = false;
|
|
views.vehicleSelection.value = false;
|
|
views.additionalItemSelection.value = false;
|
|
views.transactionHistoryView.value = false;
|
|
};
|
|
// Function to reset attachments
|
|
const resetAttachments = () => {
|
|
attachments.clearAll();
|
|
};
|
|
// Function to reset transaction items
|
|
const resetTransactionItems = () => {
|
|
transactionItems.primaryItem.value = null;
|
|
transactionItems.additionalItems.value = [];
|
|
};
|
|
// Function to reset categories
|
|
const resetCategories = () => {
|
|
categories.list.value = [];
|
|
categories.selected.value = null;
|
|
categories.loading.value = false;
|
|
};
|
|
// Function to reset products
|
|
const resetProducts = () => {
|
|
productList.list.value = [];
|
|
productList.loading.value = false;
|
|
};
|
|
// Function to reset camera
|
|
const resetCamera = () => {
|
|
camera.latestImage.value = null;
|
|
/** These values are not reset to the initial state as they are not used in the current implementation. */
|
|
//camera.isCameraMounted.value = false;
|
|
//camera.cameraImageCaptureDelayInitial.value = 3000; // Reset to initial delay
|
|
//camera.cameraImageCaptureDelaySubsequent.value = 10000; // Reset to subsequent delay
|
|
camera.clearLatestImage();
|
|
camera.clearMounted();
|
|
};
|
|
// Function to reset metadata
|
|
const resetMetadata = () => {
|
|
metadata.customerId.value = null;
|
|
metadata.notes.value = "";
|
|
metadata.reference.value = "";
|
|
metadata.safetySeal.value = "";
|
|
metadata.washId.value = null;
|
|
metadata.bookingId.value = null;
|
|
metadata.bookingSelectionSkippedPlate.value = "";
|
|
metadata.laneId.value = null;
|
|
metadata.clearLoadingState();
|
|
};
|
|
// Function to reset the last vehicle orders
|
|
const resetLastVehicleOrders = () => {
|
|
lastVehicleOrders.value = {
|
|
vehicle_1: null,
|
|
vehicle_2: null,
|
|
vehicle_3: null,
|
|
};
|
|
};
|
|
// Function to reset transaction history
|
|
const resetTransactionHistory = () => {
|
|
transactionHistory.clear();
|
|
};
|
|
// Function to reset the last saved time
|
|
const resetLastSavedTime = () => {
|
|
lastSavedTime.value = null; // Reset the last saved time to null
|
|
};
|
|
/** Search vehicles */
|
|
const searchVehicles = ref<PosVehicle[]>([]);
|
|
const addSearchVehicle = (vehicle: PosVehicle) => {
|
|
searchVehicles.value.push(vehicle);
|
|
};
|
|
const removeSearchVehicle = (vehicle: PosVehicle) => {
|
|
const index = searchVehicles.value.indexOf(vehicle);
|
|
if (index > -1) {
|
|
searchVehicles.value.splice(index, 1);
|
|
}
|
|
};
|
|
const isSearchVehicleInList = (vehicle: PosVehicle) => {
|
|
return searchVehicles.value.includes(vehicle);
|
|
};
|
|
const getSearchVehiclesByRegistrationNumber = (reg: string) => {
|
|
return searchVehicles.value.filter((v) => v.reg === reg);
|
|
};
|
|
const clearSearchVehicles = () => {
|
|
searchVehicles.value = [];
|
|
};
|
|
const getSearchVehicles = () => {
|
|
return searchVehicles.value;
|
|
};
|
|
const resetSearchVehicles = () => {
|
|
searchVehicles.value = [];
|
|
};
|
|
const search = {
|
|
list: searchVehicles,
|
|
add: addSearchVehicle,
|
|
remove: removeSearchVehicle,
|
|
isInList: isSearchVehicleInList,
|
|
getByRegistrationNumber: getSearchVehiclesByRegistrationNumber,
|
|
clear: clearSearchVehicles,
|
|
get: getSearchVehicles,
|
|
reset: resetSearchVehicles,
|
|
};
|
|
// Function to reset all POS data
|
|
const resetPos = (allData: boolean = false) => {
|
|
resetSearchVehicles();
|
|
resetVehicles();
|
|
resetViews();
|
|
resetTransactionItems();
|
|
resetCategories();
|
|
resetProducts();
|
|
resetCamera();
|
|
resetMetadata();
|
|
resetLastVehicleOrders(); // Reset the last vehicle orders
|
|
resetLastSavedTime(); // Reset the last saved time
|
|
savePos(); // Save the reset state
|
|
hasRetrievedPos.value = false; // Reset the retrieval flag
|
|
retrievePos(); // Re-retrieve to ensure the reset state is applied
|
|
resetAttachments();
|
|
if (allData) {
|
|
resetTransactionHistory(); // Reset the transaction history if allData is true
|
|
}
|
|
};
|
|
// Export reset functions for use in other components
|
|
const reset = {
|
|
vehicles: resetVehicles,
|
|
views: resetViews,
|
|
transactionItems: resetTransactionItems,
|
|
categories: resetCategories,
|
|
products: resetProducts,
|
|
camera: resetCamera,
|
|
metadata: resetMetadata,
|
|
lastOrders: resetLastVehicleOrders,
|
|
attachments: resetAttachments,
|
|
transactionHistory: resetTransactionHistory,
|
|
lastSavedTime: resetLastSavedTime,
|
|
all: resetPos,
|
|
search: resetSearchVehicles,
|
|
pos: resetPos,
|
|
};
|
|
/** Collection */
|
|
const pos = {
|
|
vehicles,
|
|
views,
|
|
transactionItems,
|
|
categories,
|
|
productList,
|
|
camera,
|
|
metadata,
|
|
lastOrders,
|
|
popups,
|
|
reset,
|
|
locations,
|
|
sounds,
|
|
search,
|
|
attachments,
|
|
transactionHistory,
|
|
};
|
|
|
|
/** Persistent data */
|
|
const STORAGE_KEY = "pos"; // Key for localStorage
|
|
// Expiration time for the stored data in milliseconds (Default: 10 minutes)
|
|
const STORAGE_EXPIRE_MS = ref<number>(1000 * 60 * 10); // 10 minutes
|
|
// Time the data was last saved (used for tracking expiration)
|
|
const lastSavedTime = ref<number | null>(null);
|
|
// Function to set the expiration time for the stored data
|
|
const setStorageExpireTime = (ms: number) => {
|
|
STORAGE_EXPIRE_MS.value = ms;
|
|
};
|
|
// Function to get the expiration time for the stored data
|
|
const getStorageExpireTime = () => {
|
|
return STORAGE_EXPIRE_MS.value;
|
|
};
|
|
// Function to check if the stored data has expired
|
|
const isStorageExpired = (): boolean => {
|
|
if (lastSavedTime.value === null) return true; // No data saved yet
|
|
const currentTime = Date.now();
|
|
return currentTime - lastSavedTime.value > STORAGE_EXPIRE_MS.value;
|
|
};
|
|
const hasRetrievedPos = ref<boolean>(false);
|
|
|
|
// Build a plain snapshot for storage (keeps reactivity separate from persisted data)
|
|
const buildSnapshot = () => ({
|
|
vehicles: {
|
|
vehicle_1: vehicles.vehicle_1.value,
|
|
vehicle_2: vehicles.vehicle_2.value,
|
|
vehicle_3: vehicles.vehicle_3.value,
|
|
activeVehicleIndex: vehicles.activeVehicleIndex.value,
|
|
},
|
|
views: {
|
|
manualInput: views.manualInput.value,
|
|
vehicleSelection: views.vehicleSelection.value,
|
|
additionalItemSelection: views.additionalItemSelection.value,
|
|
transactionHistoryView: views.transactionHistoryView.value,
|
|
},
|
|
transactionItems: {
|
|
primaryItem: transactionItems.primaryItem.value,
|
|
additionalItems: transactionItems.additionalItems.value,
|
|
},
|
|
categories: {
|
|
list: categories.list.value,
|
|
selected: categories.selected.value,
|
|
},
|
|
productList: {
|
|
list: productList.list.value,
|
|
},
|
|
metadata: {
|
|
customerId: metadata.customerId.value,
|
|
notes: metadata.notes.value,
|
|
reference: metadata.reference.value,
|
|
safetySeal: metadata.safetySeal.value,
|
|
washId: metadata.washId.value,
|
|
bookingId: metadata.bookingId.value,
|
|
bookingSelectionSkippedPlate: metadata.bookingSelectionSkippedPlate.value,
|
|
laneId: metadata.laneId.value,
|
|
},
|
|
attachments: {
|
|
files: attachments.files.value,
|
|
base64: attachments.base64.value,
|
|
wash_certificate: attachments.hasWashCertificate(),
|
|
},
|
|
transactionHistory: lastTransactions.value,
|
|
lastVehicleOrders: lastOrders.lastOrders.value,
|
|
timestamp: Date.now(), // Add a timestamp for expiration checks
|
|
});
|
|
|
|
// Debounce helper to avoid frequent writes
|
|
const debounce = <T extends (...args: any[]) => void>(fn: T, delay = 200) => {
|
|
let t: ReturnType<typeof setTimeout> | null = null;
|
|
return (...args: Parameters<T>) => {
|
|
if (t) clearTimeout(t);
|
|
t = setTimeout(() => fn(...args), delay);
|
|
};
|
|
};
|
|
|
|
// Save to localStorage
|
|
const savePos = (snapshot?: ReturnType<typeof buildSnapshot>) => {
|
|
if (!hasRetrievedPos.value) return; // don't save until after retrieval
|
|
try {
|
|
const dataToStore = snapshot ?? buildSnapshot();
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(dataToStore));
|
|
} catch (error) {
|
|
console.error("Error saving POS data:", error);
|
|
}
|
|
};
|
|
|
|
// Retrieve the pos "collection" from the local storage.
|
|
const retrievePos = () => {
|
|
try {
|
|
if (typeof window === "undefined" || !window.localStorage) return;
|
|
const storedData = localStorage.getItem(STORAGE_KEY);
|
|
if (!storedData) return;
|
|
|
|
const parsedData = JSON.parse(storedData);
|
|
if (!parsedData || typeof parsedData !== "object") return;
|
|
|
|
// Restore vehicles data
|
|
if (parsedData.vehicles) {
|
|
vehicles.vehicle_1.value = normalizeVehicleSelection(parsedData.vehicles.vehicle_1 ?? null);
|
|
vehicles.vehicle_2.value = normalizeVehicleSelection(parsedData.vehicles.vehicle_2 ?? null);
|
|
vehicles.vehicle_3.value = normalizeVehicleSelection(parsedData.vehicles.vehicle_3 ?? null);
|
|
vehicles.activeVehicleIndex.value = parsedData.vehicles.activeVehicleIndex ?? 1;
|
|
}
|
|
|
|
// Restore views state
|
|
if (parsedData.views) {
|
|
views.manualInput.value = !!parsedData.views.manualInput;
|
|
views.vehicleSelection.value = !!parsedData.views.vehicleSelection;
|
|
views.additionalItemSelection.value = !!parsedData.views.additionalItemSelection;
|
|
views.transactionHistoryView.value = !!parsedData.views.transactionHistoryView;
|
|
}
|
|
|
|
// Restore transaction items
|
|
if (parsedData.transactionItems) {
|
|
transactionItems.primaryItem.value = parsedData.transactionItems.primaryItem ?? null;
|
|
transactionItems.additionalItems.value = Array.isArray(parsedData.transactionItems.additionalItems)
|
|
? parsedData.transactionItems.additionalItems
|
|
: [];
|
|
}
|
|
|
|
// Restore categories
|
|
if (parsedData.categories) {
|
|
categories.list.value = Array.isArray(parsedData.categories.list) ? parsedData.categories.list : [];
|
|
categories.selected.value = parsedData.categories.selected ?? null;
|
|
}
|
|
|
|
// Restore products
|
|
if (parsedData.productList) {
|
|
productList.list.value = Array.isArray(parsedData.productList.list) ? parsedData.productList.list : [];
|
|
}
|
|
|
|
// Restore metadata
|
|
if (parsedData.metadata) {
|
|
metadata.customerId.value = parsedData.metadata.customerId ?? null;
|
|
metadata.notes.value = parsedData.metadata.notes ?? "";
|
|
metadata.reference.value = parsedData.metadata.reference ?? "";
|
|
metadata.safetySeal.value = parsedData.metadata.safetySeal ?? "";
|
|
metadata.washId.value = parsedData.metadata.washId ?? null;
|
|
metadata.bookingId.value = parsedData.metadata.bookingId ?? null;
|
|
metadata.bookingSelectionSkippedPlate.value = parsedData.metadata.bookingSelectionSkippedPlate ?? "";
|
|
metadata.laneId.value = parsedData.metadata.laneId ?? null;
|
|
}
|
|
|
|
// Restore last vehicle orders
|
|
if (parsedData.lastVehicleOrders) {
|
|
lastOrders.lastOrders.value.vehicle_1 = parsedData.lastVehicleOrders.vehicle_1 ?? null;
|
|
lastOrders.lastOrders.value.vehicle_2 = parsedData.lastVehicleOrders.vehicle_2 ?? null;
|
|
lastOrders.lastOrders.value.vehicle_3 = parsedData.lastVehicleOrders.vehicle_3 ?? null;
|
|
}
|
|
|
|
// Restore transaction history
|
|
if (parsedData.transactionHistory && Array.isArray(parsedData.transactionHistory)) {
|
|
lastTransactions.value = parsedData.transactionHistory;
|
|
}
|
|
|
|
// Restore attachments
|
|
if (parsedData.attachments) {
|
|
attachments.files.value = Array.isArray(parsedData.attachments.files) ? parsedData.attachments.files : [];
|
|
attachments.base64.value = Array.isArray(parsedData.attachments.base64) ? parsedData.attachments.base64 : [];
|
|
attachments.setWashCertificate(!!parsedData.attachments.wash_certificate);
|
|
}
|
|
|
|
// Set the last saved time (used for expiration checks)
|
|
lastSavedTime.value = parsedData.timestamp ?? null;
|
|
|
|
// Self-heal legacy snapshots so future reloads keep the normalized vehicle shape.
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(buildSnapshot()));
|
|
} catch (error) {
|
|
console.error("Error restoring POS data:", error);
|
|
} finally {
|
|
hasRetrievedPos.value = true;
|
|
}
|
|
};
|
|
|
|
// Start persistence only after mount/retrieval
|
|
// Start persistence only after retrieval (module-level init, guarded for browser)
|
|
if (typeof window !== "undefined" && typeof localStorage !== "undefined") {
|
|
retrievePos();
|
|
}
|
|
|
|
// Watch vehicles data
|
|
watch(
|
|
() => [
|
|
vehicles.vehicle_1.value,
|
|
vehicles.vehicle_2.value,
|
|
vehicles.vehicle_3.value,
|
|
vehicles.activeVehicleIndex.value,
|
|
],
|
|
savePos
|
|
);
|
|
|
|
// Watch views state
|
|
watch(
|
|
() => [
|
|
views.manualInput.value,
|
|
views.vehicleSelection.value,
|
|
views.additionalItemSelection.value,
|
|
views.transactionHistoryView.value,
|
|
],
|
|
savePos
|
|
);
|
|
|
|
// Watch transaction items
|
|
watch(() => [transactionItems.primaryItem.value, transactionItems.additionalItems.value], savePos);
|
|
|
|
// Watch transaction history
|
|
watch(() => lastTransactions.value, savePos);
|
|
|
|
// Watch categories
|
|
watch(() => [categories.list.value, categories.selected.value], savePos);
|
|
|
|
// Watch products
|
|
watch(() => productList.list.value, savePos);
|
|
|
|
// Watch attachments
|
|
watch(() => [attachments.files.value, attachments.base64.value, attachments.hasWashCertificate()], savePos);
|
|
|
|
// Watch metadata
|
|
watch(
|
|
() => [
|
|
metadata.customerId.value,
|
|
metadata.notes.value,
|
|
metadata.reference.value,
|
|
metadata.safetySeal.value,
|
|
metadata.washId.value,
|
|
metadata.bookingId.value,
|
|
metadata.bookingSelectionSkippedPlate.value,
|
|
metadata.laneId.value,
|
|
],
|
|
savePos
|
|
);
|
|
|
|
// Watch last vehicle orders
|
|
watch(
|
|
() => [
|
|
lastOrders.lastOrders.value.vehicle_1,
|
|
lastOrders.lastOrders.value.vehicle_2,
|
|
lastOrders.lastOrders.value.vehicle_3,
|
|
],
|
|
savePos
|
|
);
|
|
|
|
// Watch for storage expiration
|
|
watch(() => [lastSavedTime.value, STORAGE_EXPIRE_MS.value], savePos);
|
|
|
|
// One deep watcher over a serializable snapshot, debounced
|
|
const serializable = computed(buildSnapshot);
|
|
const savePosDebounced = debounce(() => savePos(serializable.value), 200);
|
|
|
|
watch(
|
|
serializable,
|
|
() => {
|
|
if (hasRetrievedPos.value) savePosDebounced();
|
|
},
|
|
{ deep: true }
|
|
);
|
|
export default defineComponent({
|
|
name: "PosDepartmentStepMobileFlow",
|
|
setup() {
|
|
return {
|
|
pos,
|
|
lastSavedTime,
|
|
setStorageExpireTime,
|
|
getStorageExpireTime,
|
|
isStorageExpired,
|
|
savePos,
|
|
retrievePos,
|
|
metadata,
|
|
vehicles,
|
|
views,
|
|
transactionItems,
|
|
categories,
|
|
productList,
|
|
camera,
|
|
lastOrders,
|
|
popups,
|
|
reset,
|
|
hasRetrievedPos,
|
|
attachments,
|
|
locations,
|
|
sounds,
|
|
search,
|
|
transactionHistory,
|
|
};
|
|
},
|
|
});
|
|
|
|
// Domain namespaces (extracted constants) for improved discoverability
|
|
export const Pos = {
|
|
pos,
|
|
lastSavedTime,
|
|
setStorageExpireTime,
|
|
getStorageExpireTime,
|
|
isStorageExpired,
|
|
savePos,
|
|
retrievePos,
|
|
hasRetrievedPos,
|
|
resetPos,
|
|
resetLastSavedTime,
|
|
resetAll: reset, // clearer intent for full reset
|
|
};
|
|
|
|
export const Vehicles = {
|
|
vehicles,
|
|
activeVehicleIndex,
|
|
setActiveVehicleIndex,
|
|
getActiveVehicle,
|
|
lastOrders,
|
|
setLastVehicleOrder,
|
|
getLastVehicleOrder,
|
|
selectLastVehicleOrder,
|
|
resetVehicles,
|
|
resetLastVehicleOrders,
|
|
};
|
|
|
|
export const ViewsUI = {
|
|
views,
|
|
popups,
|
|
isPopupSet,
|
|
resetViews,
|
|
};
|
|
|
|
export const Camera = {
|
|
camera,
|
|
isCameraMounted,
|
|
setCameraMounted,
|
|
latestImage,
|
|
setLatestImage,
|
|
clearLatestImage,
|
|
getCameraImageCaptureDelay,
|
|
resetCamera,
|
|
};
|
|
|
|
export const Transactions = {
|
|
transactionItems,
|
|
primaryItem,
|
|
setPrimaryItem,
|
|
clearPrimaryItem,
|
|
getPrimaryItemTotal,
|
|
additionalItems,
|
|
addAdditionalItem,
|
|
removeAdditionalItem,
|
|
clearAdditionalItems,
|
|
getAdditionalItemsTotal,
|
|
getTransactionTotal,
|
|
resetTransactionItems,
|
|
};
|
|
|
|
export const Categories = {
|
|
categories,
|
|
setCurrentCategory,
|
|
clearCurrentCategory,
|
|
getCurrentCategory,
|
|
isCurrentCategorySelected,
|
|
isCategorySelected,
|
|
addCategory,
|
|
removeCategory,
|
|
clearCategories,
|
|
getCategories,
|
|
resetCategories,
|
|
};
|
|
|
|
export const Products = {
|
|
productList,
|
|
addProduct,
|
|
removeProduct,
|
|
clearProducts,
|
|
getProducts,
|
|
isProductInList,
|
|
resetProducts,
|
|
};
|
|
|
|
export const Customer = {
|
|
customerId,
|
|
setCustomerId,
|
|
getCustomerId,
|
|
notes,
|
|
setNotes,
|
|
getNotes,
|
|
reference,
|
|
setReference,
|
|
getReference,
|
|
safetySeal,
|
|
setSafetySeal,
|
|
getSafetySeal,
|
|
washId,
|
|
setWashId,
|
|
getWashId,
|
|
bookingId,
|
|
setBookingId,
|
|
getBookingId,
|
|
bookingSelectionSkippedPlate,
|
|
setBookingSelectionSkippedPlate,
|
|
getBookingSelectionSkippedPlate,
|
|
clearBookingSelectionSkippedPlate,
|
|
};
|
|
|
|
export const Meta = {
|
|
metadata,
|
|
resetMetadata,
|
|
};
|
|
|
|
export const UI = {
|
|
manualInput,
|
|
vehicleSelection,
|
|
additionalItemSelection,
|
|
transactionHistoryView,
|
|
};
|
|
|
|
export const Locations = {
|
|
locations,
|
|
setLocation,
|
|
getLocation,
|
|
clearLocation,
|
|
getDistance,
|
|
locationTimeout,
|
|
};
|
|
|
|
export const Sounds = {
|
|
sounds,
|
|
soundEffects,
|
|
soundEffectsEnabled,
|
|
playSoundEffect,
|
|
toggleSoundEffects,
|
|
setSoundEffects,
|
|
getSoundEffects,
|
|
addSoundEffect,
|
|
clearSoundEffects,
|
|
};
|
|
|
|
export const Search = search;
|
|
|
|
// Backward-compatible named exports (grouped and formatted)
|
|
export {
|
|
// POS core
|
|
pos,
|
|
lastSavedTime,
|
|
setStorageExpireTime,
|
|
getStorageExpireTime,
|
|
isStorageExpired,
|
|
savePos,
|
|
retrievePos,
|
|
hasRetrievedPos,
|
|
reset,
|
|
resetPos,
|
|
resetLastSavedTime,
|
|
|
|
// Sounds
|
|
sounds,
|
|
soundEffects,
|
|
soundEffectsEnabled,
|
|
playSoundEffect,
|
|
toggleSoundEffects,
|
|
setSoundEffects,
|
|
getSoundEffects,
|
|
addSoundEffect,
|
|
clearSoundEffects,
|
|
|
|
// Vehicles
|
|
vehicles,
|
|
activeVehicleIndex,
|
|
setActiveVehicleIndex,
|
|
getActiveVehicle,
|
|
lastOrders,
|
|
setLastVehicleOrder,
|
|
getLastVehicleOrder,
|
|
selectLastVehicleOrder,
|
|
resetVehicles,
|
|
resetLastVehicleOrders,
|
|
// Attachments
|
|
attachments,
|
|
// Attachment files
|
|
attachments_files,
|
|
addAttachmentFile,
|
|
removeAttachmentFile,
|
|
clearAttachmentFiles,
|
|
getAttachmentFiles,
|
|
// Attachment base64
|
|
attachments_base64,
|
|
addAttachmentBase64,
|
|
removeAttachmentBase64,
|
|
clearAttachmentBase64,
|
|
getAttachmentBase64,
|
|
// Clear all attachments
|
|
clearAllAttachments,
|
|
|
|
// Views & Popups
|
|
views,
|
|
popups,
|
|
isPopupSet,
|
|
resetViews,
|
|
|
|
// Location
|
|
locations,
|
|
setLocation,
|
|
getLocation,
|
|
clearLocation,
|
|
getDistance,
|
|
locationTimeout,
|
|
|
|
// Transactions
|
|
transactionItems,
|
|
primaryItem,
|
|
setPrimaryItem,
|
|
clearPrimaryItem,
|
|
getPrimaryItemTotal,
|
|
additionalItems,
|
|
addAdditionalItem,
|
|
removeAdditionalItem,
|
|
clearAdditionalItems,
|
|
getAdditionalItemsTotal,
|
|
getTransactionTotal,
|
|
resetTransactionItems,
|
|
|
|
// Transaction history
|
|
transactionHistory,
|
|
lastTransactions,
|
|
addTransactionToHistory,
|
|
clearLastTransactions,
|
|
getLastTransactions,
|
|
resetTransactionHistory,
|
|
|
|
// Categories
|
|
categories,
|
|
setCurrentCategory,
|
|
clearCurrentCategory,
|
|
getCurrentCategory,
|
|
isCurrentCategorySelected,
|
|
isCategorySelected,
|
|
addCategory,
|
|
removeCategory,
|
|
clearCategories,
|
|
getCategories,
|
|
resetCategories,
|
|
|
|
// Products
|
|
productList,
|
|
addProduct,
|
|
removeProduct,
|
|
clearProducts,
|
|
getProducts,
|
|
isProductInList,
|
|
resetProducts,
|
|
|
|
// Camera
|
|
camera,
|
|
isCameraMounted,
|
|
setCameraMounted,
|
|
latestImage,
|
|
setLatestImage,
|
|
clearLatestImage,
|
|
getCameraImageCaptureDelay,
|
|
resetCamera,
|
|
|
|
// Customer / Metadata
|
|
customerId,
|
|
setCustomerId,
|
|
getCustomerId,
|
|
notes,
|
|
setNotes,
|
|
getNotes,
|
|
reference,
|
|
setReference,
|
|
getReference,
|
|
safetySeal,
|
|
setSafetySeal,
|
|
getSafetySeal,
|
|
washId,
|
|
setWashId,
|
|
getWashId,
|
|
bookingId,
|
|
setBookingId,
|
|
getBookingId,
|
|
laneId,
|
|
setLaneId,
|
|
getLaneId,
|
|
clearLaneId,
|
|
|
|
// Metadata
|
|
metadata,
|
|
resetMetadata,
|
|
|
|
// UI State
|
|
manualInput,
|
|
vehicleSelection,
|
|
additionalItemSelection,
|
|
transactionHistoryView,
|
|
|
|
// Search
|
|
search,
|
|
addSearchVehicle,
|
|
removeSearchVehicle,
|
|
isSearchVehicleInList,
|
|
getSearchVehiclesByRegistrationNumber,
|
|
clearSearchVehicles,
|
|
getSearchVehicles,
|
|
resetSearchVehicles,
|
|
|
|
// Action buttons
|
|
actionButtons,
|
|
};
|
|
|
|
// Non-breaking alias exports (clearer names while keeping originals)
|
|
export { productList as products, lastOrders as vehicleOrders, getCameraImageCaptureDelay as getCameraCaptureDelay };
|
|
</script>
|