502 lines
17 KiB
Vue
502 lines
17 KiB
Vue
<script setup lang="ts">
|
|
|
|
import {defineComponent, defineEmits, defineProps, ref, watch, onMounted} from "vue";
|
|
import { useI18n } from "vue-i18n";
|
|
import { VehicleStatusKey, statusKeyToComponent } from "@/components/displays/department/pos/steps/mobile/objects/PosVehicleStatus.vue";
|
|
import { vehicles_matching, searchVehicle, register_new_search, is_latest_search, clearCustomerSelection, isSearching, pendingBookings, loadPendingBookings, doesVehiclePlateHaveBooking, getVehiclePlateBookings, getPreferredVehiclePlateBooking, searchAndSelectCustomer, reg_1, reg_2, department_id } from "@/components/shop/POSDepartmentProcess.vue";
|
|
import { metadata, popups, setCustomerId, pos, actionButtons } from "@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue";
|
|
import {PosSearchResult} from "@/components/displays/department/pos/steps/mobile/objects/PosSearchResult.vue";
|
|
defineComponent({
|
|
name: "RegistrationNumberSearchResult"
|
|
});
|
|
|
|
const { t } = useI18n();
|
|
|
|
const emits = defineEmits(['select']);
|
|
|
|
const props = defineProps({
|
|
searchQuery: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
modifyCustomerOnChange: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
isHidden: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
automaticallySelect: {
|
|
type: Boolean,
|
|
default: false
|
|
}
|
|
});
|
|
|
|
const searchResults = ref<PosSearchResult[]>([]);
|
|
|
|
//const searchResults: SearchResult[] = Array.from({ length: debug.results }, (_, i) => ({
|
|
// registrationNumber: generateRegistrationNumber(),
|
|
// customerName: `Customer name ${i + 1}`,
|
|
// customerId: i + 1,
|
|
// customerStatus: generateCustomerStatus(),
|
|
//}));
|
|
|
|
const normalizeRegistrationNumber = (value: string | null | undefined) => String(value ?? '').replace(/\s+/g, '').toUpperCase();
|
|
const normalizeReferenceValue = (value: string | null | undefined) => String(value ?? '').trim();
|
|
|
|
const applySearchResultCustomerSelection = (result: PosSearchResult) => {
|
|
if (!props.modifyCustomerOnChange) {
|
|
return;
|
|
}
|
|
|
|
if (result?.customerId) {
|
|
searchAndSelectCustomer(result.customerId);
|
|
setCustomerId(result.customerId);
|
|
return;
|
|
}
|
|
|
|
setCustomerId(null);
|
|
};
|
|
|
|
const emitSelectedResult = (result: PosSearchResult | null) => {
|
|
emits('select', result);
|
|
};
|
|
|
|
const resolveSkippedBookingReference = (result: PosSearchResult) => {
|
|
const normalizedRegistrationNumber = normalizeRegistrationNumber(result?.registrationNumber || props.searchQuery);
|
|
const matchingVehicle = Array.isArray(vehicles_matching.value)
|
|
? vehicles_matching.value.find((vehicle: any) => normalizeRegistrationNumber(vehicle?.reg) === normalizedRegistrationNumber)
|
|
: null;
|
|
|
|
const vehicleReference = normalizeReferenceValue(matchingVehicle?.reference);
|
|
if (vehicleReference) {
|
|
return vehicleReference;
|
|
}
|
|
|
|
return normalizeReferenceValue(result?.reference);
|
|
};
|
|
|
|
const buildSkippedBookingSelection = (result: PosSearchResult) => {
|
|
const inheritedReference = resolveSkippedBookingReference(result);
|
|
|
|
if (inheritedReference) {
|
|
pos.metadata.setReference(inheritedReference);
|
|
}
|
|
|
|
return {
|
|
...result,
|
|
bookingId: null,
|
|
reference: inheritedReference || null,
|
|
} as PosSearchResult;
|
|
};
|
|
|
|
const applySelectedBooking = async (booking: any, result: PosSearchResult) => {
|
|
if (!booking?.id) {
|
|
return;
|
|
}
|
|
|
|
const bookingMatches = Array.isArray(result?.bookingMatches) ? result.bookingMatches : [booking];
|
|
const customerNumber = Number.parseInt(String(booking?.customer_number ?? booking?.customer_id ?? result?.customerId ?? 0), 10) || 0;
|
|
const bookingReg1 = normalizeRegistrationNumber(booking?.reg_1 || result?.registrationNumber || '');
|
|
const bookingReg2 = normalizeRegistrationNumber(booking?.reg_2 || '');
|
|
|
|
metadata.setBookingId(booking.id);
|
|
metadata.clearBookingSelectionSkippedPlate?.();
|
|
pos.views.manualInput.value = false;
|
|
pos.metadata.setNotes(booking.note || booking.notes || `Booking ID: ${booking.id}`);
|
|
pos.metadata.setReference(booking.reference || booking.reference_number || '');
|
|
reg_1.value = bookingReg1;
|
|
reg_2.value = bookingReg2;
|
|
|
|
pos.vehicles.select(1, {
|
|
reg: bookingReg1,
|
|
type: result?.type || 0,
|
|
customer_id: customerNumber,
|
|
status: 'booked',
|
|
barred: false,
|
|
booking_id: booking.id,
|
|
booking_matches: bookingMatches,
|
|
reference: booking.reference || booking.reference_number || result?.reference || null,
|
|
last_order_id: result?.lastOrderId || null,
|
|
wash_subscription: result?.washSubscription,
|
|
});
|
|
|
|
if (bookingReg2) {
|
|
pos.vehicles.select(2, {
|
|
reg: bookingReg2,
|
|
type: 0,
|
|
customer_id: customerNumber,
|
|
status: 'booked',
|
|
barred: false,
|
|
booking_id: booking.id,
|
|
booking_matches: bookingMatches,
|
|
});
|
|
} else {
|
|
pos.vehicles.select(2, null);
|
|
}
|
|
|
|
pos.vehicles.select(3, null);
|
|
|
|
if (customerNumber > 0) {
|
|
await searchAndSelectCustomer(customerNumber);
|
|
setCustomerId(customerNumber);
|
|
}
|
|
|
|
popups.clear();
|
|
emitSelectedResult({
|
|
...result,
|
|
registrationNumber: bookingReg1 || result?.registrationNumber,
|
|
customerName: booking.customer_name || result?.customerName || "Unknown Customer",
|
|
customerId: customerNumber,
|
|
customerStatus: 'booked',
|
|
reference: booking.reference || booking.reference_number || result?.reference || null,
|
|
bookingId: booking.id,
|
|
bookingMatches,
|
|
});
|
|
};
|
|
|
|
const continueWithoutBookingSelection = (result: PosSearchResult) => {
|
|
metadata.setBookingId(null);
|
|
metadata.setBookingSelectionSkippedPlate?.(normalizeRegistrationNumber(result?.registrationNumber || props.searchQuery));
|
|
popups.clear();
|
|
applySearchResultCustomerSelection(result);
|
|
emitSelectedResult(buildSkippedBookingSelection(result));
|
|
};
|
|
|
|
const refreshOrderBookingSelection = async (result: PosSearchResult) => {
|
|
await loadPendingBookings();
|
|
|
|
const normalizedRegistrationNumber = normalizeRegistrationNumber(result?.registrationNumber || props.searchQuery);
|
|
const refreshedMatches = normalizedRegistrationNumber ? getVehiclePlateBookings(normalizedRegistrationNumber) : [];
|
|
const refreshedResult = {
|
|
...result,
|
|
bookingMatches: refreshedMatches,
|
|
};
|
|
|
|
if (refreshedMatches.length === 1) {
|
|
await applySelectedBooking(refreshedMatches[0], refreshedResult);
|
|
return {
|
|
handled: true,
|
|
bookings: refreshedMatches,
|
|
};
|
|
}
|
|
|
|
if (refreshedMatches.length === 0) {
|
|
continueWithoutBookingSelection(refreshedResult);
|
|
return {
|
|
handled: true,
|
|
bookings: [],
|
|
};
|
|
}
|
|
|
|
return {
|
|
handled: false,
|
|
bookings: refreshedMatches,
|
|
};
|
|
};
|
|
|
|
const openOrderBookingPopup = (result: PosSearchResult) => {
|
|
let activeResult: PosSearchResult = {
|
|
...result,
|
|
bookingMatches: Array.isArray(result.bookingMatches) ? result.bookingMatches : [],
|
|
};
|
|
const updateActiveResultMatches = (bookings: any[]) => {
|
|
activeResult = {
|
|
...activeResult,
|
|
bookingMatches: Array.isArray(bookings) ? bookings : [],
|
|
};
|
|
return activeResult.bookingMatches;
|
|
};
|
|
|
|
pos.views.manualInput.value = false;
|
|
popups.select('select_order_booking', {
|
|
title: popups.getByKey('select_order_booking')?.title,
|
|
message: popups.getByKey('select_order_booking')?.message,
|
|
showHeaderClose: true,
|
|
onHeaderClose: () => continueWithoutBookingSelection(activeResult),
|
|
headerCloseTestId: 'pos-mobile-order-booking-header-close',
|
|
props: {
|
|
bookings: updateActiveResultMatches(result.bookingMatches || []),
|
|
departmentId: Number.parseInt(String(department_id.value ?? ''), 10) || null,
|
|
matchedPlate: normalizeRegistrationNumber(activeResult.registrationNumber || props.searchQuery),
|
|
onSelect: (booking: any) => applySelectedBooking(booking, activeResult),
|
|
onSkip: () => continueWithoutBookingSelection(activeResult),
|
|
onRefreshBookings: async () => {
|
|
const refreshOutcome = await refreshOrderBookingSelection(activeResult);
|
|
return updateActiveResultMatches(refreshOutcome.bookings);
|
|
},
|
|
},
|
|
actionButtons: [
|
|
{
|
|
...actionButtons.default.value.cancel,
|
|
label: t('admin.pos.order_booking_selector.continue_without_booking'),
|
|
testId: 'pos-mobile-order-booking-skip',
|
|
onClick: () => continueWithoutBookingSelection(activeResult),
|
|
},
|
|
],
|
|
});
|
|
};
|
|
|
|
const onSelect = async (result: PosSearchResult) => {
|
|
if (!result) {
|
|
emitSelectedResult(null);
|
|
return;
|
|
}
|
|
|
|
const bookingMatches = Array.isArray(result.bookingMatches) ? result.bookingMatches : [];
|
|
if (bookingMatches.length === 1) {
|
|
await applySelectedBooking(bookingMatches[0], result);
|
|
return;
|
|
}
|
|
|
|
if (bookingMatches.length > 1) {
|
|
const normalizedRegistrationNumber = normalizeRegistrationNumber(result.registrationNumber || props.searchQuery);
|
|
if (metadata.getBookingSelectionSkippedPlate?.() === normalizedRegistrationNumber) {
|
|
metadata.setBookingId(null);
|
|
applySearchResultCustomerSelection(result);
|
|
emitSelectedResult(buildSkippedBookingSelection(result));
|
|
return;
|
|
}
|
|
|
|
const existingBooking = bookingMatches.find((booking: any) => Number(booking.id) === Number(metadata.getBookingId?.()));
|
|
if (existingBooking) {
|
|
await applySelectedBooking(existingBooking, result);
|
|
return;
|
|
}
|
|
|
|
openOrderBookingPopup(result);
|
|
return;
|
|
}
|
|
|
|
metadata.setBookingId(null);
|
|
applySearchResultCustomerSelection(result);
|
|
emitSelectedResult(result);
|
|
};
|
|
|
|
const emitVehicleObject = (vehicle: any) => {
|
|
// Emit the vehicle object to the parent component
|
|
//console.log("emitVehicleObject:", vehicle);
|
|
if (!vehicle) {
|
|
// If the vehicle is null, emit null
|
|
emits('select', null);
|
|
return;
|
|
}
|
|
// If the vehicle is valid, emit the vehicle object
|
|
emits('select', vehicle);
|
|
};
|
|
|
|
|
|
const performSearch = (searchQuery: string) => {
|
|
// Perform the search and return the results
|
|
// Define the search ID for this input change
|
|
const search_id = register_new_search();
|
|
if (props.modifyCustomerOnChange) {
|
|
clearCustomerSelection();
|
|
}
|
|
|
|
// If the search is already in progress, do not perform a new search
|
|
if (isSearching.value && !is_latest_search(search_id)) {
|
|
return;
|
|
}
|
|
|
|
// Perform the search
|
|
searchVehicle(
|
|
searchQuery,
|
|
search_id
|
|
)
|
|
};
|
|
|
|
// Watch for changes in the isSearching state
|
|
watch(isSearching, (newValue) => {
|
|
//console.log("isSearching changed:", newValue);
|
|
if (!newValue) {
|
|
// If the search is not in progress, and there are no vehicles matching, emit null
|
|
if (vehicles_matching.value.length === 0 && props.modifyCustomerOnChange) {
|
|
if (props.automaticallySelect && props.searchQuery !== '') {
|
|
emitVehicleObject({
|
|
registrationNumber: props.searchQuery,
|
|
customerName: "Unknown Customer",
|
|
customerId: 0,
|
|
customerStatus: determineCustomerStatus({reg: props.searchQuery} as any),
|
|
is_known: determineCustomerStatus({reg: props.searchQuery} as any) === 'known',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Watch for changes in the searchVehicle function
|
|
watch(() => props.searchQuery, (newValue) => {
|
|
if (newValue) {
|
|
performSearch(newValue);
|
|
if (props.automaticallySelect) {
|
|
automaticallySelect();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Watch for changes in the vehicles_matching array
|
|
watch(() => vehicles_matching.value, (newValue) => {
|
|
if (newValue) {
|
|
//console.warn("vehicles_matching changed:", newValue);
|
|
const result = newValue.map(vehicle => {
|
|
const bookingMatches = getVehiclePlateBookings(vehicle.reg);
|
|
const preferredBooking = bookingMatches[0] || null;
|
|
if (preferredBooking) {
|
|
vehicle.customer_name = preferredBooking.customer_name;
|
|
}
|
|
// Transform the vehicle object to the SearchResult type
|
|
//console.warn('Transforming vehicle to PosSearchResult:', vehicle);
|
|
return {
|
|
registrationNumber: vehicle.reg,
|
|
customerName: vehicle.customer_name || "Unknown Customer",
|
|
customerId: vehicle.customer_id || 0,
|
|
customerStatus: determineCustomerStatus(vehicle),
|
|
type: vehicle?.type || null,
|
|
barred: vehicle?.barred || null,
|
|
reference: vehicle?.reference || null,
|
|
lastOrderId: vehicle?.last_order_id || null,
|
|
washSubscription: vehicle?.wash_subscription || null,
|
|
bookingId: bookingMatches.length === 1 ? bookingMatches[0].id : vehicle?.booking_id || null,
|
|
bookingMatches,
|
|
} as PosSearchResult;
|
|
});
|
|
//console.warn('Transformed PosSearchResult:', result);
|
|
// Update the search results
|
|
searchResults.value = result;
|
|
importPendingBookings();
|
|
automaticallySelect();
|
|
//console.log("searchResults updated:", searchResults.value);
|
|
}
|
|
});
|
|
|
|
const automaticallySelect = () => {
|
|
const withoutDuplicates = preventDuplicates(searchResults.value);
|
|
if (props.automaticallySelect && props.searchQuery !== '' && withoutDuplicates.length > 0) {
|
|
//console.warn("Automatically selecting vehicle for reg:", props.searchQuery, "from vehicles_matching:", vehicles_matching.value);
|
|
// Check if the vehicle is found in the results.
|
|
// If the vehicle is found.
|
|
if (withoutDuplicates.find(v => v.registrationNumber.toUpperCase() === props.searchQuery.toUpperCase())) {
|
|
const match = withoutDuplicates.find(v => v.registrationNumber.toUpperCase() === props.searchQuery.toUpperCase());
|
|
// Emit the selected vehicle object
|
|
onSelect({
|
|
registrationNumber: props.searchQuery.toUpperCase(),
|
|
customerName: match?.customerName || "Unknown Customer",
|
|
customerId: match?.customerId || 0,
|
|
customerStatus: match ? match.customerStatus : determineCustomerStatus({reg: props.searchQuery} as any),
|
|
type: match?.type || null,
|
|
barred: match?.barred,
|
|
reference: match?.reference || null,
|
|
lastOrderId: match?.lastOrderId || null,
|
|
washSubscription: match?.washSubscription,
|
|
bookingId: match?.bookingId || null,
|
|
bookingMatches: match?.bookingMatches || [],
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
const determineCustomerStatus = (vehicle: any): VehicleStatusKey => {
|
|
//console.warn('Determine customer status for vehicle:', vehicle);
|
|
// Determine the customer status based on the vehicle object
|
|
if ((Array.isArray(vehicle?.bookingMatches) && vehicle.bookingMatches.length > 0) || doesVehiclePlateHaveBooking(vehicle?.reg) || vehicle?.booking_id) {
|
|
return 'booked';
|
|
} else if (vehicle?.status === 'known') {
|
|
//console.warn('Vehicle is known:', vehicle);
|
|
return 'known'
|
|
} else if (vehicle?.status === 'unknown') {
|
|
//console.warn('Vehicle is unknown:', vehicle);
|
|
return 'unknown';
|
|
} else if (vehicle?.status === 'card' || vehicle?.barred === true) {
|
|
//console.warn('Vehicle is barred:', vehicle);
|
|
return 'card';
|
|
} else if (vehicle?.status === 'verified') {
|
|
//console.warn('Vehicle is verified:', vehicle);
|
|
return 'verified';
|
|
} else {
|
|
//console.warn('Vehicle is unknown: ', vehicle);
|
|
return 'unknown';
|
|
}
|
|
};
|
|
|
|
const importPendingBookings = () => {
|
|
// Import pending bookings and update the search results
|
|
pendingBookings.value.forEach(booking => {
|
|
const vehicle = vehicles_matching.value.find(v => v.reg.toUpperCase() === booking.reg_1.toUpperCase() || v.reg.toUpperCase() === booking.reg_2?.toUpperCase());
|
|
if (vehicle) {
|
|
vehicle.customer_name = getPreferredVehiclePlateBooking(vehicle.reg)?.customer_name || booking.customer_name;
|
|
}
|
|
});
|
|
};
|
|
|
|
onMounted(() => {
|
|
// Load pending bookings when the component is mounted
|
|
loadPendingBookings();
|
|
// If the search query is already set, perform the search
|
|
if (props.searchQuery && props.isHidden) {
|
|
performSearch(props.searchQuery);
|
|
}
|
|
});
|
|
|
|
const preventDuplicates = (results: PosSearchResult[]) => {
|
|
// The same registration number should not appear multiple times in the results
|
|
const seen = new Set();
|
|
const uniqueResults: PosSearchResult[] = [];
|
|
for (const result of results) {
|
|
const strippedReg = stripSpaces(result.registrationNumber).toUpperCase();
|
|
if (!seen.has(strippedReg)) {
|
|
//console.warn("Adding unique result:", result);
|
|
seen.add(strippedReg);
|
|
uniqueResults.push(result);
|
|
}
|
|
}
|
|
//console.warn("Unique results after preventing duplicates:", uniqueResults);
|
|
return uniqueResults;
|
|
}
|
|
const stripSpaces = (str: string) => {
|
|
return str.replace(/\s+/g, '');
|
|
};
|
|
</script>
|
|
<template>
|
|
<!-- Debugging information
|
|
{{ searchResults.length }} results found
|
|
{{ vehicles_matching.length }} vehicles matching
|
|
{{ pendingBookings.length }} pending bookings
|
|
-->
|
|
<div v-for="result in preventDuplicates(searchResults)" :key="result.registrationNumber" style="border-bottom: 1px solid #EAEAEA" @click="onSelect(result)" class="is-clickable" v-if="!props.isHidden">
|
|
<div class="columns is-mobile pb-0">
|
|
<div class="column is-align-content-center pl-4">
|
|
<p>
|
|
<!-- Registration number -->
|
|
<span>{{ result.registrationNumber }}</span>
|
|
<!-- Separator -->
|
|
<span> - </span>
|
|
<!-- Customer name -->
|
|
<span>{{ result.customerName }}</span>
|
|
</p>
|
|
</div>
|
|
<div class="column is-narrow pr-5 py-4 mb-0">
|
|
<!-- Icon -->
|
|
<div class="custom-icon" style="width: 28px; height: 28px; border-bottom: none">
|
|
<component :is="statusKeyToComponent(result.customerStatus)" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<style scoped>
|
|
|
|
.custom-icon > svg {
|
|
/* icon */
|
|
width: 28px;
|
|
height: 28px;
|
|
/* Auto layout */
|
|
flex: none;
|
|
order: 0;
|
|
flex-grow: 0;
|
|
align-self: center;
|
|
margin: 0 auto;
|
|
}
|
|
</style>
|