Refactor and enhance order management UI and functionality

Introduced new props, conditional components, and methods to improve flexibility and functionality across multiple Vue components, including dynamic payment status and tax calculations. Added print receipt capabilities and refined handling of order details, enhancing user experience.
This commit is contained in:
Jepp9350
2025-05-21 10:16:59 +02:00
parent 5d6e790fc3
commit 65a3b8263f
5 changed files with 372 additions and 33 deletions
@@ -324,4 +324,7 @@ onMounted(() => {
</template>
<style scoped>
.no-underline-text {
text-decoration: none !important;
}
</style>
@@ -1,5 +1,41 @@
<script setup>
defineProps(['orderItems', 'isLoading']);
const props = defineProps({
orderItems: {
type: Array,
required: true,
},
isLoading: {
type: Boolean,
default: false,
},
taxPercentage: {
type: Number,
default: 0,
},
paid: {
type: Boolean,
default: false,
},
showPaymentStatus: {
type: Boolean,
default: false,
},
});
const calculateTax = (price) => {
if (!price || !props.taxPercentage) {
return 0;
}
return price * (props.taxPercentage / 100);
};
const calculateTotalPrice = (price, quantity) => {
if (!price || !quantity) {
return 0;
}
const totalPrice = price * quantity;
const tax = calculateTax(totalPrice, props.taxPercentage);
return totalPrice + tax;
};
</script>
<template>
@@ -14,13 +50,13 @@ defineProps(['orderItems', 'isLoading']);
</tr>
</thead>
<tbody>
<tr v-if="isLoading" v-for="n in 5" :key="n">
<tr v-if="props.isLoading" v-for="n in 5" :key="n">
<!-- Skeleton-lines 5 rows -->
<td v-for="n in 5" :key="n">
<div class="skeleton-lines"><div style="width: 100%;"></div></div>
</td>
</tr>
<tr v-for="orderItem in orderItems" :key="orderItem.id" v-else>
<tr v-for="orderItem in props.orderItems" :key="orderItem.id" v-else>
<td>{{ orderItem.product.name }}</td>
<td>{{ orderItem.notes }}</td>
<td>{{ orderItem.reference }}</td>
@@ -32,13 +68,38 @@ defineProps(['orderItems', 'isLoading']);
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="4">Total</td>
<td style="text-align: right;" v-if="isLoading">
<!-- Tax -->
<tr v-if="props.taxPercentage > 0">
<td colspan="4">Tax ({{ props.taxPercentage }}%)</td>
<td style="text-align: right;" v-if="props.isLoading">
<!-- Skeleton-lines 1 row -->
<div class="skeleton-lines"><div style="width: 100%;"></div></div>
</td>
<td style="text-align: right;" v-else>{{ orderItems.reduce((acc, orderItem) => acc + orderItem.price * orderItem.quantity, 0) }} DKK</td>
<td style="text-align: right;" v-else>{{ calculateTax(props.orderItems.reduce((acc, orderItem) => acc + orderItem.price * orderItem.quantity, 0)) }} DKK</td>
</tr>
<tr v-if="props.taxPercentage > 0">
<td colspan="4">Subtotal</td>
<td style="text-align: right;" v-if="props.isLoading">
<!-- Skeleton-lines 1 row -->
<div class="skeleton-lines"><div style="width: 100%;"></div></div>
</td>
<td style="text-align: right;" v-else>{{ props.orderItems.reduce((acc, orderItem) => acc + orderItem.price * orderItem.quantity, 0) }} DKK</td>
</tr>
<tr>
<td colspan="4">Total</td>
<td style="text-align: right;" v-if="props.isLoading">
<!-- Skeleton-lines 1 row -->
<div class="skeleton-lines"><div style="width: 100%;"></div></div>
</td>
<td style="text-align: right;" v-else>{{ props.orderItems.reduce((acc, orderItem) => acc + orderItem.price * orderItem.quantity, 0) + calculateTax(props.orderItems.reduce((acc, orderItem) => acc + orderItem.price * orderItem.quantity, 0)) }} DKK</td>
</tr>
<tr v-if="props.showPaymentStatus">
<td colspan="4">Payment Status</td>
<td style="text-align: right;" v-if="props.isLoading">
<!-- Skeleton-lines 1 row -->
<div class="skeleton-lines"><div style="width: 100%;"></div></div>
</td>
<td style="text-align: right;" :class="props.paid ? 'has-text-success' : 'has-text-danger'" v-else>{{ props.paid ? 'Paid' : 'Unpaid' }}</td>
</tr>
</tfoot>
</table>
+1 -1
View File
@@ -96,7 +96,7 @@ export const getSessionData = async () => {
SessionUser.economicData.customerNumber.value = response.data.data.economic_customer.customerNumber;
SessionUser.economicData.name.value = response.data.data.economic_customer.name;
// Set the display name to the e-conomic name if it's empty
if (SessionUser.user.display_name.value === null) {
if (SessionUser.user.display_name.value === null || SessionUser.user.display_name.value === 'Unnamed') {
SessionUser.user.display_name.value = response.data.data.economic_customer.name;
}
SessionUser.economicData.address.value = response.data.data.economic_customer.address;
@@ -4,8 +4,8 @@ import DepartmentDashboardHero from "@/views/dashboards/departmentDashboard/Depa
import PosDepartmentMVP from "@/components/displays/department/pos/PosDepartmentMVP.vue";
import DepartmentPosNavigation from "@/views/dashboards/departmentDashboard/modules/Pos/DepartmentPosNavigation.vue";
import { useRouter } from "vue-router";
import {ref} from "vue";
import { selectCustomer, setOrderId, customer_name, deleteOrder, showDeleteOrderDialog, invoiceAllOrdersIndividually, invoiceUsingStripe, setProductsCategory, order_items } from "@/components/shop/POSDepartmentProcess.vue";
import {onMounted, ref, watch} from "vue";
import { selectCustomer, setOrderId, customer_name, deleteOrder, showDeleteOrderDialog, invoiceAllOrdersIndividually, invoiceUsingStripe, setProductsCategory, order_items, customer_id, invoiceCollectionId } from "@/components/shop/POSDepartmentProcess.vue";
import { getOrder, editOrderNotes } from "@/components/shop/Orders.vue";
import { getOrderItems } from "@/components/shop/OrdersItems.vue";
import { showEditOrderNotesForm } from "@/components/forms/superUser/editOrderNotesForm.vue";
@@ -16,9 +16,11 @@ import ExportOrderToDraftButton from "@/components/search/economic/exportOrderTo
import ExportOrderToInvoiceButton from "@/components/search/economic/exportOrderToInvoiceButton.vue";
import RestrictedPageWrapper from "@/components/page/wrappers/RestrictedPageWrapper.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { Colors } from '@/ThemeConfig.vue'
import RemoveOrderDraftInvoiceButton from "@/components/search/economic/removeOrderDraftInvoiceButton.vue";
const router = useRouter();
import { getDepartmentName } from "@/components/pagination/departmentTabs.vue";
import { getDepartmentName, getDepartments } from "@/components/pagination/departmentTabs.vue";
import NotFoundFallBackPageWrapper from "@/components/page/wrappers/NotFoundFallBackPageWrapper.vue";
import PosDepartmentStep2 from "@/components/displays/department/pos/steps/PosDepartmentStep2.vue";
import SelectProductsFormPOS from "@/components/forms/department/pos/SelectProductsFormPOS.vue";
@@ -30,6 +32,8 @@ import ButtonsBox from "@/components/displays/boxes/ButtonsBox.vue";
import EditableTableColumn from "@/components/displays/buttons/EditableTableColumn.vue";
import ShowErrorField from "@/components/global/ShowErrorField.vue";
import PrintInvoiceFromOrderItems from "@/components/forms/department/pos/buttons/PrintInvoiceFromOrderItems.vue";
import ColorIndicator from "@/components/displays/buttons/ColorIndicator.vue";
import MyOrder from "@/views/dashboards/userDashboard/orders/MyOrder.vue";
// Get the id from the route
const orderId = ref(router.currentRoute.value.params.orderId);
@@ -79,6 +83,7 @@ const loadOrder = async () => {
}
};
const isInvoicedWithStripe = () => {
// If the stripe module is anything else than an empty object, then the order is invoiced
if (Object.keys(stripeModule.value).length !== 0) {
@@ -156,11 +161,122 @@ const isBookedWithEconomic = () => {
const isCompleted = () => {
return order.value.completed_at !== null;
};
const isPaymentMethodStripe = ref(() => {
return customer_id.value === 999;
});
const invoiceCollection = ref(null);
const getInvoiceCollection = async () => {
if (!invoiceCollectionId.value) {
return;
}
const response = await SessionUser.objects.collectedOrderInvoices.get.single(invoiceCollectionId.value);
invoiceCollection.value = response;
}
const isPaymentCollectionLoaded = ref(() => {
return invoiceCollection.value !== null;
});
const getInvoiceCollectionValue = () => {
return invoiceCollection.value;
};
watch(() => invoiceCollectionId.value, async () => {
await getInvoiceCollection();
});
const getStripePaymentStatus = () => {
let result = {
color_class: 'has-text-dark',
text: 'Ikke betalt',
classes: [],
amount: null,
amount_capturable: null,
amount_received: null,
}
if (!invoiceCollection.value) {
result.text = SessionUser.objects.global.language.loading
return result;
}
let tmp_amounts = {
amount: (getInvoiceCollectionValue().stripe.amount ?? null),
amount_capturable: (getInvoiceCollectionValue().stripe.amount_capturable ?? null),
amount_received: (getInvoiceCollectionValue().stripe.amount_received ?? null),
};
function isNotNull(value) {
return value !== null;
}
function isPaid() {
return (
tmp_amounts.amount === tmp_amounts.amount_received &&
tmp_amounts.amount_capturable === 0 &&
isNotNull(tmp_amounts.amount) &&
isNotNull(tmp_amounts.amount_received)
);
}
function isPartiallyPaid() {
return (
tmp_amounts.amount > tmp_amounts.amount_received &&
tmp_amounts.amount_capturable > 0 &&
isNotNull(tmp_amounts.amount) &&
isNotNull(tmp_amounts.amount_received)
);
}
function isNotPaid() {
return (
tmp_amounts.amount > 0 &&
tmp_amounts.amount_received === 0 &&
tmp_amounts.amount_capturable > 0 &&
isNotNull(tmp_amounts.amount) &&
isNotNull(tmp_amounts.amount_received)
);
}
if (isPaid()) {
result.color_class = 'has-text-success';
result.text = 'Betalt';
} else if (isPartiallyPaid()) {
result.color_class = 'has-text-warning';
result.text = 'Delvist betalt';
} else if (isNotPaid()) {
result.color_class = 'has-text-danger';
result.text = 'Ikke betalt';
}
else {
result.color_class = 'has-text-dark';
result.text = 'Ukendt';
}
// Add the amounts to the result
result = {
...result,
amount: tmp_amounts.amount,
amount_capturable: tmp_amounts.amount_capturable,
amount_received: tmp_amounts.amount_received,
};
return result;
};
const isShowingPrintReceipt = ref(false);
getDepartments();
const openPrintDialog = () => {
isShowingPrintReceipt.value = true;
// Use the window.print() function to open the print dialog
setTimeout(() => {
window.print();
isShowingPrintReceipt.value = false;
}, 500);
};
</script>
<template>
<RestrictedPageWrapper :hasPermission="SessionUser.canAccessAdmin()">
<DepartmentDashboardPageWrapper
v-show="!isShowingPrintReceipt"
title="Kassesystem"
subtitle="Transaktion"
>
@@ -184,6 +300,62 @@ const isCompleted = () => {
<div class="tag is-success" v-if="isStripeInvoicePaid()"> Betalt </div>
<div class="tag is-warning" v-else> {{ getStripeInvoiceStatus() }} </div>
</div>
<div v-else-if="isPaymentMethodStripe()">
<div class="tag">
<ColorIndicator
v-bind:label="getStripePaymentStatus()"
v-bind:color_class="getStripePaymentStatus().color_class"
v-bind:is_loading="!isPaymentCollectionLoaded()"
v-bind:dropdown_content="{
title: 'Betalings status',
content: [
{
text: 'Betalings status',
action: () => {},
classes: ['has-text-grey'],
button: true,
button_classes: ['no-underline-text', 'is-text', ...[getStripePaymentStatus().color_class]],
v_centered: true,
button_text: getStripePaymentStatus().text,
},
...(getStripePaymentStatus().amount_received > 0 ? [
{
text: 'Beløb modtaget',
action: () => {},
classes: ['has-text-grey'],
button: true,
button_classes: ['no-underline-text', 'has-text-grey', 'is-text'],
v_centered: true,
button_text: SessionUser.functions.currency.toLocal(getStripePaymentStatus().amount_received / 100),
},
] : []),
...(getStripePaymentStatus().amount_capturable > 0 ? [
{
text: 'Beløb reserveret',
action: () => {},
classes: ['has-text-grey'],
button: true,
button_classes: ['no-underline-text', 'is-text', ...[(getStripePaymentStatus().amount_capturable <= 0 ? 'has-text-grey' : 'has-text-warning')]],
v_centered: true,
button_text: SessionUser.functions.currency.toLocal(getStripePaymentStatus().amount_capturable / 100),
},
] : []),
...(getStripePaymentStatus().amount - getStripePaymentStatus().amount_received > 0 ? [
{
text: 'Skyldig beløb',
action: () => {},
classes: ['has-text-grey'],
button: true,
button_classes: ['no-underline-text', 'is-text', ...[(getStripePaymentStatus().amount - getStripePaymentStatus().amount_received) <= 0 ? 'has-text-grey' : 'has-text-danger']],
v_centered: true,
button_text: SessionUser.functions.currency.toLocal(getStripePaymentStatus().amount / 100 - getStripePaymentStatus().amount_received / 100),
},
] : []),
]
}"
/>
</div>
</div>
<div v-else-if="economicModule.invoice_id">
<div class="tag is-dark"> Faktura {{ economicModule.invoice_id }} </div>
</div>
@@ -458,15 +630,76 @@ const isCompleted = () => {
<span class="icon"><i class="fas fa-file-invoice"></i></span>
<span>Hent faktura</span>
</button> -->
<!--<PrintInvoiceFromOrderItems v-bind:orderItems="order_items" tax_percentage="25" v-bind:paid="isStripeInvoicePaid()" v-bind:order_id="orderId" /> -->
<!-- Print invoice button -->
<button class="button is-dark is-fullwidth" @click="isShowingPrintReceipt = !isShowingPrintReceipt; openPrintDialog()">
<span class="icon"><i class="fas fa-print"></i></span>
<span>Print kvittering</span>
</button>
</div>
</div>
</div>
<!-- Test -->
</notFoundFallBackPageWrapper>
</DepartmentDashboardPageWrapper>
<div v-show="isShowingPrintReceipt">
<!-- Image -->
<div class="has-text-centered py-6 keep-bg-during-print" :style="{'background-color': Colors.menus.parentBackgroundColor}">
<img src="@/assets/branding/truckwash-banner-white-compressed.png" alt="Truck Wash Logo" style="max-width: 100%; max-height: 200px;"/>
</div>
<!-- Order details -->
<div class="box">
<div class="columns is-mobile is-multiline">
<!-- Order information -->
<div class="column is-half">
<div class="content">
<h3 class="title is-4">Transaktions information</h3>
<p><strong>Transaktions ID:</strong> {{ orderId }}</p>
<p><strong>Faktura ID:</strong> {{ getInvoiceCollectionValue().id }}</p>
<p><strong>Oprettet:</strong> {{ order.created_at }}</p>
<p><strong>Afdeling:</strong> {{ getDepartmentName(order.department_id)}}</p>
</div>
</div>
<!-- Customer information -->
<div class="column is-half">
<div class="content has-text-right">
<h3 class="title is-4">Kunde information</h3>
<p><strong>Kunde:</strong> {{ customer_name }}</p>
</div>
</div>
</div>
<!-- Vehicle information -->
<div class="column is-12 pt-0">
<div class="divider">Køretøjer</div>
<div class="columns has-text-centered">
<div class="column" v-if="order.reg_1">
<p><strong>Reg. 1:</strong> {{ order.reg_1 }}</p>
</div>
<div class="column" v-if="order.reg_2">
<p><strong>Reg. 2:</strong> {{ order.reg_2 }}</p>
</div>
<div class="column" v-if="order.reg_3">
<p><strong>Reg. 3:</strong> {{ order.reg_3 }}</p>
</div>
</div>
</div>
</div>
<!-- Order items table -->
<OrderItemsTable
v-bind:orderItems="order_items"
v-bind:show-payment-status="true"
v-bind:is-loading="false"
v-bind:paid="true"
v-bind:tax-percentage="25"
/>
</div>
</RestrictedPageWrapper>
</template>
<style scoped>
.keep-bg-during-print {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
</style>
@@ -1,14 +1,45 @@
<script setup>
<script setup lang="ts">
import UserDashboardHero from "@/views/dashboards/userDashboard/UserDashboardHero.vue";
import OrderItemsTable from "@/components/displays/department/pos/order/orderItemsTable.vue";
import {useRouter} from "vue-router";
import {ref} from "vue";
import {ref, defineProps, Component} from "vue";
import {getUserOrder} from "@/components/shop/Orders.vue";
import {getDepartmentName, getDepartments} from "@/components/pagination/departmentTabs.vue";
import '@creativebulma/bulma-divider/dist/bulma-divider.min.css';
import UserDashboardPageWrapper from "@/views/dashboards/userDashboard/UserDashboardPageWrapper.vue";
const props = defineProps({
visibleComponents: {
type: Object as () => {
warnings: boolean;
transaction_details: boolean;
customer_details: boolean;
vehicle_details: boolean;
order_items: boolean;
payment_status: boolean;
},
default: () => ({
warnings: true,
transaction_details: true,
customer_details: true,
vehicle_details: true,
order_items: true,
payment_status: false,
})
},
orderObject: {
type: Object as () => {
orderId: number | null;
paid: boolean | null;
},
default: () => ({
orderId: null,
paid: false,
tax_percentage: 0,
})
}
});
const router = useRouter();
const orderId = ref(router.currentRoute.value.params.orderId);
@@ -18,11 +49,20 @@ const economicModuleOrders = ref([]);
const isLoading = ref(true);
const customer = ref([]);
const getOrderId = () => {
// If the orderObject is set, use that
if (props.orderObject && props.orderObject.orderId) {
return props.orderObject.orderId;
}
// Otherwise, use the orderId from the URL
return orderId.value;
};
// Load the departments
getDepartments();
// Load the order, when the page is loaded
getUserOrder(orderId.value, true).then((response) => {
getUserOrder(getOrderId(), true).then((response) => {
orderItems.value = response.data.includes.orderItems;
order.value = response.data.data;
economicModuleOrders.value = response.data.includes.economicModuleOrders;
@@ -32,24 +72,26 @@ getUserOrder(orderId.value, true).then((response) => {
</script>
<template>
<UserDashboardPageWrapper :title="'Transaktion ' + orderId" subtitle="Se detaljer om transaktionen">
<div class="message is-warning mt-2" v-if="!isLoading && !economicModuleOrders.invoice_draft_id && !economicModuleOrders.invoice_id">
<div class="message-body">
<p class="is-size-6">Denne transaktion er under behandling, og er ikke klar til at blive faktureret.</p>
<UserDashboardPageWrapper :title="'Transaktion ' + getOrderId()" subtitle="Se detaljer om transaktionen">
<template v-if="props.visibleComponents.warnings">
<div class="message is-warning mt-2" v-if="!isLoading && !economicModuleOrders.invoice_draft_id && !economicModuleOrders.invoice_id">
<div class="message-body">
<p class="is-size-6">Denne transaktion er under behandling, og er ikke klar til at blive faktureret.</p>
</div>
</div>
</div>
<div class="message is-dark mt-2" v-if="!isLoading && economicModuleOrders.invoice_draft_id && !economicModuleOrders.invoice_id">
<div class="message-body">
<p class="is-size-6">Denne transaktion er blevet gemt, og er klar til at blive faktureret.</p>
<div class="message is-dark mt-2" v-if="!isLoading && economicModuleOrders.invoice_draft_id && !economicModuleOrders.invoice_id">
<div class="message-body">
<p class="is-size-6">Denne transaktion er blevet gemt, og er klar til at blive faktureret.</p>
</div>
</div>
</div>
<div class="notification is-success mt-2" v-if="!isLoading && economicModuleOrders.invoice_id">
<p class="is-size-6">Denne transaktion er blevet faktureret.</p>
</div>
<div class="notification is-success mt-2" v-if="!isLoading && economicModuleOrders.invoice_id">
<p class="is-size-6">Denne transaktion er blevet faktureret.</p>
</div>
</template>
<div class="box">
<div class="columns is-mobile is-multiline">
<!-- Order information -->
<div class="column is-half">
<div class="column" :class="(props.visibleComponents.transaction_details && props.visibleComponents.customer_details) ? 'is-half' : 'is-full'" v-if="props.visibleComponents.transaction_details">
<div class="content">
<h3 class="title is-4">Transaktions information</h3>
<p><strong>Dato:</strong> {{ order.created_at }}</p>
@@ -60,8 +102,8 @@ getUserOrder(orderId.value, true).then((response) => {
</div>
</div>
<!-- Customer information -->
<div class="column is-half">
<div class="content has-text-right">
<div class="column" :class="(props.visibleComponents.transaction_details && props.visibleComponents.customer_details) ? 'is-half' : 'is-full'" v-if="props.visibleComponents.customer_details">
<div class="content" :class="(props.visibleComponents.transaction_details ? 'has-text-right' : 'has-text-left')">
<h3 class="title is-4">Kunde information</h3>
<p><strong>CVR:</strong> {{ customer.economic_customer.corporateIdentificationNumber ? customer.economic_customer.corporateIdentificationNumber : 'Ingen CVR' }}</p>
<p><strong>Kunde ID:</strong> {{ customer.customer_number }}</p>
@@ -70,7 +112,7 @@ getUserOrder(orderId.value, true).then((response) => {
</div>
</div>
<!-- Vehicle information -->
<div class="column is-12 pt-0">
<div class="column is-12 pt-0" v-if="props.visibleComponents.vehicle_details">
<div class="divider">Køretøjer</div>
<div class="columns has-text-centered">
<div class="column" v-if="order.reg_1">
@@ -86,7 +128,7 @@ getUserOrder(orderId.value, true).then((response) => {
</div>
</div>
</div>
<OrderItemsTable :orderItems="orderItems" :isLoading="isLoading" />
<OrderItemsTable :orderItems="orderItems" :isLoading="isLoading" v-if="props.visibleComponents.order_items" v-bind:paid="props.orderObject.paid" v-bind:tax-percentage="order.tax_percentage" v-bind:show-payment-status="props.visibleComponents.payment_status" />
</UserDashboardPageWrapper>
</template>