Add customer suggestions and selectable invoices features

Introduced customer suggestion functionality for unregistered vehicles based on `reg_1`. Enabled multiple invoice selection with toggle option in order tables. Updated visual styles and handling of errors with automatic clearing.
This commit is contained in:
Jepp9350
2025-04-30 13:05:19 +02:00
parent 6c5818615b
commit 991d516fd3
16 changed files with 588 additions and 112 deletions
+6
View File
@@ -13,6 +13,7 @@
"@sweetalert2/theme-bulma": "^5.0.18",
"@sweetalert2/theme-dark": "^5.0.18",
"@vueuse/core": "^12.7.0",
"animate.css": "^4.1.1",
"axios": "^1.7.8",
"bulma": "^1.0.2",
"bulma-calendar": "^7.1.1",
@@ -2718,6 +2719,11 @@
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/animate.css": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz",
"integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ=="
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+1
View File
@@ -14,6 +14,7 @@
"@sweetalert2/theme-bulma": "^5.0.18",
"@sweetalert2/theme-dark": "^5.0.18",
"@vueuse/core": "^12.7.0",
"animate.css": "^4.1.1",
"axios": "^1.7.8",
"bulma": "^1.0.2",
"bulma-calendar": "^7.1.1",
@@ -40,12 +40,15 @@ const highestPage = ref(0); // The highest page that has been loaded (This is us
const limitReached = ref(false);
/** Fetch the scans when the component is mounted */
onMounted(() => {
const onSetup = () => {
limitReached.value = false;
setEndpoint("/numberplatescans", false);
setFilter('department_id', parseInt(getDepartment()), false);
setOrder('created_at', 'desc', false);
setMetaItemsPerPage(10, false);
}
onMounted(() => {
onSetup();
});
/** Whenever the search changes, reset the infinite scroll */
@@ -8,6 +8,7 @@ import {popperBox, popper, removePopperIfOpen, showPopperWithContent, showPopper
import RequiresPermission from "@/components/displays/permissionbased/RequiresPermission.vue";
import CustomerDiscountsDepartmentDisplay
from "@/components/displays/department/pos/displays/CustomerDiscountsDepartmentDisplay.vue";
import ExpandableContentBox from "@/components/displays/boxes/ExpandableContentBox.vue";
const panel_tabs = ref([
{
@@ -160,6 +161,12 @@ const hasAttribute = (prop) => {
};
/** Fetch the customer attributes when the component is mounted */
loadCustomerAttributes();
const isCustomerDetailsExpanded = ref(false);
const customer_data_has_empty_details = () => {
return details.value.some(detail => customer_data.value[detail.prop] === null || customer_data.value[detail.prop] === '');
};
</script>
<template>
@@ -189,12 +196,21 @@ loadCustomerAttributes();
</div>
</div>
</div>
<a class="panel-block" v-if="panel_tabs[0].active" v-for="detail in details" :key="detail.name">
<span class="panel-icon">
<i :class="detail.icon" aria-hidden="true"></i>
</span>
<span style="width: 50%;">{{ detail.name }}</span><span class="has-text-right" style="width: 50%;">{{ customer_data[detail.prop] }}</span>
</a>
<template v-if="panel_tabs[0].active" v-for="detail in details" :key="detail.name">
<!-- Details that's not empty (unless isCustomerDetailsExpanded is true) -->
<a class="panel-block" v-if="customer_data[detail.prop] || isCustomerDetailsExpanded">
<span class="panel-icon">
<i :class="detail.icon" aria-hidden="true"></i>
</span>
<span style="width: 50%;">{{ detail.name }}</span><span class="has-text-right" style="width: 50%;">{{ customer_data[detail.prop] }}</span>
</a>
</template>
<!-- Details that's empty, (If there are any) -->
<ExpandableContentBox
v-if="panel_tabs[0].active && customer_data_has_empty_details"
@update:expanded="isCustomerDetailsExpanded = !isCustomerDetailsExpanded"
v-bind:expanded="isCustomerDetailsExpanded"
/>
<!-- Attributes -->
<RequiresPermission permission="list_customer_attributes">
<div class="panel-block" v-if="panel_tabs[1].active" v-for="attribute in attributes" :key="attribute.name">
@@ -10,6 +10,10 @@ const props = defineProps({
invoiceView: {
type: Boolean,
default: false,
},
allowSelectMultiple: {
type: Boolean,
default: false,
}
});
import { ref } from 'vue';
@@ -290,12 +294,68 @@ const isDropdownActive = (object) => {
// Check if the object is in the active dropdown list
return active_dropdown_object_ids.value.includes(object.id);
};
/**
* Invoice collection selection
*/
const selectedInvoiceCollections = ref([]);
const toggleInvoiceCollectionSelection = (invoiceCollectionId) => {
// Check if the invoice collection is already selected
if (selectedInvoiceCollections.value.includes(invoiceCollectionId)) {
// If it is, remove it from the list
selectedInvoiceCollections.value = selectedInvoiceCollections.value.filter(id => id !== invoiceCollectionId);
} else {
// If it is not, add it to the list
selectedInvoiceCollections.value.push(invoiceCollectionId);
}
};
const isInvoiceCollectionSelected = (invoiceCollectionId) => {
// Check if the invoice collection is in the selected list
return selectedInvoiceCollections.value.includes(invoiceCollectionId);
};
const isInvoiceCollectionSelectedAll = () => {
// Check if all invoice collections are selected
return selectedInvoiceCollections.value.length === props.orders.length;
};
const selectAllInvoiceCollections = () => {
// Check if all invoice collections are selected
if (isInvoiceCollectionSelectedAll()) {
// If they are, deselect all
selectedInvoiceCollections.value = [];
} else {
// If they are not, select all
selectedInvoiceCollections.value = props.orders.map(order => order.invoice_collection_id);
}
};
</script>
<template>
<table class="table is-fullwidth is-hoverable">
<thead>
<tr>
<td>
<p>
{{selectedInvoiceCollections.length}}
</p>
</td>
<td colspan="90%">
<code>{{selectedInvoiceCollections}}</code>
</td>
</tr>
<tr>
<th class="is-narrow" v-if="props.invoiceView && props.allowSelectMultiple">
<button
class="button is-small"
@click="selectAllInvoiceCollections()"
:class="{
'is-light': !isInvoiceCollectionSelectedAll(),
'is-dark': isInvoiceCollectionSelectedAll()
}"
>
{{ isInvoiceCollectionSelectedAll() ? SessionUser.objects.global.language.unselect : SessionUser.objects.global.language.select }} {{ SessionUser.objects.global.language.all.toLowerCase() }}
</button>
</th>
<th class="is-narrow" v-if="props.invoiceView"></th>
<th class="status-bar-table-header"></th>
<template v-for="tableHeaders in tableHeaders" :key="tableHeaders">
@@ -335,6 +395,20 @@ const isDropdownActive = (object) => {
<tbody>
<template v-for="order in orders" :key="order.id">
<tr>
<template v-if="props.invoiceView && props.allowSelectMultiple">
<td>
<button
class="button is-small"
@click="toggleInvoiceCollectionSelection(order.invoice_collection_id)"
:class="{
'is-light': !isInvoiceCollectionSelected(parseInt(order.invoice_collection_id)),
'is-dark': isInvoiceCollectionSelected(parseInt(order.invoice_collection_id))
}"
>
{{ isInvoiceCollectionSelected(parseInt(order.invoice_collection_id)) ? SessionUser.objects.global.language.unselect : SessionUser.objects.global.language.select }}
</button>
</td>
</template>
<template v-if="props.invoiceView">
<td>
<button
@@ -60,6 +60,18 @@ watch(() => isCustomerSelected(), (newValue) => {
}
});
// attempt to focus on the reg_1 input field
const focusOnReg1 = () => {
setTimeout(() => {
const reg1Input = document.getElementById("reg_1");
if (reg1Input) {
reg1Input.focus();
}
}, 100);
};
focusOnReg1();
</script>
<template>
@@ -151,7 +163,7 @@ watch(() => isCustomerSelected(), (newValue) => {
</ElementTabsBox>
</template>
</WhiteBox>
<NextStepError class="is-fullwidth" />
<NextStepError class="is-fullwidth" :clear-automatically="true" :clear-delay="8000"/>
<ButtonsBox>
<NextStep class="is-fullwidth" tabindex="6"/>
</ButtonsBox>
@@ -411,7 +411,7 @@ const getTimeShortcutIcon = (boolean) => {
</div>
</template>
</PaginationDisplay>
<OrdersTable :orders="list" :invoiceView="props.invoiceView"/>
<OrdersTable :orders="list" :invoiceView="props.invoiceView" :allowSelectMultiple="true"/>
<PaginationNavigation :currentPage="metaCurrentPage" :totalPages="Math.ceil(metaTotalItems / metaItemsPerPage)" :loadFunction="loadList" :setPage="setPage" :isLoading="isLoading" />
<LoadButtonWhileAwait class="is-dark" :isLoading="isLoading" :loadFunction="loadList" icon="fas fa-sync-alt">Reload</LoadButtonWhileAwait>
</template>
@@ -1,6 +1,6 @@
<script setup>
import { ref } from 'vue';
import { customer_id, notes, isCustomerBarred, customer_name, department_id, reference, reg_1, reg_2, reg_3, selectCustomer, isCustomerSelected, customer_attributes } from "@/components/shop/POSDepartmentProcess.vue";
import { customer_id, notes, isCustomerBarred, customer_name, department_id, reference, reg_1, reg_2, reg_3, searchAndSelectCustomer, selectCustomer, isCustomerSelected, customer_attributes } from "@/components/shop/POSDepartmentProcess.vue";
import CustomerSearchField from "@/components/search/economic/customerSearchField.vue";
import { searchCustomerResults } from "@/components/search/economic/customerSearch.vue";
import PosNotes from "@/components/displays/department/pos/PosNotes.vue";
@@ -13,6 +13,7 @@ import ExpandableContentBox from "@/components/displays/boxes/ExpandableContentB
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";
import VehicleCustomerSuggestionsPos from "@/components/forms/department/pos/input/vehicleCustomerSuggestionsPos.vue";
const isRegistrationNumbersExpanded = ref(false);
const isOtherExpanded = ref(false);
@@ -132,23 +133,38 @@ const customerHasAttribute = (attribute) => {
</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">
<!-- If the vehicle is not registered to a customer -->
<template v-if="(!vehicleObject || !vehicleObject.customer_id) && reg_1.length > 0">
<div class="divider">{{SessionUser.objects.global.language.select}} {{ SessionUser.objects.global.language.customer }}</div>
<customerSearchFieldPos />
<vehicleCustomerSuggestionsPos
v-bind:reg_1="reg_1"
@customerSelected="searchAndSelectCustomer"
/>
</template>
<!-- Expandable content box for rarely used inputs -->
<ExpandableContentBox
v-bind:expanded="isOtherExpanded"
@update:expanded="isOtherExpanded = $event"
>
<template #expandedContent>
<!-- If the vehicle is registered to a customer -->
<template v-if="vehicleObject && vehicleObject.customer_id">
<!-- 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">
</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="{
@@ -157,90 +173,79 @@ const customerHasAttribute = (attribute) => {
}"
>
{{vehicleObject.wash_subscription
? SessionUser.objects.global.language.have
: SessionUser.objects.global.language.have_not
? 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"
</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-auto-fill" v-if="vehicleObject.last_order_id">
<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.vehicles.columns.wash_subscription.label.toLowerCase() }}</span>
</button>
<span>{{ SessionUser.objects.global.language.copy }} {{ SessionUser.objects.global.language.last_wash.toLowerCase() }}</span>
</button>
</div>
</div>
<!-- Wash subscription -->
<div class="column is-6" v-if="vehicleObject.wash_subscription">
<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>
</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>
<!-- Vehicle object -->
<!--<pre>{{vehicleObject}}</pre>-->
</template>
<!-- Divider -->
<div class="divider">{{ SessionUser.objects.global.language.other }}</div>
<!-- Customer search field (Located discreetly here, if the vehicle is registered to a customer) -->
@@ -5,14 +5,14 @@ const props = defineProps(['label']);
const label = props.label || 'Næste';
import { Colors } from "@/ThemeConfig.vue";
import 'animate.css/animate.min.css';
</script>
<template>
<button
class="button"
class="button slide-green-on-hover-left-to-right has-text-white"
@click="nextStep" v-if="nextStepDelay === 0"
:style="{ 'background-color': Colors.buttons.warning.backgroundColor, 'color': Colors.buttons.warning.textColor }"
>
{{ label }}</button>
<button class="button is-loading" v-else>Please wait... {{ nextStepDelay }}</button>
@@ -20,4 +20,36 @@ import { Colors } from "@/ThemeConfig.vue";
<style scoped>
.button.slide-green-on-hover-left-to-right {
transition: all .3s;
overflow: hidden;
z-index: 1;
&:after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #f0ad4e; /* yellow */
z-index: -2;
}
&:before {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 0%;
height: 100%;
background-color: #45a049; /* green */
transition: all .3s;
z-index: -1;
}
&:hover {
&:before {
width: 100%;
}
}
}
</style>
@@ -1,10 +1,92 @@
<script setup>
import { getError } from "@/components/request/HandleGlobalError.vue";
import {clearErrors, getError} from "@/components/request/HandleGlobalError.vue";
import {defineProps, ref, watch} from 'vue';
const props = defineProps({
clearAutomatically: {
type: Boolean,
default: false,
},
allowClear: {
type: Boolean,
default: true,
},
clearDelay: {
type: Number,
default: 5000,
},
});
const timer_start_ms = ref(null);
const onClick = () => {
if (props.allowClear) {
clearErrors();
}
};
if (props.clearAutomatically) {
watch(() => getError('stepError'), (newValue) => {
if (newValue && props.allowClear) {
console.log("Clearing error after delay", props.clearDelay);
timer_start_ms.value = Date.now();
setTimeout(() => {
console.log("Clearing error");
clearErrors();
}, props.clearDelay);
}
});
}
const progress_value_percent = ref(0);
const getProgressValue = () => {
if (!timer_start_ms.value) {
return 100;
}
if (!props.allowClear) {
return 100;
}
if (props.clearAutomatically && timer_start_ms.value) {
// Check if the duration has passed, if so, return 100
if (Date.now() - timer_start_ms.value >= props.clearDelay) {
// Clear the timer
timer_start_ms.value = null;
return 100;
}
// Calculate the progress percentage
const elapsed = Date.now() - timer_start_ms.value;
return Math.min((elapsed / props.clearDelay) * 100, 100);
}
// If not clearing automatically, return 100
return 100;
};
const updateProgress = () => {
progress_value_percent.value = getProgressValue();
};
// Update the progress value every 100ms
setInterval(() => {
progress_value_percent.value = getProgressValue();
}, 10);
</script>
<template>
<div class="notification is-danger" v-if="getError('stepError')">
<p><strong>{{ getError('stepError') }}</strong></p>
<div class="mb-3" v-if="getError('stepError')">
<div class="notification is-danger mb-0" style="border-bottom-left-radius: 0; border-bottom-right-radius: 0;">
<button class="delete" @click="onClick" v-if="props.allowClear"></button>
<p><strong>{{ getError('stepError') }}</strong></p>
</div>
<!-- Progress bar, before the error message deletes automatically -->
<div class="is-fixed-bottom has-text-centered mt-0" style="border-bottom-left-radius: 6px; border-bottom-right-radius: 6px;">
<progress
class="progress is-danger"
style="border-radius: 0 0 6px 6px;"
:value="progress_value_percent"
:max="100"
></progress>
</div>
</div>
</template>
@@ -43,6 +43,26 @@ const vehicle_response_object = {
}
}
const get_unregistered_vehicle_object = (reg) => {
// Generate an id for the unregistered vehicle, this is only used for the dropdown
let id_prefix = 999999999999;
let id = id_prefix + Math.floor(Math.random() * 1000000);
return {
id: id,
user_id: null,
customer_id: null,
customer_name: "",
type: null,
reg: reg,
wash_subscription: false,
addons: {
enabled: 0,
available: 0,
list: []
}
};
}
// To prevent the searches from overriding more recent searches
const current_search_id = ref(null);
const register_new_search = () => {
@@ -95,6 +115,7 @@ const searchVehicle = async ( inputValue, search_id ) => {
// If no results, clear the vehicles_matching array
vehicles_matching.value = [];
}
getUnregisteredVehicleObjects(inputValue, search_id);
}).catch(error => {
console.error("Error:", error, search_id);
}).finally(() => {
@@ -106,6 +127,54 @@ const searchVehicle = async ( inputValue, search_id ) => {
});
};
const getUnregisteredVehicleObjects = (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;
}
// If the input value is empty, clear the vehicles_matching array
if (!inputValue) {
vehicles_matching.value = [];
return;
}
SessionUser.request(
'/department/vehicles/unknown-customer',
'GET',
{
search: inputValue,
page: 1,
limit: 10,
}
).then(response => {
console.log("Unregistered vehicles:", response.data.data);
if (!is_latest_search(search_id)) {
return
}
let result = response?.data?.data;
let unregistered_vehicles = [];
if (result && result.length > 0) {
// If there are results, parse them into unregistered_vehicles
for (let i = 0; i < result.length; i++) {
unregistered_vehicles.push(get_unregistered_vehicle_object(result[i].reg_1));
}
// Set the vehicles_matching array to include both registered and unregistered vehicles
vehicles_matching.value = [...vehicles_matching.value, ...unregistered_vehicles];
} 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);
@@ -149,7 +218,11 @@ const selectVehicle = (vehicle_id) => {
customer_id.value = vehicle.customer_id;
department_id.value = vehicle.type;
getCustomerName(vehicle.customer_id);
searchAndSelectCustomer(vehicle.customer_id);
if (vehicle.customer_id) {
// If the vehicle has a customer ID, search and select the customer
// This is to prevent the unregistered vehicle from trying to select an unknown customer
searchAndSelectCustomer(vehicle.customer_id);
}
// Emit the vehicle object to the parent component
emitVehicleObject(vehicle);
}
@@ -227,7 +300,11 @@ const lostfocus = () => {
customer_id.value = vehicle.customer_id;
department_id.value = vehicle.type;
getCustomerName(vehicle.customer_id);
searchAndSelectCustomer(vehicle.customer_id);
if (vehicle.customer_id) {
// If the vehicle has a customer ID, search and select the customer
// This is to prevent the unregistered vehicle from trying to select an unknown customer
searchAndSelectCustomer(vehicle.customer_id);
}
// Emit the vehicle object to the parent component
emitVehicleObject(vehicle);
} else {
@@ -249,7 +326,11 @@ watch(vehicles_matching, (newValue) => {
customer_id.value = vehicle.customer_id;
department_id.value = vehicle.type;
getCustomerName(vehicle.customer_id);
searchAndSelectCustomer(vehicle.customer_id);
if (vehicle.customer_id) {
// If the vehicle has a customer ID, search and select the customer
// This is to prevent the unregistered vehicle from trying to select an unknown customer
searchAndSelectCustomer(vehicle.customer_id);
}
// Emit the vehicle object to the parent component
emitVehicleObject(vehicle);
}
@@ -315,7 +396,7 @@ const emitFocus = (isFocused) => {
'is-active': getSearchIndexByVehicleId(result.id) === selectedDropdownItem,
'is-drop-down-selected': getSearchIndexByVehicleId(result.customerNumber) === selectedDropdownItem,
}">
{{ result.reg }} - {{ result.customer_name }} {{ (result.wash_subscription ? ' ( VA )' : '') }}
{{ result.reg }}{{ result.customer_name ? (' - ' + result.customer_name ) : '' }}{{ (result.wash_subscription ? ' ( VA ) ' : '') }}
</a>
</div>
</div>
@@ -95,7 +95,7 @@ const keyDownNextTabIndexButton = (event, tabIndex) => {
</p>
<p class="control">
<button class="button is-danger" @click="selectCustomer(null)" :tabindex="isCustomerSelected() ? 1 : -1">
Clear
{{SessionUser.objects.global.language.clear}}
</button>
</p>
</div>
@@ -0,0 +1,155 @@
<script setup>
import {ref, defineProps, defineEmits, watch} from 'vue';
import { selectCustomer, isCustomerSelected, customer_id } from "@/components/shop/POSDepartmentProcess.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
const props = defineProps({
reg_1: {
type: String,
required: true,
},
})
const emit = defineEmits(['customerSelected']);
const getCustomerSuggestionObject = (id, customer_number, customer_name) => {
return {
id: parseInt(id),
customer_number: parseInt(customer_number),
customer_name: customer_name,
};
}
const customer_suggestions = ref([
/* Dummy data for testing purposes
getCustomerSuggestionObject(1, '1234567890', 'John Doe'),
getCustomerSuggestionObject(2, '0987654321', 'Jane Smith'),
getCustomerSuggestionObject(3, '1122334455', 'Alice Johnson'),
getCustomerSuggestionObject(4, '5566778899', 'Bob Brown'),
getCustomerSuggestionObject(5, '2233445566', 'Charlie Davis'),
getCustomerSuggestionObject(6, '6677889900', 'Diana Prince'),
getCustomerSuggestionObject(7, '3344556677', 'Ethan Hunt'),
getCustomerSuggestionObject(8, '7788990011', 'Felicity Smoak'),
getCustomerSuggestionObject(9, '4455667788', 'George Clooney'),
getCustomerSuggestionObject(10, '8899001122', 'Hannah Montana'),
getCustomerSuggestionObject(11, '5566778899', 'Ian Malcolm'),
getCustomerSuggestionObject(12, '9988776655', 'Jack Sparrow'),
getCustomerSuggestionObject(13, '2233445566', 'Katherine Johnson'),
getCustomerSuggestionObject(14, '6677889900', 'Leonardo DiCaprio'),
getCustomerSuggestionObject(15, '3344556677', 'Mia Thermopolis'),
*/
]);
const getCustomerSuggestions = () => {
// Check if the reg_1 prop is provided (and valid)
if (!props.reg_1) {
return;
}
SessionUser.request(
'/department/vehicle/customer-suggestions',
'GET',
{
reg_1: props.reg_1,
},
).then(response => {
// Assuming the response contains an array of customer suggestions
console.log('Customer suggestions:', response.data.data);
let suggestions = [];
for (let i = 0; i < response.data.data.length; i++) {
suggestions.push(getCustomerSuggestionObject(
response.data.data[i].id,
response.data.data[i].customer_number,
response.data.data[i].customer_name,
));
}
customer_suggestions.value = suggestions;
}).catch(error => {
console.error('Error fetching customer suggestions:', error);
});
}
const isSettingCustomer = ref(false);
const isSettingCustomerToInteger = ref(0);
const isSettingCustomerStartTime = ref(null);
const isSettingCustomerTo = (customer_number) => {
return isSettingCustomerToInteger.value === parseInt(customer_number);
}
const onClick = (customer) => {
isSettingCustomer.value = true;
isSettingCustomerToInteger.value = parseInt(customer.customer_number);
isSettingCustomerStartTime.value = new Date();
emit('customerSelected', parseInt(customer.customer_number));
}
watch(() => props.reg_1, (newValue) => {
resetIsSettingCustomer();
if (newValue) {
getCustomerSuggestions();
} else {
customer_suggestions.value = [];
}
});
watch(() => customer_id.value, (newValue) => {
resetIsSettingCustomer();
});
const resetIsSettingCustomer = () => {
isSettingCustomer.value = false;
isSettingCustomerToInteger.value = 0;
isSettingCustomerStartTime.value = null;
}
// Watch for the isSettingCustomer to reset after 5 seconds
watch(() => isSettingCustomer.value, (newValue) => {
if (newValue) {
setTimeout(() => {
isSettingCustomer.value = false;
isSettingCustomerToInteger.value = 0;
isSettingCustomerStartTime.value = null;
}, 5000);
}
});
// If the reg_1 is set when the component is mounted, fetch the customer suggestions
if (props.reg_1) {
getCustomerSuggestions();
}
</script>
<template>
<div class="field" :class="{'is-hidden': isCustomerSelected()}">
<div class="control">
<template v-if="customer_suggestions.length > 0">
<div class="columns is-multiline is-mobile">
<!-- Divider -->
<div class="column is-12 pb-0">
<div class="divider">{{SessionUser.objects.global.language.previous}} {{SessionUser.objects.global.language.customers}}</div>
</div>
<!-- Loop through the customer suggestions and display them -->
<template v-for="(customer, index) in customer_suggestions" :key="index">
<div class="column is-6">
<div class="card">
<!-- Name of the customer -->
<div class="card-content">
<div class="content">
<p class="title is-5">{{ customer.customer_name }}</p>
<p class="subtitle is-6">{{ customer.customer_id }}</p>
</div>
<div class="buttons">
<button class="button is-light is-fullwidth" @click="onClick(customer)" :class="{'is-loading': isSettingCustomerTo(customer.customer_number)}" :disabled="isSettingCustomer">
{{SessionUser.objects.global.language.select}}
</button>
</div>
</div>
</div>
</div>
</template>
</div>
</template>
</div>
</div>
</template>
<style scoped>
</style>
@@ -18,14 +18,19 @@ export const ObjectsGlobal = {
},
split: "Opdel",
save: "Gem",
customers: "Kunder",
invalid: "Ugyldig",
last_wash: "Sidste vask",
have_not: "Har ikke",
clear: "Ryd",
have: "Har",
hide: "Skjul",
cancel: "Annuller",
previous: "Tidligere",
unselect: "Fravælg",
select: "Vælg",
month: "Måned",
all: "Alle",
copy: "Kopier",
yes: "Ja",
no: "Nej",
@@ -452,6 +452,9 @@ export const selectScan = (scan) => {
/** Get the customer's name */
export const getCustomerName = async (customerNumber) => {
if (!customerNumber) {
return '';
}
// Get the customer data
await authenticatedRequest(
`/users/customer?customer_number=${customerNumber}`,
+5 -4
View File
@@ -2,16 +2,17 @@ import '@/themes/Dark.sass';
import '@popperjs/core';
//export const API_URL = 'https://truckwashdev.maintenancemode.cloud';
//export const API_URL = 'https://nnks.truckwash.dk';
export const IS_DEV = false;
//export const API_URL = 'https://api.truckwash.dk:4433';
export const POS_STEP_1_VERSION = 1;
export const API_URL = 'https://api.truckwash.dk';
export const IS_DEV = true;
export const API_URL = 'https://api.truckwash.dk:4433';
export const POS_STEP_1_VERSION = 2;
//export const API_URL = 'https://api.truckwash.dk';
import { createApp } from 'vue'
import App from './App.vue'
import store from './store/user.vue'
import {router} from "@/router.js";
import {Colors} from "./ThemeConfig.vue";
import '@/assets/main.css';
import 'animate.css';
import applyMiddleware from '@/middleware/index.js';
applyMiddleware(router);