Add invoice management and improve POS functionality

Introduced "CollectedOrderInvoices" for better invoice handling, including UI components, pagination, and back-end setup. Improved user feedback in POS steps, enhanced form behaviors with new props and watchers, and refined visual styles across components.
This commit is contained in:
Jepp9350
2025-02-28 13:49:54 +01:00
parent e2cffbad4b
commit c47fed6ae8
17 changed files with 453 additions and 45 deletions
@@ -10,7 +10,7 @@ import { showPopper, popper, showPopperWithContent, removePopperIfOpen, popperBo
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
import { customer_name, getOrderDetails, setOrderId, order_id, reference, order_notes } from "@/components/shop/POSDepartmentProcess.vue";
import { customer_name, getOrderDetails, setOrderId, order_id, reference, order_notes, reg_1, reg_3, reg_2 } from "@/components/shop/POSDepartmentProcess.vue";
import SpanSkeleton from "@/components/displays/skeletons/SpanSkeleton.vue";
import PosOrderLicensePlates from "@/components/displays/department/pos/order/PosOrderLicensePlates.vue";
import POSOrderReference from "@/components/displays/department/pos/order/POSOrderReference.vue";
@@ -80,13 +80,17 @@ const registration_numbers = ref({
const loadOrder = () => {
// Wait 100ms before loading the order.
// This is to ensure that the order_id is set before loading the order.
setTimeout(() => {
setOrderId(props.orderId);
getOrderDetails().then((orderDetails) => {
const reg_numbers = registration_numbers.value;
reg_numbers[1] = orderDetails.reg_1;
reg_numbers[2] = orderDetails.reg_2;
reg_numbers[3] = orderDetails.reg_3;
});
console.log('order note',order_notes.value);
}, 100);
};
const isTabZeroExpanded = ref(false);
@@ -115,16 +115,22 @@ const getRows = () => {
const shortNote = ref('');
watch(() => props.note, (value) => {
let shownNote = '';
if (value.length > 20 || value.split("\n").length > 1) {
// Get the first line, and add ...
shownNote = value.split("\n")[0].substring(0, 20) + '...';
} else {
shownNote = value;
}
console.log(shownNote);
shortNote.value = shownNote;
shortNote.value = generateShortNote();
});
const generateShortNote = () => {
let shownNote = '';
if (input.value.length > 20 || input.value.split("\n").length > 1) {
// Get the first line, and add ...
shownNote = input.value.split("\n")[0].substring(0, 20) + '...';
} else {
shownNote = input.value;
}
shortNote.value = shownNote;
return shownNote;
}
generateShortNote();
</script>
<template>
@@ -148,7 +154,7 @@ watch(() => props.note, (value) => {
:readonly="true"
:rows="1"
v-if="!isEditing"
v-text="shortNote"
v-model="shortNote"
placeholder="Ordre note, denne bliver inkluderet i fakturaen"
></textarea>
</div>
@@ -115,16 +115,21 @@ const getRows = () => {
const shortReference = ref('');
watch(() => props.reference, (value) => {
let shownReference = '';
if (value.length > 20 || value.split("\n").length > 1) {
// Get the first line, and add ...
shownReference = value.split("\n")[0].substring(0, 20) + '...';
} else {
shownReference = value;
}
console.log(shownReference);
shortReference.value = shownReference;
generateShortReference();
});
const generateShortReference = () => {
let shownReference = '';
if (input.value.length > 20 || input.value.split("\n").length > 1) {
// Get the first line, and add ...
shownReference = input.value.split("\n")[0].substring(0, 20) + '...';
} else {
shownReference = input.value;
}
shortReference.value = shownReference;
}
generateShortReference();
</script>
<template>
@@ -83,7 +83,6 @@ const right_tabs = ref([
<ButtonsBox>
<NextStep class="is-fullwidth" tabindex="6"/>
</ButtonsBox>
<NextStepError class="is-fullwidth" />
</div>
</div>
</div>
@@ -12,7 +12,7 @@ defineProps(['currentPage', 'totalPages', 'setPage', 'loadFunction', 'isLoading'
>Forrige side</a>
<!-- Pagination skeleton while loading -->
<!-- Pagination (IF there's less than 20 pages) -->
<ul class="pagination-list">
<ul class="pagination-list mt-1">
<li v-for="page in totalPages" :key="page" v-if="totalPages <= 15">
<a class="pagination-link"
@click="setPage(page); loadFunction()"
@@ -0,0 +1,57 @@
<script setup>
import CollectedOrderInvoicesTable from "@/components/displays/superuser/tables/collectedOrderInvoicesTable.vue";
let props = defineProps(["setCustomerFilter", "hideSearch", "autoLoad"]);
import { defineProps } from "vue";
import { useRouter } from "vue-router";
import {
isLoaded,
isLoading,
list,
loadList,
metaCurrentPage,
metaItemsPerPage,
metaTotalItems,
setEndpoint,
setMetaItemsPerPage,
setPage,
metaSearch,
search,
setFilter,
setOrder,
hideSearchField,
setHideSearchField,
} from "@/components/pagination/paginatedList.vue";
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
import OrdersTable from "@/components/displays/department/pos/orders/ordersTable.vue";
import CategoriesTable from "@/components/displays/superuser/tables/categoriesTable.vue";
const router = useRouter();
setEndpoint("/collected-invoices", true);
// Hide the search field, if the hideSearch prop is set
if (props.hideSearch) {
setHideSearchField(true);
}
if (props.autoLoad) {
loadList();
}
</script>
<template>
<input @input="search($event.target.value)" class="input" type="text" :placeholder="'Søg efter ' + SessionUser.objects.collectedOrderInvoices.meta.labels.single" v-if="!hideSearchField"/>
<PaginationDisplay :metaItemsPerPage="metaItemsPerPage" :loadFunction="loadList" :isLoading="isLoading" :setMetaItemsPerPage="setMetaItemsPerPage"/>
<collected-order-invoices-table :objects="list" />
<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>
<style scoped>
</style>
@@ -157,8 +157,8 @@ window.addEventListener('resize', () => {
</div>
</template>
</PaginationDisplay>
<BookingsTable :objects="list" :meta="meta" />
<PaginationNavigation :currentPage="metaCurrentPage" :totalPages="Math.ceil(metaTotalItems / metaItemsPerPage)" :loadFunction="loadList" :setPage="setPage" :isLoading="isLoading" />
<BookingsTable :objects="list" :meta="meta" class="mb-4" />
<PaginationNavigation :currentPage="metaCurrentPage" :totalPages="Math.ceil(metaTotalItems / metaItemsPerPage)" :loadFunction="loadList" :setPage="setPage" :isLoading="isLoading" class="mt-3" />
<LoadButtonWhileAwait class="is-dark" :isLoading="isLoading" :loadFunction="loadList" icon="fas fa-sync-alt">Opdater</LoadButtonWhileAwait>
</template>
@@ -0,0 +1,96 @@
<script setup>
import EditableTableColumn from "@/components/displays/buttons/EditableTableColumn.vue";
import {getCustomerName} from "@/components/shop/POSDepartmentProcess.vue";
defineProps(['objects']);
import { ref } from 'vue';
import { loadList } from "@/components/pagination/paginatedList.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
const redirect = (path) => {
window.location = path;
}
</script>
<template>
<table class="table is-fullwidth is-hoverable is-striped">
<thead>
<tr>
<th>{{ SessionUser.objects.collectedOrderInvoices.columns.id.label }}</th>
<th>{{ SessionUser.objects.collectedOrderInvoices.columns.customer_number.label }}</th>
<th>{{ SessionUser.objects.collectedOrderInvoices.columns.name.label }}</th>
<th>{{ SessionUser.objects.collectedOrderInvoices.columns.notes.label }}</th>
<th>{{ SessionUser.objects.collectedOrderInvoices.columns.processor.label }}</th>
<th>Transaktioner</th>
<th>{{ SessionUser.objects.collectedOrderInvoices.columns.closed_at.label }}</th>
<th>{{ SessionUser.objects.collectedOrderInvoices.columns.created_at.label }}</th>
<th><!-- Actions --></th>
</tr>
</thead>
<tbody>
<tr v-for="object in objects" :key="object.id">
<td>{{ object.id }}</td>
<!-- Customer number -->
<EditableTableColumn
:object="object"
:loadList="loadList"
column="customer_number"
/>
<!-- Name -->
<EditableTableColumn
:object="object"
:loadList="loadList"
:editFunction="SessionUser.objects.collectedOrderInvoices.showEditObjectFieldForm"
column="name"
/>
<!-- Notes -->
<EditableTableColumn
:object="object"
:loadList="loadList"
:editFunction="SessionUser.objects.collectedOrderInvoices.showEditObjectFieldForm"
column="notes"
/>
<!-- Processor -->
<EditableTableColumn
:object="object"
:loadList="loadList"
:editFunction="SessionUser.objects.collectedOrderInvoices.showEditObjectFieldForm"
column="processor"
/>
<!-- Transactions -->
<EditableTableColumn
:object="object"
:loadList="loadList"
column="orders"
/>
<!-- Closed at -->
<EditableTableColumn
:object="object"
:loadList="loadList"
column="closed_at"
/>
<!-- Created at -->
<EditableTableColumn
:object="object"
:loadList="loadList"
column="created_at"
/>
</tr>
<!-- If the list is empty -->
<tr v-if="objects.length === 0">
<td colspan="9">
Ingen {{ SessionUser.objects.collectedOrderInvoices.meta.labels.multiple }} fundet
</td>
</tr>
<!-- Show a count of the total number of objects -->
<tr>
<td colspan="9">Viser {{ objects.length }} {{ SessionUser.objects.collectedOrderInvoices.meta.labels.multiple }}</td>
</tr>
</tbody>
</table>
</template>
<style scoped>
</style>
@@ -134,20 +134,24 @@ const keyDownNextTabIndexButton = (event, tabIndex) => {
@keydown="keyDownNextInput($event, 'reg_2')"
input-id="reg_1"
v-model:inputModel="reg_1"
v-bind:actual-value="reg_1"
:customer_number="parseInt(customer_id) || 0"
class="has-sharp-edges"
/>
{{ reg_1}}
</div>
<div class="field">
<label class="label">Reg 2</label>
<LicensePlateInput :tab-index="4" @keydown="keyDownNextInput($event, 'reg_3')" input-id="reg_2" v-model:inputModel="reg_2" :customer_number="parseInt(customer_id) || 0" />
<LicensePlateInput :tab-index="4" @keydown="keyDownNextInput($event, 'reg_3')" input-id="reg_2" v-model:inputModel="reg_2" :customer_number="parseInt(customer_id) || 0" v-bind:actual-value="reg_2" />
{{ reg_2}}
</div>
<div class="field">
<label class="label">Reg 3</label>
<!--<div class="control">
<input class="input reg-input" type="text" v-model="reg_3" tabindex="5" @keydown="keyDownNextTabIndexButton($event, 6)" id="reg_3" />
</div> -->
<LicensePlateInput :tab-index="5" @keydown="keyDownNextTabIndexButton($event, 6)" input-id="reg_3" v-model:inputModel="reg_3" :customer_number="parseInt(customer_id) || 0" />
<LicensePlateInput :tab-index="5" @keydown="keyDownNextTabIndexButton($event, 6)" input-id="reg_3" v-model:inputModel="reg_3" :customer_number="parseInt(customer_id) || 0" v-bind:actual-value="reg_3" />
{{ reg_3}}
</div>
</div>
<PosNotes :notes="notes" :is-loading="false" :isAddFormVisible="SessionUser.adminUser" :isOldNotesVisible="false" :is-label-visible="true" class="mt-3" v-if="isCustomerSelected()" />
@@ -1,5 +1,5 @@
<script setup>
import {defineEmits, defineProps, ref} from 'vue';
import {defineEmits, defineProps, ref, watch} from 'vue';
import {SessionUser} from "@/components/session/token/SessionUser.vue";
import {getError, parseError} from "@/components/request/HandleGlobalError.vue";
@@ -17,6 +17,10 @@ const props = defineProps(
type: String,
required: false
},
actualValue: {
type: String,
required: false
},
customer_number: {
type: Number,
required: false
@@ -203,6 +207,20 @@ const setInputValue = (licensePlate) => {
const isDropdownItemSelected = (registrationNumber) => {
return registrationNumber === customerRegistrationNumbers.value[selectedDropdownItem.value];
};
const inputField = ref(null);
watch(() => props.actualValue, (newValue) => {
if (newValue === currentFieldVal.value) {
return;
}
// Set the value of the input field
inputField.value.value = newValue;
currentFieldVal.value = newValue;
checkLicensePlateValid(newValue);
searchCustomerRegistrationNumbers(newValue);
});
</script>
<template>
@@ -218,6 +236,7 @@ const isDropdownItemSelected = (registrationNumber) => {
:tabindex="props.tabIndex"
autocomplete="off"
@keydown="pressedKey($event)"
ref="inputField"
/>
<span class="icon is-small is-right" v-if="isValueValid()">
<i class="fas fa-check"></i>
@@ -15,6 +15,7 @@ import {EconomicDepartments} from "@/components/session/token/SessionUser/Object
import {EconomicProducts} from "@/components/session/token/SessionUser/Objects/economicProducts.vue";
import {Roles} from "@/components/session/token/SessionUser/Objects/Roles.vue";
import {Permissions} from "@/components/session/token/SessionUser/Objects/Permissions.vue";
import {CollectedOrderInvoices} from "@/components/session/token/SessionUser/Objects/CollectedOrderInvoices.vue";
import {ObjectsGlobal} from "@/components/session/token/SessionUser/Objects/ObjectsGlobal.vue";
import {useRouter} from "vue-router";
@@ -175,6 +176,7 @@ export const SessionUser = {
economic_products: EconomicProducts,
roles: Roles,
permissions: Permissions,
collectedOrderInvoices: CollectedOrderInvoices,
global: ObjectsGlobal,
},
/** The user's token & authentication status */
@@ -0,0 +1,174 @@
<script>
import Swal from "sweetalert2";
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
import { ObjectsGlobal } from "@/components/session/token/SessionUser/Objects/ObjectsGlobal.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import {ref} from "vue";
/**
* The CollectOrderInvoices object
*/
export const CollectedOrderInvoices = {
meta: {
title: "Samlede ordrefakturaer",
icon: "fas fa-list",
description: "Oversigt over samlede ordrefakturaer",
endpoint: "/collected-invoices",
labels: {
single: "samlet ordrefaktura",
multiple: "samlede ordrefakturaer"
}
},
columns: {
id: {
label: "ID",
type: "number",
sortable: true,
creation: {
required: false
}
},
customer_number: {
label: "E-conomic kundenummer",
type: "number",
sortable: true,
creation: {
required: true
}
},
name: {
label: "Navn",
type: "string",
sortable: true,
creation: {
required: false
}
},
notes: {
label: "Noter",
type: "string",
sortable: true,
creation: {
required: false
}
},
processor: {
label: "Faktureringsprocessor",
type: "number",
sortable: true,
creation: {
required: false
}
},
external_id: {
label: "Eksternt ID",
type: "number",
sortable: true,
creation: {
required: false
}
},
closed_at: {
label: "Lukket",
type: "date",
sortable: true,
creation: {
required: false
}
},
created_at: {
label: "Oprettet",
type: "date",
sortable: true,
creation: {
required: false
}
},
updated_at: {
label: "Opdateret",
type: "date",
sortable: true,
creation: {
required: false
}
},
},
add: async (name, description) => {
return ObjectsGlobal.add.object(
CollectedOrderInvoices.meta.endpoint,
{
name: name,
description: description
}
);
},
set: {
name: async (id, name) => {
return ObjectsGlobal.set.column(
CollectedOrderInvoices.meta.endpoint,
id,
"name",
name
)
},
notes: async (id, notes) => {
return ObjectsGlobal.set.column(
CollectedOrderInvoices.meta.endpoint,
id,
"notes",
notes
)
},
processor: async (id, processor = null) => {
return ObjectsGlobal.set.column(
CollectedOrderInvoices.meta.endpoint,
id,
"processor",
processor ? parseInt(processor) : null
)
},
external_id: async (id, external_id) => {
return ObjectsGlobal.set.column(
CollectedOrderInvoices.meta.endpoint,
id,
"external_id",
parseInt(external_id)
)
},
},
get: {
all: async () => {
return ObjectsGlobal.get.objects(CollectedOrderInvoices.meta.endpoint);
},
single: async (id) => {
return ObjectsGlobal.get.object(CollectedOrderInvoices.meta.endpoint, id);
}
},
functions: {},
/**
* Show the create object form
* @param onAfterSubmit
* @returns {Promise<SweetAlertResult<Awaited<any>>>}
*/
showCreateObjectForm: (onAfterSubmit = null) => {
return ObjectsGlobal.showCreateObjectForm(CollectedOrderInvoices, onAfterSubmit);
},
/**
* Show the edit object field form
* @param id
* @param column
* @param value
* @param onAfterSubmit
* @returns {Promise<SweetAlertResult<Awaited<any>>>}
*/
showEditObjectFieldForm: (id, column, value, onAfterSubmit = null) => {
return ObjectsGlobal.showEditObjectFieldForm(
CollectedOrderInvoices,
id,
column,
value,
onAfterSubmit
);
}
};
</script>
+21 -2
View File
@@ -80,8 +80,17 @@ export const nextStep = async () => {
setProductsCategory(4);
}
if (step.value === 3) {
// If the current step is 3, redirect to the /admin/:departmentId/modules/pos/orders/:orderId page
window.location.href = `/admin/${department_id.value}/modules/pos/orders/${order_id.value}`;
// Show a "Success" dialog, that closes after 2 seconds, then redirects to the next page
Swal.fire({
title: 'Fuldført',
text: 'Transaktionen er fuldført, omdirigerer automatisk...',
icon: 'success',
showConfirmButton: false,
timer: 2000
}).then(() => {
// Redirect to the next page
window.location.href = '/admin/' + department_id.value + '/modules/pos';
});
return;
}
step.value++;
@@ -566,9 +575,19 @@ export const canBuyTankCleaning = () => {
return !hasAttribute('restrictTankCleaning');
};
function loadOrderDetails() {
getOrderDetails().then((response) => {
console.log(response);
});
}
/** Set the order id */
export const setOrderId = (id) => {
order_id.value = id;
// Load the order details
loadOrderDetails();
// Load the order items
loadOrderItems();
};
/** Delete everything button */
+7
View File
@@ -68,6 +68,7 @@ import Roles from "@/views/dashboards/superUserDashboard/roles/Roles.vue";
import SuperUserRolesPermissions from "@/views/dashboards/superUserDashboard/roles/SuperUserRolesPermissions.vue";
import DepartmentStripeTerminalsReaders from "@/views/dashboards/superUserDashboard/department/stripe/DepartmentStripeTerminalsReaders.vue";
import DepartmentStripeSetup from "@/views/dashboards/superUserDashboard/department/stripe/DepartmentStripeSetup.vue";
import CollectedOrderInvoices from "@/views/dashboards/superUserDashboard/CollectedOrderInvoices.vue";
// Export the router as router
export const router = createRouter({
@@ -241,6 +242,12 @@ export const router = createRouter({
component: Products,
meta: { middleware: superUserMiddleware }
},
{
name: 'collectedorderinvoices',
path: '/superuser/invoices',
component: CollectedOrderInvoices,
meta: { middleware: superUserMiddleware }
},
{
name: 'orders',
path: '/superuser/orders',
+4 -9
View File
@@ -54,7 +54,7 @@ const slots = defineSlots()
</div>
</div>
<!-- Left menu -->
<div class="column is-2 is-auto-height-fill"
<div class="column is-2 is-continuous-to-bottom"
:style="{ 'background-color': Colors.menus.parentBackgroundColor, 'color': Colors.menus.textColor }"
>
<slot name="left-menu"></slot>
@@ -63,6 +63,7 @@ const slots = defineSlots()
<div
class="column is-10"
:style="{ 'background-color': Colors.global.backgroundColor, 'color': Colors.global.textColor }"
style="min-height: 100vh;"
>
<div class="pl-6 pr-6 pb-6">
<!-- Buttons slot -->
@@ -78,18 +79,12 @@ const slots = defineSlots()
<slot name="without-formatting"></slot>
</div>
</div>
<!-- Footer -->
<!-- Footer
<div class="column is-12">
<Footer />
</div>
</div> -->
</div>
</template>
<style scoped>
.is-auto-height-fill {
height: auto;
max-height: 100vh;
min-height: 100vh;
overflow-y: auto;
}
</style>
@@ -0,0 +1,20 @@
<script setup>
import SuperUserDashboardNavigation from "@/views/dashboards/superUserDashboard/SuperUserDashboardNavigation.vue";
import RestrictedPageWrapper from "@/components/page/wrappers/RestrictedPageWrapper.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import CollectedOrderInvoicesPagination
from "@/components/displays/pagination/models/SuperUserDashboard/CollectedOrderInvoicesPagination.vue";
import PageTitle from "@/components/global/PageTitle.vue";
</script>
<template>
<RestrictedPageWrapper :hasPermission="SessionUser.canAccessSuperUser()">
<SuperUserDashboardNavigation />
<PageTitle title="Fakturaer" subtitle="Her kan du se en liste over alle fakturaer i systemet" />
<CollectedOrderInvoicesPagination />
</RestrictedPageWrapper>
</template>
<style scoped>
</style>
@@ -14,6 +14,7 @@ const tabs = [
{ name: 'Medarbejdere', path: '/superuser/users' },
{ name: 'Customers', path: '/superuser/customers' },
{ name: 'Products', path: '/superuser/products' },
{ name: 'Fakturaer', path: '/superuser/invoices' },
{ name: 'Transaktionshistorik', path: '/superuser/orders' },
{ name: 'Scanners', path: '/superuser/scanners' },
{ name: 'Roles', path: '/superuser/roles' },