Update vehicle and customer selection workflows in POS form
Refactored `SelectVehicleFormPOS` and related components to improve vehicle and customer selection, including new UI elements for license plate input, dropdown suggestions, and customer search. Introduced reusable `ExpandableContentBox` and optimized handling of vehicle objects and focus states.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import {defineProps, defineSlots, onMounted, ref, watch} from 'vue';
|
||||
import {defineProps, defineSlots, onMounted, ref, watch, defineEmits} from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -32,10 +32,20 @@ const props = defineProps({
|
||||
allowCompactWhenOneTab: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Forcefully set the active tab
|
||||
* @default null
|
||||
* @type String
|
||||
*/
|
||||
forceActiveTab: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
});
|
||||
|
||||
const slots = defineSlots();
|
||||
const emit = defineEmits(['update:activeTab']);
|
||||
const visibleTabs = ref([]);
|
||||
/**
|
||||
* The active tab
|
||||
@@ -113,6 +123,30 @@ const isTabVisible = (tab) => {
|
||||
return tmp_visible;
|
||||
};
|
||||
|
||||
const onClickTab = (tab) => {
|
||||
// If the tab is not visible, do nothing
|
||||
if (!isTabVisible(tab)) {
|
||||
return;
|
||||
}
|
||||
// Set the active tab to the clicked tab
|
||||
if (props.forceActiveTab === null || props.forceActiveTab === undefined) {
|
||||
activeTab.value = tab;
|
||||
}
|
||||
// Emit the active tab
|
||||
emit('update:activeTab', tab);
|
||||
};
|
||||
|
||||
// Watch the forceActiveTab prop and set the active tab to the forceActiveTab value
|
||||
watch(() => props.forceActiveTab, (newValue) => {
|
||||
if (newValue !== null && newValue !== undefined) {
|
||||
activeTab.value = newValue;
|
||||
}
|
||||
});
|
||||
|
||||
if (props.forceActiveTab !== null && props.forceActiveTab !== undefined) {
|
||||
activeTab.value = props.forceActiveTab;
|
||||
}
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
@@ -131,7 +165,7 @@ const isTabVisible = (tab) => {
|
||||
:class="{ 'is-active': tab.slot === activeTab }"
|
||||
v-if="isTabVisible(tab)"
|
||||
>
|
||||
<a @click="activeTab = tab.slot">
|
||||
<a @click="onClickTab(tab.slot)">
|
||||
<!-- Display the tab icon (if any) -->
|
||||
<span v-if="tab.icon" class="icon is-small">
|
||||
<i :class="tab.icon"></i>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<script setup>
|
||||
import { ref, defineProps, defineEmits } from 'vue';
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
|
||||
const props = defineProps({
|
||||
expanded: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(['update:expanded']);
|
||||
const expandIcon = ref(null);
|
||||
const toggleIsSelected = () => {
|
||||
emit('update:expanded', !props.expanded);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="props.expanded">
|
||||
<slot name="expandedContent"></slot>
|
||||
</div>
|
||||
<!-- Expander -->
|
||||
<div class="column is-12-desktop">
|
||||
<p
|
||||
class="has-text-centered"
|
||||
@click="toggleIsSelected"
|
||||
@mouseover="expandIcon.classList.add('has-text-link')"
|
||||
@mouseleave="expandIcon.classList.remove('has-text-link')"
|
||||
>
|
||||
<span class="icon is-small" style="color: #c7c7c7;">
|
||||
<i
|
||||
class="fas fa-angle-down fa-lg"
|
||||
:class="{ 'fa-rotate-180': props.expanded }"
|
||||
aria-hidden="true"
|
||||
ref="expandIcon"
|
||||
></i>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,442 @@
|
||||
<script setup>
|
||||
import { getDepartment, selectScan } from "@/components/shop/POSDepartmentProcess.vue";
|
||||
import {
|
||||
isLoaded,
|
||||
isLoading,
|
||||
list,
|
||||
loadList,
|
||||
metaCurrentPage,
|
||||
metaItemsPerPage,
|
||||
metaTotalItems,
|
||||
setEndpoint,
|
||||
setMetaItemsPerPage,
|
||||
setPage,
|
||||
search,
|
||||
setFilter,
|
||||
setOrder,
|
||||
metaSearch,
|
||||
} from "@/components/pagination/paginatedList.vue";
|
||||
import {useElementVisibility} from "@vueuse/core";
|
||||
import {SessionUser} from "@/components/session/token/SessionUser.vue";
|
||||
import {ref, watch, onMounted, useTemplateRef} from 'vue';
|
||||
import {popperBox, showPopper, showPopperWithContent, removePopperIfOpen} from "@/components/displays/PopperDefault.vue";
|
||||
import { Colors } from "@/ThemeConfig.vue";
|
||||
|
||||
const activeTab = ref(null);
|
||||
|
||||
const panel_tabs = ref([
|
||||
{
|
||||
name: 'Seneste scanninger',
|
||||
active: true
|
||||
},
|
||||
{
|
||||
name: 'Søg efter nummerplade',
|
||||
active: false
|
||||
}
|
||||
]);
|
||||
|
||||
const scansInfinityScroll = ref(null);
|
||||
const highestPage = ref(0); // The highest page that has been loaded (This is used to prevent loading the same page multiple times)
|
||||
const limitReached = ref(false);
|
||||
|
||||
/** Fetch the scans when the component is mounted */
|
||||
onMounted(() => {
|
||||
limitReached.value = false;
|
||||
setEndpoint("/numberplatescans", false);
|
||||
setFilter('department_id', parseInt(getDepartment()), false);
|
||||
setOrder('created_at', 'desc', false);
|
||||
setMetaItemsPerPage(10, false);
|
||||
});
|
||||
|
||||
/** Whenever the search changes, reset the infinite scroll */
|
||||
const onChanged = () => {
|
||||
scansInfinityScroll.value = null;
|
||||
highestPage.value = 0;
|
||||
console.log('Changed', 'total items: ' + metaTotalItems.value, 'items per page: ' + metaItemsPerPage.value, 'current page: ' + metaCurrentPage.value, 'search: ' + metaSearch.value, 'limit reached: ' + limitReached.value);
|
||||
};
|
||||
|
||||
/** Whenever the tab changes, update the items per page, and reset the search */
|
||||
const changeTab = (tabIndex) => {
|
||||
// Reset the infinite scroll
|
||||
scansInfinityScroll.value = null;
|
||||
// Set the highest page to 0
|
||||
highestPage.value = 0;
|
||||
search('', false);
|
||||
// Set the page to 1
|
||||
setPage(1);
|
||||
// Load the list
|
||||
loadList();
|
||||
};
|
||||
|
||||
/** Whenever the search changes, reset the infinite scroll */
|
||||
watch(metaSearch, () => {
|
||||
onChanged();
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
// Watch the list, and add the items to the infinite scroll list
|
||||
watch(list, (newList) => {
|
||||
if (scansInfinityScroll.value === null) {
|
||||
scansInfinityScroll.value = newList;
|
||||
} else {
|
||||
// Check if the list already contains the ids from the new list (To prevent duplicates)
|
||||
const ids = scansInfinityScroll.value.map((item) => item.id);
|
||||
newList = newList.filter((item) => !ids.includes(item.id));
|
||||
// If the new list is empty, then return (There's no new items)
|
||||
scansInfinityScroll.value = scansInfinityScroll.value.concat(newList);
|
||||
}
|
||||
});
|
||||
|
||||
const activePopperPlate = ref({
|
||||
plate: '',
|
||||
isLoaded: false
|
||||
});
|
||||
|
||||
const tmp = {
|
||||
"registration_number": "BA95215",
|
||||
"status": "Registreret",
|
||||
"status_date": "2016-04-04T14:12:14.000+02:00",
|
||||
"type": "Stor personbil",
|
||||
"use": "Busku00f8rsel",
|
||||
"first_registration": "2016-04-04+02:00",
|
||||
"vin": "YV3T2U829G1177642",
|
||||
"own_weight": 15900,
|
||||
"cerb_weight": 16500,
|
||||
"total_weight": 24350,
|
||||
"axels": 3,
|
||||
"pulling_axels": 1,
|
||||
"seats": 54,
|
||||
"coupling": true,
|
||||
"trailer_maxweight_nobrakes": 0,
|
||||
"trailer_maxweight_withbrakes": 2800,
|
||||
"doors": null,
|
||||
"make": "VOLVO",
|
||||
"model": "9700",
|
||||
"variant": "9700HD (9711R 13,83)",
|
||||
"model_type": "B6SC",
|
||||
"model_year": 0,
|
||||
"color": null,
|
||||
"chassis_type": "",
|
||||
"engine_cylinders": 6,
|
||||
"engine_volume": 10837,
|
||||
"engine_power": 345,
|
||||
"fuel_type": "Diesel",
|
||||
"is_hybrid": false,
|
||||
"hybrid_type": "mild",
|
||||
"registration_zipcode": "",
|
||||
"vehicle_id": 9000000001742548,
|
||||
"mot_info": {
|
||||
"type": "PeriodiskSyn",
|
||||
"date": "2024-02-22",
|
||||
"result": "Godkendt",
|
||||
"status": "Aktiv",
|
||||
"status_date": "2024-02-22",
|
||||
"mileage": 546000
|
||||
},
|
||||
"is_leasing": false,
|
||||
"leasing_from": null,
|
||||
"leasing_to": null
|
||||
};
|
||||
|
||||
|
||||
const decodeMotorAPIString = (string) => {
|
||||
// Check if the string contains u00, if so add a backslash
|
||||
if (string.includes('u00')) {
|
||||
string = string.replace(/u00/g, '\\u00');
|
||||
}
|
||||
// unicode decode
|
||||
return string.replace(/\\u[\dA-F]{4}/gi,
|
||||
(match) => String.fromCharCode(parseInt(match.replace(/\\u/g, ''), 16)));
|
||||
};
|
||||
|
||||
|
||||
|
||||
const generatePopperPlate = (scan, motorapi_data) => {
|
||||
const data = {
|
||||
make: motorapi_data.make,
|
||||
model: motorapi_data.model,
|
||||
variant: motorapi_data.variant,
|
||||
type: motorapi_data.type,
|
||||
use: decodeMotorAPIString(motorapi_data.use),
|
||||
time: '<strong>'
|
||||
+ new Date(scan.created_at).toLocaleTimeString(
|
||||
'da-DK',
|
||||
{
|
||||
timeZone: 'Europe/Copenhagen',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
})
|
||||
+ '</strong>' +
|
||||
' d. ' +
|
||||
'<strong>' +
|
||||
new Date(scan.created_at).toLocaleDateString(
|
||||
'da-DK',
|
||||
{
|
||||
timeZone: 'Europe/Copenhagen'
|
||||
}
|
||||
)
|
||||
+ '</strong>'
|
||||
}
|
||||
return `
|
||||
<div class="numberplate-popper" data-plate="${scan.plate}">
|
||||
<p><strong>Mærke:</strong> ${data.make}</p>
|
||||
<p><strong>Model:</strong> ${data.model}</p>
|
||||
<p><strong>Variant:</strong> ${data.variant}</p>
|
||||
<p><strong>Type:</strong> ${data.type}</p>
|
||||
<p><strong>Use:</strong> ${data.use}</p>
|
||||
<p><strong>Scannet:</strong> kl. ${data.time}</p>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
const isPlatePopperLoading = ref(false);
|
||||
|
||||
const shownNumberPlatePoppers = ref([]);
|
||||
|
||||
const removePlatePoppersIfOpen = () => {
|
||||
shownNumberPlatePoppers.value.forEach((popperId) => {
|
||||
removePopperIfOpen(popperId);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const showPopperPlate = (scan, element) => {
|
||||
// If the plate is loading, then return
|
||||
if (isPlatePopperLoading.value) {
|
||||
return;
|
||||
}
|
||||
if (scan === null) {
|
||||
return;
|
||||
}
|
||||
// Set the plate as loading
|
||||
isPlatePopperLoading.value = true;
|
||||
// Get the plate registration
|
||||
SessionUser.superUser.modules.motorapi.functions.lookup(scan.plate).then((response) => {
|
||||
const motorapi_data = response.data.data;
|
||||
let content = generatePopperPlate(scan, motorapi_data);
|
||||
const uniqueId = "plate-popper-" + scan.plate;
|
||||
showPopper(popperBox(
|
||||
'Nummerplade: ' + scan.plate,
|
||||
content,
|
||||
uniqueId
|
||||
), element);
|
||||
shownNumberPlatePoppers.value.push(uniqueId);
|
||||
isPlatePopperLoading.value = false;
|
||||
}).catch((error) => {
|
||||
isPlatePopperLoading.value = false;
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
const plate_scanners = ref([]);
|
||||
|
||||
const getPlateScanners = async () => {
|
||||
await SessionUser.request('/department/numberplatescanners?id=' + getDepartment(),
|
||||
'GET')
|
||||
.then((response) => {
|
||||
plate_scanners.value = response.data.data;
|
||||
console.log(plate_scanners.value);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getPlateScanners();
|
||||
|
||||
const getPlateScannerName = (id) => {
|
||||
for (let i = 0; i < plate_scanners.value.length; i++) {
|
||||
if (plate_scanners.value[i].id === id) {
|
||||
return plate_scanners.value[i].name;
|
||||
}
|
||||
}
|
||||
return 'N/A';
|
||||
};
|
||||
|
||||
/**
|
||||
* End of list reference
|
||||
*/
|
||||
const endOfList = useTemplateRef('endOfList');
|
||||
|
||||
/**
|
||||
* If the end of the list is visible, then load more items
|
||||
*/
|
||||
const endOfListVisibility = useElementVisibility(endOfList);
|
||||
|
||||
watch(endOfListVisibility, (isVisible) => {
|
||||
console.log('Checking visibility');
|
||||
console.log(isVisible);
|
||||
if (isVisible) {
|
||||
// If the current page is the highest page, then return
|
||||
console.log('Current page: ' + metaCurrentPage.value);
|
||||
console.log('Highest page: ' + highestPage.value);
|
||||
console.log('Total items: ' + metaTotalItems.value);
|
||||
console.log('Items per page: ' + metaItemsPerPage.value);
|
||||
// Calculate the total pages
|
||||
const totalPages = Math.ceil(metaTotalItems.value / metaItemsPerPage.value);
|
||||
console.log('Total pages: ' + totalPages);
|
||||
// If the current page is the highest page, then return
|
||||
if (metaCurrentPage.value === totalPages) {
|
||||
limitReached.value = true;
|
||||
return;
|
||||
} else {
|
||||
limitReached.value = false;
|
||||
}
|
||||
// Load more items
|
||||
setPage(metaCurrentPage.value + 1);
|
||||
// If the current page is 1, then we don't need to load the list again
|
||||
if (metaCurrentPage.value !== 1) {
|
||||
loadList();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const isLimitReached = () => {
|
||||
return limitReached.value;
|
||||
};
|
||||
|
||||
// Wait 1s before loading the list
|
||||
setTimeout(() => {
|
||||
loadList();
|
||||
}, 1000);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div @mouseleave="removePlatePoppersIfOpen()">
|
||||
<article class="" id="scans-panel">
|
||||
<p
|
||||
class="is-size-4 title"
|
||||
style="font-weight: 400;"
|
||||
>
|
||||
{{ SessionUser.objects.orders.columns.reg_1.label }}
|
||||
</p>
|
||||
<p
|
||||
class="is-size-6 subtitle mb-1"
|
||||
>
|
||||
Seneste scanninger
|
||||
</p>
|
||||
|
||||
<p class="control mb-2">
|
||||
<input
|
||||
@input="onChanged(); search($event.target.value);"
|
||||
class="input mt-2 has-sharp-edges has-placeholder-italic"
|
||||
type="text"
|
||||
placeholder="Søg efter nummerplade"
|
||||
style="border-color: transparent;"
|
||||
:style="{
|
||||
'background-color': Colors.inputs.default.backgroundColor,
|
||||
'color': Colors.inputs.default.textColor
|
||||
}"
|
||||
v-model="metaSearch"
|
||||
/>
|
||||
</p>
|
||||
<div id="scans-list" class="scans-list">
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ SessionUser.objects.orders.columns.reg_1.label }}</th>
|
||||
<th class="is-narrow">Scanner</th>
|
||||
<th class="is-narrow">{{ SessionUser.objects.global.language.time.at_hour }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="scan in scansInfinityScroll" :key="scan.id">
|
||||
<tr class="plate-scan-entry" @click="selectScan(scan)"
|
||||
@mouseenter="showPopperPlate(scan, $event.target)"
|
||||
@mouseleave="removePlatePoppersIfOpen()">
|
||||
<!--<span class="panel-icon">
|
||||
<i class="fas fa-car" aria-hidden="true"></i>
|
||||
</span> -->
|
||||
<td style="width: 50%;">{{ scan.plate }}</td>
|
||||
<td class="" style="width: 15%;"
|
||||
v-if="plate_scanners.length > 0"
|
||||
>
|
||||
{{ getPlateScannerName(parseInt(scan.plate_scanner_id)) }}
|
||||
</td>
|
||||
<!-- Time, and date HH:MM DD/MM (European format - 13:00 01/01) -->
|
||||
<td
|
||||
class="is-narrow"
|
||||
style="width: 50%;"
|
||||
>
|
||||
{{ SessionUser.functions.date.toLocalTimeHours(scan.created_at) }}
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<!-- If the infinity scroll is null, show a loading spinner -->
|
||||
<tr v-if="scansInfinityScroll === null">
|
||||
<td colspan="3">
|
||||
<div class="has-text-centered">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- If the infinity scroll is empty, show a message -->
|
||||
<tr v-if="scansInfinityScroll !== null && scansInfinityScroll.length === 0">
|
||||
<td colspan="3">
|
||||
<div class="has-text-centered">
|
||||
Ingen flere nummerplader
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Element to detect the end of the list -->
|
||||
<tr ref="endOfList" class="end-of-list">
|
||||
<td colspan="3">
|
||||
<div class="has-text-centered" v-if="!isLimitReached()">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
<div class="has-text-centered" v-else>
|
||||
<span class="icon">
|
||||
<i class="fas fa-check"></i>
|
||||
</span>
|
||||
<span>{{ SessionUser.objects.global.language.nothing_left_to_show }}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- If there's no scans, and the infinity scroll is also empty or null -->
|
||||
<div class="panel-block" v-if="list.length === 0 && getDepartment() !== '' && (scansInfinityScroll === null || scansInfinityScroll.length === 0)">
|
||||
<p>
|
||||
Ingen nummerplader blev fundet
|
||||
</p>
|
||||
</div>
|
||||
<!-- Reload button -->
|
||||
<div class="panel-block" v-if="list.length === 0 && getDepartment() !== '' && scansInfinityScroll === null">
|
||||
<button class="button is-dark is-fullwidth" @click="loadList">
|
||||
<span class="icon">
|
||||
<i class="fas fa-redo"></i>
|
||||
</span>
|
||||
<span>Prøv igen</span>
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
<div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.scans-list {
|
||||
overflow-y: auto;
|
||||
max-height: 300px;
|
||||
}
|
||||
.scans-list::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
.scans-list::-webkit-scrollbar-thumb {
|
||||
background-color: #4a4a4a;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.scans-list::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
.input.has-sharp-edges {
|
||||
border-radius: 0;
|
||||
}
|
||||
.input.has-placeholder-italic::placeholder {
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
@@ -166,13 +166,13 @@ loadCustomerAttributes();
|
||||
<div v-if="customer_name">
|
||||
<article class="">
|
||||
<div class="panel-block">
|
||||
<div class="columns is-vcentered">
|
||||
<div class="column is-4">
|
||||
<div class="columns is-vcentered is-multiline">
|
||||
<div class="column is-12">
|
||||
<span class="is-size-6 title"
|
||||
>{{ customer_name }}</span>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="tabs is-right is-small is-float-right">
|
||||
<div class="tabs is-small is-centered">
|
||||
<ul>
|
||||
<li v-for="(tab, index) in panel_tabs" :key="index" :class="{'is-active': tab.active}" @click="panel_tabs.forEach(tab => tab.active = false); tab.active = true">
|
||||
<RequiresPermission :permission="tab.permission" v-if="tab.permission">
|
||||
|
||||
@@ -46,7 +46,14 @@ const isReaderConnected = (reader) => {
|
||||
return reader.status === 'online';
|
||||
};
|
||||
const isReaderAvailable = (reader) => {
|
||||
return (reader.action === null || reader.action === undefined || reader.action === "") && isReaderConnected(reader);
|
||||
if (!isReaderConnected(reader)) {
|
||||
return false;
|
||||
}
|
||||
// If the action status is available, the reader is available
|
||||
if (getReaderStatus(reader) === 'Available') {
|
||||
return true;
|
||||
}
|
||||
return (reader.action === null || reader.action === undefined || reader.action === "")
|
||||
};
|
||||
const getAvailableReaders = () => {
|
||||
return readers.value.filter(isReaderAvailable);
|
||||
|
||||
@@ -15,7 +15,8 @@ import {onMounted, ref} from "vue";
|
||||
import ElementTabsBox from "@/components/displays/boxes/ElementTabsBox.vue";
|
||||
import {POS_STEP_1_VERSION} from "@/main.js";
|
||||
import SelectVehicleFormPOS from "@/components/forms/department/pos/SelectVehicleFormPOS.vue";
|
||||
|
||||
import PosLastScannedLicensePlatesV2 from "@/components/displays/department/pos/PosLastScannedLicensePlatesV2.vue";
|
||||
import {watch} from "vue";
|
||||
// Clear cache
|
||||
clearCache();
|
||||
|
||||
@@ -42,6 +43,23 @@ const right_tabs = ref([
|
||||
}
|
||||
]);
|
||||
|
||||
const forceActiveTab = ref('license_plates'); // Force the active tab to be license plates
|
||||
|
||||
const setTab = (tab) => {
|
||||
console.log("Tab changed to: " + tab);
|
||||
forceActiveTab.value = tab;
|
||||
};
|
||||
|
||||
// Watch for changes in the isCustomerSelected variable
|
||||
watch(() => isCustomerSelected(), (newValue) => {
|
||||
// If the customer is selected, set the active tab to customer
|
||||
if (newValue) {
|
||||
forceActiveTab.value = 'customer';
|
||||
} else {
|
||||
forceActiveTab.value = 'license_plates';
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -121,12 +139,14 @@ const right_tabs = ref([
|
||||
default-active-tab="license_plates"
|
||||
class="is-boxed"
|
||||
:allowCompactWhenOneTab="true"
|
||||
@update:activeTab="setTab"
|
||||
v-bind:force-active-tab="forceActiveTab"
|
||||
>
|
||||
<template #customer>
|
||||
<PosSelectedCustomer />
|
||||
</template>
|
||||
<template #license_plates>
|
||||
<PosLastScannedLicensePlates class="my-3"/>
|
||||
<PosLastScannedLicensePlatesV2 class="my-3"/>
|
||||
</template>
|
||||
</ElementTabsBox>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { defineProps, ref } from "vue";
|
||||
import {defineProps, ref, watch} from "vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -7,6 +7,26 @@ const props = defineProps({
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
displayPrice: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
displayReference: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
displayNotes: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
useLocalOrderItems: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
localOrderItems: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
|
||||
const order_item_default = {
|
||||
@@ -37,6 +57,10 @@ const order_item_default = {
|
||||
const orderItems = ref([]);
|
||||
|
||||
const loadOrderItems = async () => {
|
||||
if (props.useLocalOrderItems) {
|
||||
orderItems.value = props.localOrderItems;
|
||||
return;
|
||||
}
|
||||
await SessionUser.request(
|
||||
'/order/items',
|
||||
'GET',
|
||||
@@ -66,6 +90,12 @@ const ucFirst = (str) => {
|
||||
}
|
||||
|
||||
loadOrderItems();
|
||||
|
||||
watch(() => props.orderId, (newValue, oldValue) => {
|
||||
if (newValue !== oldValue) {
|
||||
loadOrderItems();
|
||||
}
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -74,10 +104,10 @@ loadOrderItems();
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ucFirst(SessionUser.objects.products.meta.labels.single)}}</th>
|
||||
<th>{{ucFirst(SessionUser.objects.orders.columns.notes.label)}}</th>
|
||||
<th>{{ucFirst(SessionUser.objects.orders.columns.reference.label)}}</th>
|
||||
<th v-if="props.displayNotes">{{ucFirst(SessionUser.objects.orders.columns.notes.label)}}</th>
|
||||
<th v-if="props.displayReference">{{ucFirst(SessionUser.objects.orders.columns.reference.label)}}</th>
|
||||
<th>{{ucFirst(SessionUser.objects.global.language.quantity)}}</th>
|
||||
<th>{{ucFirst(SessionUser.objects.products.columns.price.label)}}</th>
|
||||
<th v-if="props.displayPrice">{{ucFirst(SessionUser.objects.products.columns.price.label)}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -87,18 +117,18 @@ loadOrderItems();
|
||||
<!-- If the item is included in the invoice, show it normally -->
|
||||
<tr v-if="item.include_in_invoice">
|
||||
<td>{{ item.product.name }}</td>
|
||||
<td>{{ item.reference }}</td>
|
||||
<td>{{ item.notes }}</td>
|
||||
<td v-if="props.displayReference">{{ item.reference }}</td>
|
||||
<td v-if="props.displayNotes">{{ item.notes }}</td>
|
||||
<td>{{ item.quantity }}</td>
|
||||
<td>{{ SessionUser.functions.currency.toLocal(item.price) }}</td>
|
||||
<td v-if="props.displayPrice">{{ SessionUser.functions.currency.toLocal(item.price) }}</td>
|
||||
</tr>
|
||||
<!-- If the item is not included in the invoice, show it with a strikethrough -->
|
||||
<tr v-else class="has-background-warning-light">
|
||||
<td><span style="text-decoration: line-through;">{{ item.product.name }}</span> ( {{SessionUser.objects.vehicles.columns.wash_subscription.label }} )</td>
|
||||
<td>{{ item.reference }}</td>
|
||||
<td>{{ item.notes }}</td>
|
||||
<td v-if="props.displayReference">{{ item.reference }}</td>
|
||||
<td v-if="props.displayNotes">{{ item.notes }}</td>
|
||||
<td>{{ item.quantity }}</td>
|
||||
<td style="text-decoration: line-through;">{{ SessionUser.functions.currency.toLocal(item.price) }}</td>
|
||||
<td v-if="props.displayPrice" style="text-decoration: line-through;">{{ SessionUser.functions.currency.toLocal(item.price) }}</td>
|
||||
</tr>
|
||||
</template>
|
||||
<!-- Show the add-on items -->
|
||||
@@ -106,18 +136,18 @@ loadOrderItems();
|
||||
<!-- If the add-on item is included in the invoice, show it normally -->
|
||||
<tr v-if="addon_item.include_in_invoice">
|
||||
<td>+ {{ addon_item.product.name }}</td>
|
||||
<td>{{ addon_item.reference }}</td>
|
||||
<td>{{ addon_item.notes }}</td>
|
||||
<td v-if="props.displayReference">{{ addon_item.reference }}</td>
|
||||
<td v-if="props.displayNotes">{{ addon_item.notes }}</td>
|
||||
<td>{{ addon_item.quantity }}</td>
|
||||
<td>{{ SessionUser.functions.currency.toLocal(addon_item.price) }}</td>
|
||||
<td v-if="props.displayPrice">{{ SessionUser.functions.currency.toLocal(addon_item.price) }}</td>
|
||||
</tr>
|
||||
<!-- If the add-on item is not included in the invoice, show it with a strikethrough -->
|
||||
<tr v-else class="has-background-warning">
|
||||
<td>+ <span style="text-decoration: line-through;">{{ addon_item.product.name }}</span> ( {{SessionUser.objects.vehicles.columns.wash_subscription.label }} )</td>
|
||||
<td>{{ addon_item.reference }}</td>
|
||||
<td>{{ addon_item.notes }}</td>
|
||||
<td v-if="props.displayReference">{{ addon_item.reference }}</td>
|
||||
<td v-if="props.displayNotes">{{ addon_item.notes }}</td>
|
||||
<td>{{ addon_item.quantity }}</td>
|
||||
<td style="text-decoration: line-through;">{{ SessionUser.functions.currency.toLocal(addon_item.price) }}</td>
|
||||
<td v-if="props.displayPrice" style="text-decoration: line-through;">{{ SessionUser.functions.currency.toLocal(addon_item.price) }}</td>
|
||||
</tr>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -8,22 +8,263 @@ import { isSearching } from "@/components/search/economic/customerSearch.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import LicensePlateInput from "@/components/forms/department/pos/input/LicensePlateInput.vue";
|
||||
import LicensePlateReg1Input from "@/components/forms/department/pos/input/LicensePlateReg1Input.vue";
|
||||
import CustomerSearchFieldPos from "@/components/forms/department/pos/input/customerSearchFieldPos.vue";
|
||||
import ExpandableContentBox from "@/components/displays/boxes/ExpandableContentBox.vue";
|
||||
import WhiteBox from "@/components/displays/boxes/WhiteBox.vue";
|
||||
import OrderContentTable from "@/components/displays/superuser/tables/OrderContentTable.vue";
|
||||
import OrderItemsTable from "@/components/displays/department/pos/order/orderItemsTable.vue";
|
||||
|
||||
const isRegistrationNumbersExpanded = ref(false);
|
||||
const isOtherExpanded = ref(false);
|
||||
|
||||
const keyDownNextInput = (event, field_id) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
document.getElementById(field_id).focus();
|
||||
}
|
||||
};
|
||||
|
||||
const keyDownNextTabIndexButton = (event, tabIndex) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
// First unfocus the input field. This is to ensure the sync between the input field and the POSDepartmentProcess.vue data.
|
||||
event.target.blur();
|
||||
document.querySelector(`button[tabindex="${tabIndex}"]`).click();
|
||||
}
|
||||
};
|
||||
|
||||
const vehicleObject = ref(null);
|
||||
|
||||
// debug:
|
||||
//vehicleObject.value = { "id": 324, "user_id": 1952, "customer_id": 12345679, "type": 1, "reg": "EC21233", "wash_subscription": true, "addons": { "enabled": 1, "available": 2, "list": [ { "id": 287, "vehicle_id": 324, "addon_id": 14, "amount": 1, "product": { "id": 24, "name": "Spot Free- Lastbil", "description": " ", "price": 39, "subscription_allowed": 1, "category": "4", "piktogram": "", "economic_product_id": "33", "apply_category_discount": false, "requires_note": false, "created_at": "2024-12-09 14:31:49", "updated_at": "2025-04-08 14:07:41" } } ] }, "last_order_id": 6249 }
|
||||
// debug end
|
||||
|
||||
const setVehicleObject = (emittedVehicleObject) => {
|
||||
//console.log(vehicleObject, 'Emitted vehicleObject');
|
||||
// Set the vehicle object to the ref
|
||||
vehicleObject.value = emittedVehicleObject;
|
||||
};
|
||||
|
||||
const ucFirst = (str) => {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
};
|
||||
|
||||
const isUserCurrentlyInFocusReg1 = ref(false);
|
||||
const setFocusStateReg1 = (isFocused) => {
|
||||
isUserCurrentlyInFocusReg1.value = isFocused;
|
||||
};
|
||||
|
||||
|
||||
const customerHasAttribute = (attribute) => {
|
||||
// Check if the user has the attribute
|
||||
if (customer_attributes.value === null) {
|
||||
return false;
|
||||
}
|
||||
if (customer_attributes.value.length === 0) {
|
||||
return false;
|
||||
}
|
||||
// Check if the attribute is in the list
|
||||
for (let i = 0; i < customer_attributes.value.length; i++) {
|
||||
if (customer_attributes.value[i].attribute === attribute) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- License plate input fields -->
|
||||
<!-- License plate 1 -->
|
||||
<div class="field">
|
||||
<label class="label">{{SessionUser.objects.orders.columns.reg_1.label}}</label>
|
||||
<div class="control">
|
||||
<LicensePlateReg1Input
|
||||
:tabindex="0"
|
||||
@update:vehicleObject="setVehicleObject"
|
||||
@update:focus="setFocusStateReg1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- License plate 2 -->
|
||||
<div class="field">
|
||||
<div class="is-pulled-right">
|
||||
<div class="tag is-small is-clickable" @click="isRegistrationNumbersExpanded = !isRegistrationNumbersExpanded">
|
||||
<span>
|
||||
{{ (isRegistrationNumbersExpanded ? (SessionUser.objects.global.language.hide) : (SessionUser.objects.global.language.show)) + ' ' + SessionUser.objects.orders.columns.reg_3.label}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<label class="label">{{ SessionUser.objects.orders.columns.reg_2.label }}</label>
|
||||
<LicensePlateInput :tab-index="2"
|
||||
@keydown="keyDownNextInput($event, (isRegistrationNumbersExpanded ? 'reg_3' : 'reference'))"
|
||||
input-id="reg_2"
|
||||
v-model:inputModel="reg_2"
|
||||
:customer_number="parseInt(customer_id) || 0"
|
||||
v-bind:actual-value="reg_2"
|
||||
/>
|
||||
</div>
|
||||
<!-- License plate 3 -->
|
||||
<div class="field" v-show="isRegistrationNumbersExpanded">
|
||||
<label class="label">{{ SessionUser.objects.orders.columns.reg_3.label }}</label>
|
||||
<LicensePlateInput :tab-index="3"
|
||||
@keydown="keyDownNextInput($event, 'reference')"
|
||||
input-id="reg_3"
|
||||
v-model:inputModel="reg_3"
|
||||
:customer_number="parseInt(customer_id) || 0"
|
||||
v-bind:actual-value="reg_3"
|
||||
/>
|
||||
</div>
|
||||
<!-- Reference field -->
|
||||
<div class="field" v-if="customerHasAttribute('requiresReferenceNumber')">
|
||||
<label class="label">{{SessionUser.objects.orders.columns.reference.label}}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
type="text"
|
||||
class="input has-sharp-edges"
|
||||
id="reference"
|
||||
:tabindex="4"
|
||||
v-model="reference"
|
||||
@keydown="keyDownNextInput($event, 'customer_id')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Notes -->
|
||||
<PosNotes :notes="notes" :is-loading="false" :isAddFormVisible="false" :isOldNotesVisible="true" class="mb-3"/>
|
||||
<!-- If the vehicle is registered to a customer -->
|
||||
<template v-if="vehicleObject">
|
||||
<!-- Divider -->
|
||||
<div class="divider">{{ SessionUser.objects.vehicles.meta.labels.single.toUpperCase() }}</div>
|
||||
<!-- Vehicle information -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<!-- Icon -->
|
||||
<div class="card-header-icon">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-car" aria-hidden="true"></i>
|
||||
</span>
|
||||
</div>
|
||||
<!-- Vehicle registration number -->
|
||||
<div class="card-header-title">{{ SessionUser.objects.products.functions.getProductName(parseInt(vehicleObject.type)) }}</div>
|
||||
<!-- Subscription -->
|
||||
<div class="card-header-icon">
|
||||
<span
|
||||
class="tag is-light"
|
||||
:class="{
|
||||
'is-success': vehicleObject.wash_subscription,
|
||||
'is-info': !vehicleObject.wash_subscription
|
||||
}"
|
||||
>
|
||||
{{vehicleObject.wash_subscription
|
||||
? SessionUser.objects.global.language.have
|
||||
: SessionUser.objects.global.language.have_not
|
||||
}} {{ SessionUser.objects.vehicles.columns.wash_subscription.label.toLowerCase() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Vehicle information -->
|
||||
<div class="card-content">
|
||||
<div class="content">
|
||||
<!-- Suggest order items -->
|
||||
<div class="columns">
|
||||
<!-- Last time, the vehicle was washed -->
|
||||
<div class="column is-6">
|
||||
<div class="divider">{{ SessionUser.objects.global.language.last_wash }}</div>
|
||||
<!-- Content of the last wash -->
|
||||
<order-content-table v-bind:order-id="vehicleObject.last_order_id" :displayPrice="false" :displayReference="false" :display-notes="false"/>
|
||||
<!-- Copy last wash content -->
|
||||
<div class="control mt-3">
|
||||
<button
|
||||
class="button is-small is-light is-fullwidth"
|
||||
>
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-copy" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span>{{ SessionUser.objects.global.language.copy }} {{ SessionUser.objects.global.language.last_wash.toLowerCase() }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Wash subscription -->
|
||||
<div class="column is-6">
|
||||
<div class="divider is-success">{{ SessionUser.objects.vehicles.columns.wash_subscription.label }}</div>
|
||||
<!-- Content of the wash subscription -->
|
||||
<table class="table is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ucFirst(SessionUser.objects.products.meta.labels.single)}}</th>
|
||||
<th>{{ucFirst(SessionUser.objects.global.language.quantity)}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ SessionUser.objects.products.functions.getProductName(parseInt(vehicleObject.type)) }}</td>
|
||||
<td>1</td>
|
||||
</tr>
|
||||
<template v-for="item in vehicleObject.addons.list" :key="item.id">
|
||||
<!-- If the item is a primary item, show it -->
|
||||
<tr>
|
||||
<!-- If the item is included in the invoice, show it normally -->
|
||||
<td>+ {{ item.product.name }}</td>
|
||||
<td>{{ item.amount }}</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Copy last wash subscription content -->
|
||||
<div class="control mt-3">
|
||||
<button
|
||||
class="button is-small is-light is-fullwidth is-success"
|
||||
>
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-copy" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span>{{ SessionUser.objects.global.language.copy }} {{ SessionUser.objects.vehicles.columns.wash_subscription.label.toLowerCase() }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Vehicle object -->
|
||||
<!--<pre>{{vehicleObject}}</pre>-->
|
||||
</template>
|
||||
<!-- If the vehicle is not registered to a customer -->
|
||||
<template v-else-if="!vehicleObject && reg_1.length > 0 && !isUserCurrentlyInFocusReg1">
|
||||
<div class="divider">{{ SessionUser.objects.global.language.customer }}</div>
|
||||
<customerSearchFieldPos />
|
||||
</template>
|
||||
<!-- Expandable content box for rarely used inputs -->
|
||||
<ExpandableContentBox
|
||||
v-bind:expanded="isOtherExpanded"
|
||||
@update:expanded="isOtherExpanded = $event"
|
||||
>
|
||||
<template #expandedContent>
|
||||
<!-- Divider -->
|
||||
<div class="divider">{{ SessionUser.objects.global.language.other }}</div>
|
||||
<!-- Customer search field (Located discreetly here, if the vehicle is registered to a customer) -->
|
||||
<CustomerSearchFieldPos v-if="vehicleObject" />
|
||||
<!-- Reference field -->
|
||||
<div class="field" v-if="!customerHasAttribute('requiresReferenceNumber')">
|
||||
<label class="label">{{SessionUser.objects.orders.columns.reference.label}}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
type="text"
|
||||
class="input has-sharp-edges"
|
||||
id="reference"
|
||||
:tabindex="4"
|
||||
v-model="reference"
|
||||
@keydown="keyDownNextInput($event, 'customer_id')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Add notes -->
|
||||
<PosNotes :notes="notes" :is-loading="false" :isAddFormVisible="SessionUser.adminUser" :isOldNotesVisible="false" :is-label-visible="true" class="mt-3" v-if="isCustomerSelected()" />
|
||||
</template>
|
||||
</ExpandableContentBox>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.input.has-sharp-edges {
|
||||
|
||||
@@ -1,53 +1,325 @@
|
||||
<script setup>
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { ref, watch } from "vue";
|
||||
import { reg_1, department_id, getCustomerName, searchAndSelectCustomer } from "@/components/shop/POSDepartmentProcess.vue";
|
||||
import { ref, watch, defineEmits } from "vue";
|
||||
import { reg_1, department_id, getCustomerName, searchAndSelectCustomer, customer_id, clearCache, clearCustomerSelection } from "@/components/shop/POSDepartmentProcess.vue"
|
||||
|
||||
// Define the matching customer default object
|
||||
const matching_customer_default = {
|
||||
id: null,
|
||||
number: null,
|
||||
name: null,
|
||||
const emit = defineEmits(['update:vehicleObject', 'update:focus']);
|
||||
|
||||
// Define the vehicle response object (Example)
|
||||
const vehicle_response_object = {
|
||||
id: 43, // Vehicle ID
|
||||
user_id: 1820, // User ID
|
||||
customer_id: 25266211, // Economic customer ID
|
||||
customer_name: "", // Customer name (From E-conomic)
|
||||
type: 5, // Vehicle primary product id
|
||||
reg: "EC25573", // License plate
|
||||
wash_subscription: true, // Subscription status
|
||||
addons: {
|
||||
enabled: 1, // How many addons are enabled
|
||||
available: 2, // How many addons are available
|
||||
list: [
|
||||
// List of enabled addons
|
||||
{
|
||||
id: 289, // Vehicle addon ID (Unique, not the same as the product ID or option ID)
|
||||
vehicle_id: 43, // Vehicle ID
|
||||
addon_id: 64, // Product option ID (Unique, not the same as the product ID or vehicle ID)
|
||||
amount: 1, // Amount of this addon
|
||||
product: {
|
||||
id: 24, // Product ID (Unique, not the same as the vehicle ID or option ID)
|
||||
name: "Spot Free- Lastbil", // Product name
|
||||
description: " ", // Product description
|
||||
price: 39, // Product (default) price
|
||||
subscription_allowed: 1, // True if subscription is allowed (This should be true for all products listed here.)
|
||||
category: "4", // Product category ID
|
||||
piktogram: "", // Product image URL, can be empty
|
||||
economic_product_id: "33", // Economic product ID
|
||||
apply_category_discount: false, // True if the product allows category, or E-conomic customer discount rules.
|
||||
requires_note: false, // True if the product requires a note when added to a vehicle
|
||||
created_at: "2024-12-09 14:31:49", // Product created date
|
||||
updated_at: "2025-04-08 14:07:41" // Product updated date
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// To prevent the searches from overriding more recent searches
|
||||
const current_search_id = ref(null);
|
||||
const register_new_search = () => {
|
||||
let search_id = Date.now();
|
||||
current_search_id.value = search_id;
|
||||
return search_id;
|
||||
};
|
||||
|
||||
const is_latest_search = (search_id) => {
|
||||
return search_id === current_search_id.value;
|
||||
};
|
||||
|
||||
// Define reactive properties
|
||||
const customers_matching = ref([]);
|
||||
const vehicles_matching = ref([]);
|
||||
|
||||
const onInputChange = (event) => {
|
||||
const inputValue = event.target.value;
|
||||
// Perform any necessary validation or processing on the input value
|
||||
console.log("Input value changed:", inputValue);
|
||||
|
||||
};
|
||||
|
||||
// Function to search for vehicles
|
||||
const searchVehicle = async ( inputValue, search_id ) => {
|
||||
// Check if the search ID is the latest
|
||||
if (!is_latest_search(search_id)) {
|
||||
console.log("Search ID is not the latest. Ignoring this search.");
|
||||
return;
|
||||
}
|
||||
// Set the isSearching flag to true
|
||||
isSearching.value = true;
|
||||
// Perform the search
|
||||
SessionUser.request(
|
||||
SessionUser.objects.vehicles.meta.endpoint,
|
||||
'GET',
|
||||
{
|
||||
search: inputValue,
|
||||
page: 1,
|
||||
limit: 10,
|
||||
}
|
||||
).then(response => {
|
||||
if (!is_latest_search(search_id)) {
|
||||
return
|
||||
}
|
||||
let result = response?.data?.data;
|
||||
if (result && result.length > 0) {
|
||||
// If there are results, set the vehicles_matching array
|
||||
vehicles_matching.value = result;
|
||||
} else {
|
||||
// If no results, clear the vehicles_matching array
|
||||
vehicles_matching.value = [];
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error("Error:", error, search_id);
|
||||
}).finally(() => {
|
||||
console.log("Finish search", search_id);
|
||||
if (is_latest_search(search_id)) {
|
||||
// Set the isSearching flag to false
|
||||
isSearching.value = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getVehicleObjectFromId = (id) => {
|
||||
// Find the vehicle object by ID
|
||||
return vehicles_matching.value.find(vehicle => vehicle.id === id);
|
||||
};
|
||||
|
||||
// Watch for changes in reg_1
|
||||
watch(reg_1, (newValue) => {
|
||||
// Define the search ID for this input change
|
||||
const search_id = register_new_search();
|
||||
console.log("reg_1 changed:", newValue);
|
||||
// Perform any necessary actions when reg_1 changes
|
||||
unselectCustomerOnChange();
|
||||
emitVehicleObject(null);
|
||||
// Check if the new value is empty, if so, clear the vehicles_matching array
|
||||
if (!newValue) {
|
||||
vehicles_matching.value = [];
|
||||
// Set the isSearching flag to false, as no search is performed
|
||||
if (is_latest_search(search_id)) {
|
||||
// Set the isSearching flag to false
|
||||
isSearching.value = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Perform the search
|
||||
searchVehicle(
|
||||
newValue,
|
||||
search_id
|
||||
);
|
||||
});
|
||||
|
||||
const unselectCustomerOnChange = () => {
|
||||
clearCustomerSelection();
|
||||
};
|
||||
|
||||
const selectVehicle = (vehicle_id) => {
|
||||
// Find the vehicle object by ID
|
||||
const vehicle = getVehicleObjectFromId(vehicle_id);
|
||||
if (vehicle) {
|
||||
// If a vehicle is found, set the reg_1 value to the vehicle's reg
|
||||
reg_1.value = vehicle.reg;
|
||||
// Optionally, you can also set other properties like customer_id, etc.
|
||||
customer_id.value = vehicle.customer_id;
|
||||
department_id.value = vehicle.type;
|
||||
getCustomerName(vehicle.customer_id);
|
||||
searchAndSelectCustomer(vehicle.customer_id);
|
||||
// Emit the vehicle object to the parent component
|
||||
emitVehicleObject(vehicle);
|
||||
}
|
||||
};
|
||||
|
||||
const getSearchIndexByVehicleId = (vehicle_id) => {
|
||||
// Find the index of the vehicle in the vehicles_matching array
|
||||
return vehicles_matching.value.findIndex(vehicle => vehicle.id === vehicle_id);
|
||||
};
|
||||
|
||||
const selectDropdownItem = (index) => {
|
||||
// Select a vehicle from the dropdown
|
||||
if (vehicles_matching.value[index]) {
|
||||
selectVehicle(vehicles_matching.value[index].id);
|
||||
} else {
|
||||
emitVehicleObject(null);
|
||||
}
|
||||
// Go to the next input field
|
||||
focusNextField();
|
||||
};
|
||||
|
||||
const keyDownNextTabIndexButton = (event, tabIndex) => {
|
||||
// Handle keydown event to navigate to the next button
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
const button = document.querySelector(`button[tabindex="${tabIndex}"]`);
|
||||
if (button) {
|
||||
button.click();
|
||||
}
|
||||
}
|
||||
};
|
||||
const arrowKeyHandler = (event) => {
|
||||
// Handle arrow key navigation in the dropdown
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
if (selectedDropdownItem.value < vehicles_matching.value.length - 1) {
|
||||
selectedDropdownItem.value++;
|
||||
}
|
||||
}
|
||||
if (event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
if (selectedDropdownItem.value > -1) {
|
||||
selectedDropdownItem.value--;
|
||||
}
|
||||
}
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
selectDropdownItem(selectedDropdownItem.value);
|
||||
selectedDropdownItem.value = -1;
|
||||
// Imitate the tab key press to move to the next input
|
||||
focusNextField();
|
||||
}
|
||||
if (event.key === 'Tab') {
|
||||
event.preventDefault();
|
||||
selectDropdownItem(selectedDropdownItem.value);
|
||||
selectedDropdownItem.value = -1;
|
||||
// Set the active input to the next field
|
||||
focusNextField();
|
||||
}
|
||||
};
|
||||
const selectedDropdownItem = ref(-1);
|
||||
const showSelector = ref(false);
|
||||
const lostfocus = () => {
|
||||
emitFocus(false);
|
||||
// Delay to prevent the dropdown from closing immediately
|
||||
setTimeout(() => {
|
||||
showSelector.value = false;
|
||||
}, 200);
|
||||
// Check if the current input value is in the vehicles_matching array
|
||||
const currentValue = reg_1.value;
|
||||
const vehicle = vehicles_matching.value.find(vehicle => vehicle.reg === currentValue);
|
||||
if (vehicle) {
|
||||
// If the vehicle is found, set the reg_1 value to the vehicle's reg
|
||||
reg_1.value = vehicle.reg;
|
||||
customer_id.value = vehicle.customer_id;
|
||||
department_id.value = vehicle.type;
|
||||
getCustomerName(vehicle.customer_id);
|
||||
searchAndSelectCustomer(vehicle.customer_id);
|
||||
// Emit the vehicle object to the parent component
|
||||
emitVehicleObject(vehicle);
|
||||
} else {
|
||||
// If not found, clear the vehicles_matching array
|
||||
vehicles_matching.value = [];
|
||||
emitVehicleObject(null);
|
||||
}
|
||||
};
|
||||
const isSearching = ref(false);
|
||||
|
||||
// Watch for changes in the vehicles_matching array, to check if the current index is valid
|
||||
watch(vehicles_matching, (newValue) => {
|
||||
// Check if the input directly matches an item in the vehicles_matching array (Then we can automatically select it)
|
||||
const currentValue = reg_1.value;
|
||||
const vehicle = newValue.find(vehicle => vehicle.reg === currentValue);
|
||||
if (vehicle) {
|
||||
// If the vehicle is found, set the reg_1 value to the vehicle's reg
|
||||
reg_1.value = vehicle.reg;
|
||||
customer_id.value = vehicle.customer_id;
|
||||
department_id.value = vehicle.type;
|
||||
getCustomerName(vehicle.customer_id);
|
||||
searchAndSelectCustomer(vehicle.customer_id);
|
||||
// Emit the vehicle object to the parent component
|
||||
emitVehicleObject(vehicle);
|
||||
}
|
||||
// Check if the selectedDropdownItem index is valid
|
||||
if (selectedDropdownItem.value >= newValue.length) {
|
||||
// Set the selectedDropdownItem to the last index if it exceeds the new length
|
||||
selectedDropdownItem.value = newValue.length - 1;
|
||||
}
|
||||
});
|
||||
|
||||
const focusNextField = () => {
|
||||
// Focus on the next input field
|
||||
const nextField = document.querySelector('input[tabindex="2"]');
|
||||
if (nextField) {
|
||||
nextField.focus();
|
||||
}
|
||||
};
|
||||
|
||||
// Function to emit the vehicle object to the parent component
|
||||
const emitVehicleObject = (vehicle) => {
|
||||
// Emit the vehicle object
|
||||
emit('update:vehicleObject', vehicle);
|
||||
};
|
||||
|
||||
const emitFocus = (isFocused) => {
|
||||
// Emit the focus event
|
||||
emit('update:focus', isFocused);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- License Plate Registration 1 Input -->
|
||||
<div class="field">
|
||||
<div class="control" :class="{'is-loading': isSearching}">
|
||||
<input
|
||||
type="text"
|
||||
class="input has-sharp-edges"
|
||||
class="input has-sharp-edges is-loading"
|
||||
v-model="reg_1"
|
||||
@focus="showSelector = true; emitFocus(true)"
|
||||
@blur="lostfocus"
|
||||
@keydown="arrowKeyHandler"
|
||||
id="reg_1"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<!-- Matching Customers, if multiple -->
|
||||
<template v-if="1 === 1">
|
||||
<!-- Divider -->
|
||||
<div class="divider">{{SessionUser.objects.global.language.customer}}</div>
|
||||
<!-- Matching Customers -->
|
||||
<div class="columns is-multiline">
|
||||
<template v-for="matching_customer in customers_matching">
|
||||
<div class="column is-4">
|
||||
{{ matching_customer }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Dropdown for license plate suggestions -->
|
||||
<div
|
||||
class="dropdown"
|
||||
:class="{'is-active': showSelector}"
|
||||
v-show="showSelector && vehicles_matching.length > 0"
|
||||
>
|
||||
<div class="dropdown-menu">
|
||||
<div class="dropdown-content">
|
||||
<a
|
||||
class="dropdown-item is-clickable"
|
||||
v-for="result in vehicles_matching"
|
||||
:key="result.id"
|
||||
@click="selectVehicle(result.id)"
|
||||
:class="{
|
||||
'is-active': getSearchIndexByVehicleId(result.id) === selectedDropdownItem,
|
||||
'is-drop-down-selected': getSearchIndexByVehicleId(result.customerNumber) === selectedDropdownItem,
|
||||
}">
|
||||
{{ result.reg }} - {{ result.customer_name }} {{ (result.wash_subscription ? ' ( VA )' : '') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup>
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { ref, watch, defineEmits } from "vue";
|
||||
import { reg_1, department_id, getCustomerName, searchAndSelectCustomer, customer_id, clearCache, clearCustomerSelection } from "@/components/shop/POSDepartmentProcess.vue"
|
||||
|
||||
const props = defineProps({
|
||||
showSuggestions: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const suggestions = ref([]);
|
||||
|
||||
const onChange = () => {
|
||||
if (props.showSuggestions) {
|
||||
// Show suggestions
|
||||
console.log("Showing suggestions");
|
||||
}
|
||||
}
|
||||
|
||||
const onClick = (suggestion) => {
|
||||
// Handle click on suggestion
|
||||
console.log("Clicked suggestion:", suggestion);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="tags">
|
||||
<span
|
||||
v-for="(suggestion, index) in suggestions"
|
||||
:key="index"
|
||||
class="tag is-link is-light"
|
||||
@click="onClick(suggestion)"
|
||||
>
|
||||
{{ suggestion }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,106 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { customer_id, isCustomerBarred, customer_name, selectCustomer, isCustomerSelected } from "@/components/shop/POSDepartmentProcess.vue";
|
||||
import CustomerSearchField from "@/components/search/economic/customerSearchField.vue";
|
||||
import { searchCustomerResults } from "@/components/search/economic/customerSearch.vue";
|
||||
import { isSearching } from "@/components/search/economic/customerSearch.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
const selectedDropdownItem = ref(-1);
|
||||
const showSelector = ref(false);
|
||||
|
||||
const arrowKeyHandler = (event) => {
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
if (selectedDropdownItem.value < searchCustomerResults.value.length - 1) {
|
||||
selectedDropdownItem.value++;
|
||||
}
|
||||
}
|
||||
if (event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
if (selectedDropdownItem.value > -1) {
|
||||
selectedDropdownItem.value--;
|
||||
}
|
||||
}
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
selectCustomer(searchCustomerResults.value[selectedDropdownItem.value]);
|
||||
selectedDropdownItem.value = -1;
|
||||
// Set the active input to the reference field
|
||||
document.getElementById('reference').focus();
|
||||
}
|
||||
};
|
||||
|
||||
const getSearchIndexByCustomerNumber = (customerNumber) => {
|
||||
return searchCustomerResults.value.findIndex((result) => result.customerNumber === customerNumber);
|
||||
};
|
||||
|
||||
// This delay is needed to prevent the dropdown from closing when clicking on a dropdown item (Making the dropdown item clickable)
|
||||
const lostfocus = () => {
|
||||
setTimeout(() => {
|
||||
showSelector.value = false;
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const keyDownNextInput = (event, field_id) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
document.getElementById(field_id).focus();
|
||||
}
|
||||
};
|
||||
|
||||
const keyDownNextTabIndexButton = (event, tabIndex) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
// First unfocus the input field. This is to ensure the sync between the input field and the POSDepartmentProcess.vue data.
|
||||
event.target.blur();
|
||||
document.querySelector(`button[tabindex="${tabIndex}"]`).click();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="field" :class="{'is-hidden': isCustomerSelected()}">
|
||||
<label class="label">{{SessionUser.objects.global.language.customer}}</label>
|
||||
<div class="control" :class="{'is-loading': isSearching}">
|
||||
<CustomerSearchField
|
||||
:v-model="customer_id"
|
||||
class="input has-sharp-edges"
|
||||
type="text"
|
||||
:disabled="isCustomerSelected()"
|
||||
@keydown="arrowKeyHandler"
|
||||
@focusout="selectedDropdownItem = -1; lostfocus()"
|
||||
@focusin="showSelector = true"
|
||||
:tabindex="isCustomerSelected() ? -1 : 0"
|
||||
autocomplete="off"
|
||||
id="pos_select_customer_input"
|
||||
/>
|
||||
</div>
|
||||
<div class="dropdown" :class="{'is-active': showSelector && searchCustomerResults.length > 0}">
|
||||
<div class="dropdown-menu">
|
||||
<div class="dropdown-content">
|
||||
<a class="dropdown-item customer-drop-down-select" v-for="result in searchCustomerResults" :key="result.id" @click="selectCustomer(result)" :class="{'is-active': getSearchIndexByCustomerNumber(result.customerNumber) === selectedDropdownItem, 'is-drop-down-selected': getSearchIndexByCustomerNumber(result.customerNumber) === selectedDropdownItem, 'has-text-warning': isCustomerBarred(result)}">
|
||||
{{ result.name }} - {{ result.customerNumber }} {{ (isCustomerBarred(result) ? ' (Spærret)' : '') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Stripe payment button -->
|
||||
<button class="button is-text mt-1" @click="selectCustomer({name: 'Stripe Payment', customerNumber: 999})" :tabindex="isCustomerSelected() ? 1 : -1" :class="{'is-hidden': isCustomerSelected()}" style="text-decoration-line: none;">
|
||||
Vælg direkte betaling med betalingskort
|
||||
</button>
|
||||
</div>
|
||||
<div class="field has-addons-right has-addons mt-1" :class="{'is-hidden': !isCustomerSelected()}">
|
||||
<p class="control is-expanded">
|
||||
<input class="input has-sharp-edges" type="text" v-model="customer_name" disabled />
|
||||
</p>
|
||||
<p class="control">
|
||||
<button class="button is-danger" @click="selectCustomer(null)" :tabindex="isCustomerSelected() ? 1 : -1">
|
||||
Clear
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -19,9 +19,14 @@ export const ObjectsGlobal = {
|
||||
split: "Opdel",
|
||||
save: "Gem",
|
||||
invalid: "Ugyldig",
|
||||
last_wash: "Sidste vask",
|
||||
have_not: "Har ikke",
|
||||
have: "Har",
|
||||
hide: "Skjul",
|
||||
cancel: "Annuller",
|
||||
select: "Vælg",
|
||||
month: "Måned",
|
||||
copy: "Kopier",
|
||||
yes: "Ja",
|
||||
no: "Nej",
|
||||
email: "Email",
|
||||
|
||||
@@ -441,21 +441,11 @@ export const selectScan = (scan) => {
|
||||
showSelectedScanDialog(scan);
|
||||
console.log(scan);
|
||||
// Check which registration number is empty
|
||||
if (!Object.keys(reg_1.value).length > 0 || !reg_1.value) {
|
||||
//if (!Object.keys(reg_1.value).length > 0 || !reg_1.value) {
|
||||
// Set the registration number 1
|
||||
reg_1.value = scan.plate;
|
||||
return;
|
||||
}
|
||||
if (!reg_2.value) {
|
||||
// Set the registration number 2
|
||||
reg_2.value = scan.plate;
|
||||
return;
|
||||
}
|
||||
if (!reg_3.value) {
|
||||
// Set the registration number 3
|
||||
reg_3.value = scan.plate;
|
||||
return;
|
||||
}
|
||||
//reg_1.value = scan.plate;
|
||||
//return;
|
||||
//}
|
||||
// Set the registration number 1
|
||||
reg_1.value = scan.plate;
|
||||
};
|
||||
@@ -489,6 +479,7 @@ export const searchAndSelectCustomer = async (customerNumber) => {
|
||||
|
||||
/** Show selected scan dialog */
|
||||
export const showSelectedScanDialog = (scan) => {
|
||||
return;
|
||||
// Show a loading swal
|
||||
Swal.fire({
|
||||
title: 'Henter nummerplade',
|
||||
@@ -688,9 +679,7 @@ export const clearCache = () => {
|
||||
// Reset the query parameters
|
||||
window.history.pushState({}, '', `?step=1`);
|
||||
// Clear the customer data
|
||||
customer_id.value = '';
|
||||
customer_name.value = '';
|
||||
customer_data.value = [];
|
||||
clearCustomerSelection();
|
||||
// Clear the order data
|
||||
order_id.value = null;
|
||||
order_items.value = [];
|
||||
@@ -698,13 +687,20 @@ export const clearCache = () => {
|
||||
reg_1.value = '';
|
||||
reg_2.value = '';
|
||||
reg_3.value = '';
|
||||
// Clear the scans
|
||||
scans.value = [];
|
||||
};
|
||||
|
||||
export const clearCustomerSelection = () => {
|
||||
// Clear the customer data
|
||||
customer_id.value = '';
|
||||
customer_name.value = '';
|
||||
customer_data.value = [];
|
||||
// Clear the notes
|
||||
notes.value = [];
|
||||
order_notes.value = '';
|
||||
// Clear the attributes
|
||||
customer_attributes.value = [];
|
||||
// Clear the scans
|
||||
scans.value = [];
|
||||
};
|
||||
|
||||
/** Show delete order dialog */
|
||||
|
||||
Reference in New Issue
Block a user