Improve print invoice flow and caching strategy:

- Simplified print dialog logic in `PrintInvoiceFromOrderItems.vue` using dynamic URL-based receipts and onload handling.
- Enhanced service worker caching configuration in `vite.config.js` with specific caching rules for assets, API responses, images, and HTML files.
- Updated `ViewportHeader.vue` with visibility toggle and responsiveness improvements.
- Adjusted camera capture delays in `PosDepartmentStepMobileFlow.vue` for better user experience.
- Refactored department order views to manage display states for print receipt mode.
This commit is contained in:
Jeppe Bundgaard
2025-08-28 14:07:32 +02:00
parent 8b27a02e3a
commit f2bfef3c88
6 changed files with 145 additions and 33 deletions
@@ -461,7 +461,7 @@ const latestImage = ref<string | null>(null);
const isCameraMounted = ref<boolean>(false);
const cameraImageCaptureDelayInitial = ref<number>(2000); // Initial delay for camera image capture in milliseconds (First capture)
const cameraImageCaptureDelaySubsequent = ref<number>(1000); // Further captures delay in milliseconds (Every capture after the first one)
const cameraImageCaptureDelayAfterSuccess = ref<number>(5000); // Delay after a successful capture in milliseconds (After a successful capture, before the next one)
const cameraImageCaptureDelayAfterSuccess = ref<number>(2000); // Delay after a successful capture in milliseconds (After a successful capture, before the next one)
const cameraImageCaptureLastSuccess = ref<number | null>(null); // Timestamp of the last successful capture
// Function to set the timestamp of the last successful capture
@@ -29,26 +29,21 @@ const calculatePriceAfterTax = (price, tax_percentage) => {
const printInvoice = () => {
// Show a print dialog, with a table of the order items
// and the tax percentage
const printWindow = window.open('', '_blank');
printWindow.document.write('<html><head><title>Invoice #' + props.order_id + '</title></head>');
printWindow.document.write('<table border="1" style="width: 100%"><tr><th>Item</th><th>Amount</th><th>Price</th><th>Total</th></tr>');
props.orderItems.forEach(item => {
printWindow.document.write(`<tr><td>${item.product.name}</td><td>${item.quantity}</td><td>${item.price.toFixed(2)} DKK</td><td>${(item.price * item.quantity).toFixed(2)} DKK</td></tr>`);
});
// Add a row for the tax percentage
printWindow.document.write(`<tr><td colspan="3">Tax (${props.tax_percentage}%)</td><td>${props.orderItems.reduce((total, item) => total + (item.price * item.quantity), 0) * (props.tax_percentage / 100).toFixed(2)} DKK</td></tr>`);
// Add a row for the total
printWindow.document.write(`<tr><td colspan="3">Total</td><td>${props.orderItems.reduce((total, item) => total + (item.price * item.quantity), 0) * (1 + props.tax_percentage / 100).toFixed(2)} DKK</td></tr>`);
printWindow.document.write('</table>');
// Add a PAID / UNPAID label
if (props.paid) {
printWindow.document.write('<h2 style="color: green; text-align: center">PAID</h2>');
const origin = window.location.origin;
const departmentId = SessionUser.functions.getDepartmentIdFromUrl();
const printWindow = window.open(`${origin}/admin/${departmentId}/modules/pos/orders/${props.order_id}?print_receipt=true`, '_blank');
//console.warn('Print window URL:', `${origin}/admin/${departmentId}/modules/pos/orders/${props.order_id}?print_receipt=true`);
// Wait for the window to load before printing
printWindow.onload = () => {
// Set the title of the print window
printWindow.document.title = `Invoice #${props.order_id}`;
// wait 1 second to ensure content is fully loaded
setTimeout(() => {
printWindow.focus();
printWindow.print();
printWindow.close();
}, 500);
}
printWindow.document.write('</body></html>');
printWindow.document.close();
printWindow.focus();
printWindow.print();
printWindow.close();
};
</script>
@@ -2,10 +2,11 @@
import ViewportResponsiveWrapper from "@/components/viewport/conditions/elements/ViewportResponsiveWrapper.vue";
import MobileHeader from "@/components/viewport/page/headers/MobileHeader.vue";
import DesktopHeader from "@/components/viewport/page/headers/DesktopHeader.vue";
import { isHidden } from "./ViewportHeaderSettings.vue";
</script>
<template>
<ViewportResponsiveWrapper>
<ViewportResponsiveWrapper v-if="!isHidden">
<template #mobile>
<MobileHeader />
</template>
@@ -1,6 +1,7 @@
<script lang="ts">
import { defineComponent, ref } from 'vue';
const isOpen = ref(false); // State of the navigation drawer (Expanded or collapsed)
const isHidden = ref(false); // State of the header (Visible or hidden)
const backgroundColor = ref('#ffffff'); // Background color of header
const defaultImagePath = '@/assets/branding/truckwash-banner-white-compressed.png'
const isTransparent = ref(false); // State for transparency of the header
@@ -30,6 +31,21 @@ const setOverflow = (isVisible: boolean) => {
isOverflowVisible.value = isVisible;
};
// Function to toggle the visibility of the header
const toggleVisibility = () => {
isHidden.value = !isHidden.value;
};
// Function to set the visibility of the header
const setVisibility = (visible: boolean) => {
isHidden.value = !visible;
};
// Function to get the current visibility state of the header
const getVisibility = () => {
return !isHidden.value;
};
export default defineComponent({
name: 'ViewportHeaderSettings',
setup() {
@@ -44,9 +60,13 @@ export default defineComponent({
backgroundColors,
isOverflowVisible,
setOverflow,
isHidden,
toggleVisibility,
setVisibility,
getVisibility,
};
},
});
export { toggleExpanded, setBackgroundColor, isOpen, backgroundColor, defaultImagePath, isTransparent, setTransparency, backgroundColors, isOverflowVisible, setOverflow };
export { toggleExpanded, setBackgroundColor, isOpen, backgroundColor, defaultImagePath, isTransparent, setTransparency, backgroundColors, isOverflowVisible, setOverflow, isHidden, toggleVisibility, setVisibility, getVisibility };
</script>
@@ -4,7 +4,7 @@ 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 {onMounted, ref, watch} from "vue";
import {onMounted, onUnmounted, 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";
@@ -34,6 +34,20 @@ 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";
import { getVisibility, setVisibility } from "@/components/viewport/page/headers/ViewportHeaderSettings.vue"
import { defineProps } from "vue";
// Props
const props = defineProps({
isPrintReceipt: {
type: Boolean,
default: false
}
});
onUnmounted(() => {
setVisibility(true);
});
// Get the id from the route
const orderId = ref(router.currentRoute.value.params.orderId);
@@ -136,10 +150,24 @@ const getStripeInvoiceStatus = () => {
const isStripeInvoicePaid = () => {
return stripeModule.value.paid;
};
const isShowingPrintReceipt = ref(false);
// Load the order, when the page is loaded
loadOrder();
onMounted(async () => {
await loadOrder();
await loadOrderItems();
await getInvoiceCollection();
// Check if the query parameter "print_receipt" is set to true
if (props.isPrintReceipt || router.currentRoute.value.query.print_receipt === 'true') {
console.warn('Printing receipt...');
setVisibility(false);
isShowingPrintReceipt.value = true;
} else {
console.warn('Not printing receipt...');
setVisibility(true);
}
});
const forceAllowEdit = ref(false);
// Make sure the order isn't invoiced, and the user has permission to edit the order items
@@ -277,32 +305,35 @@ const getStripePaymentStatus = () => {
return result;
};
const isShowingPrintReceipt = ref(false);
getDepartments();
const openPrintDialog = () => {
isShowingPrintReceipt.value = true;
setVisibility(false);
// Use the window.print() function to open the print dialog
setTimeout(() => {
window.print();
isShowingPrintReceipt.value = false;
}, 500);
setVisibility(true);
}, 1000);
};
const isDisplayingReceipt = () => {
return props.isPrintReceipt || !getVisibility() || router.currentRoute.value.query.print_receipt === 'true' || isShowingPrintReceipt.value;
};
</script>
<template>
<RestrictedPageWrapper :hasPermission="SessionUser.canAccessAdmin()">
<DepartmentDashboardPageWrapper
v-show="!isShowingPrintReceipt"
title="Kassesystem"
subtitle="Transaktion"
>
<notFoundFallBackPageWrapper
v-bind:exists="doesOrderExist || isLoading"
error="Denne handel findes ikke, eller du har ikke adgang til at se den.">
<div class="columns">
<div class="columns" v-show="!isDisplayingReceipt()">
<div class="column is-8-desktop" v-if="AddOrderItemsVisible">
<SelectProductsFormPOS class="mt-3" category="products" />
</div>
@@ -632,7 +663,7 @@ const openPrintDialog = () => {
<!--<ExportOrderToDraftButton :order_id="orderId" class="is-fullwidth" v-if="!isInvoiced() && invoiceAllOrdersIndividually() && !invoiceUsingStripe()" /> -->
<getOrderInvoicePDFButton :invoice_id="economicModule.invoice_id" class="is-fullwidth" v-if="isBookedWithEconomic()" />
<!-- If the order is invoiced with Stripe, then show a button to get the invoice -->
<ExportOrderToInvoiceStripeButton :order_id="orderId" class="is-fullwidth" v-if="!isInvoiced() && invoiceUsingStripe()" />
<ExportOrderToInvoiceStripeButton :order_id="orderId" class="is-fullwidth" v-if="!isInvoiced() && invoiceUsingStripe() && isPaymentMethodStripe()" />
<button class="button is-dark is-fullwidth" @click="openStripeInvoice" v-if="isInvoicedWithStripe()">
<span class="icon"><i class="fas fa-file-invoice"></i></span>
<span>Hent faktura</span>
@@ -673,7 +704,7 @@ const openPrintDialog = () => {
</notFoundFallBackPageWrapper>
</DepartmentDashboardPageWrapper>
<div v-if="isShowingPrintReceipt">
<div v-show="isDisplayingReceipt()" v-if="isPaymentCollectionLoaded()">
<!-- 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;"/>
+67 -2
View File
@@ -15,11 +15,11 @@ function getGitCommit() {
return 'unknown'
}
}
export default defineConfig(() => {
// Set COMMIT_HASH env var for use in the app
const version = process.env.npm_package_version || '0.0.0'
const commit = getGitCommit()
console.log(`Building version ${version} (commit: ${commit})`)
process.env.COMMIT_HASH = commit
process.env.APP_VERSION = version
@@ -37,8 +37,73 @@ export default defineConfig(() => {
},
workbox: {
maximumFileSizeToCacheInBytes: 12 * 1024 * 1024,
cleanupOutdatedCaches: true,
skipWaiting: true,
clientsClaim: true
clientsClaim: true,
// Set the entire site to be precached (maximum for 24 hours).
globPatterns: ['**/*.{js,css,html,ico,svg,woff2}'],
globIgnores: ['**/registerSW.js', '**/sw.js'],
runtimeCaching: [
{ // cache API responses
urlPattern: ({url}) => url.origin === 'https://api.truckwash.dk',
handler: 'NetworkFirst',
options: {
cacheName: 'pleno-api-cache',
expiration: {
maxEntries: 50,
maxAgeSeconds: 2, // 2 seconds
},
networkTimeoutSeconds: 10,
cacheableResponse: {
statuses: [0, 200]
}
}
},
{ // cache website assets
urlPattern: ({url}) => url.origin === 'https://truckwash.dk' || url.origin === 'https://www.truckwash.dk' || (origin.includes('http://localhost') || origin.includes('https://twdev.jeppeb.dk')),
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'pleno-website-cache',
expiration: {
maxEntries: 20,
maxAgeSeconds: 24 * 60 * 60 // 24 hours
},
cacheableResponse: {
statuses: [0, 200]
}
}
},
{ // cache images
urlPattern: ({url}) => url.pathname.endsWith('.png') || url.pathname.endsWith('.jpg') || url.pathname.endsWith('.jpeg') || url.pathname.endsWith('.svg') || url.pathname.endsWith('.gif'),
handler: 'CacheFirst',
options: {
cacheName: 'pleno-image-cache',
expiration: {
maxEntries: 50,
maxAgeSeconds: 7 * 24 * 60 * 60 // 7 days
},
cacheableResponse: {
statuses: [0, 200]
}
}
},
{ // cache .html files
urlPattern: ({url}) => url.pathname.endsWith('.html'),
handler: 'NetworkFirst',
options: {
cacheName: 'pleno-html-cache',
expiration: {
maxEntries: 10,
maxAgeSeconds: 24 * 60 * 60 // 24 hours
},
networkTimeoutSeconds: 10,
cacheableResponse: {
statuses: [0, 200]
}
}
}
]
},
manifest: {
name: 'Pleno',