Files
pleno-vue/src/components/models/pos/step1/RegistrationNumberSearchResult.vue
T

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>