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:
+1
-1
@@ -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
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user