Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions frontend/src/components/Sidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
{{ __("Main") }}
</p>
<router-link
v-for="item in mainNavItems"
v-for="item in mainNavItems.filter((it) => it.show)"
:key="item.route"
:to="item.route"
@click="isOpen = false"
Expand Down Expand Up @@ -147,6 +147,7 @@ import {
Wallet,
Landmark,
BarChart3,
Banknote,
} from "lucide-vue-next";

import LogoDark from "@/assets/images/xpos-logo-dark.svg";
Expand All @@ -171,11 +172,17 @@ onUnmounted(() => {
window.removeEventListener("xpos:toggle-sidebar", handleToggleSidebar);
});

const mainNavItems = [
{ route: "/pos", label: __("POS"), icon: LayoutGrid },
{ route: "/orders", label: __("Orders"), icon: FileText },
{ route: "/reports", label: __("Reports"), icon: BarChart3 },
];
const mainNavItems = computed(() => [
{ route: "/pos", label: __("POS"), icon: LayoutGrid, show: true },
{ route: "/orders", label: __("Orders"), icon: FileText, show: true },
{
route: "/cashier",
label: __("Cashier"),
icon: Banknote,
show: posStore.enableCashierSettlement && posStore.isCashier,
},
{ route: "/reports", label: __("Reports"), icon: BarChart3, show: true },
]);

const purchaseNavItems = [
{ route: "/purchase-order", label: __("Purchase Order"), icon: ClipboardList },
Expand Down
90 changes: 89 additions & 1 deletion frontend/src/components/cart/CartSummary.vue
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@
: 'bg-gradient-to-r from-primary to-primary/90 hover:from-primary/90 hover:to-primary/80 shadow-primary/25'
"
:disabled="cartStore.isEmpty || !cartStore.customer"
@click="cartStore.openPaymentDialog()"
@click="handleCheckout()"
>
<Wallet class="w-5 h-5" />
{{ payButtonLabel }}
Expand All @@ -349,6 +349,7 @@ import { call, showSuccess, showError } from "@/services/api";
import { __ } from "@/lib/translate";
import { isElectron } from "@/services/electronBridge";
import { useOfflineStore } from "@/stores/offlineStore";
import { usePrintInvoice } from "@/composables/usePrintInvoice";
import { Button } from "@/components/ui/button";
import { TooltipWrapper } from "@/components/ui/tooltip";
import { Input } from "@/components/ui/input";
Expand All @@ -373,6 +374,7 @@ const posStore = usePosStore();
const cartStore = useCartStore();
const offerStore = useOfferStore();
const offlineStore = useOfflineStore();
const { printInvoice, printInvoiceLocal } = usePrintInvoice();

const showDiscount = ref(false);
const showCoupon = ref(false);
Expand Down Expand Up @@ -432,10 +434,19 @@ const discountValue = computed(() => {
const payButtonLabel = computed(() => {
if (cartStore.isEmpty) return __("Add items to pay");
if (!cartStore.customer) return __("Select customer first");
if (posStore.enableCashierSettlement && !cartStore.isReturnMode) return __("Send to Cashier");
const amt = `${posStore.currencySymbol}${formatPrice(Math.abs(cartStore.grandTotal))}`;
return cartStore.isReturnMode ? __("Process Return {0}", [amt]) : __("Pay {0}", [amt]);
});

function handleCheckout() {
if (posStore.enableCashierSettlement && !cartStore.isReturnMode) {
sendToCashier();
return;
}
cartStore.openPaymentDialog();
}

function applyDiscount() {
cartStore.setDiscount(discountType.value as "percentage" | "amount", discountInput.value || 0);
}
Expand Down Expand Up @@ -666,6 +677,83 @@ async function holdOrder() {
}
}

async function sendToCashier() {
if (cartStore.isEmpty) return;

const profileName = posStore.profileName;
const shiftName = posStore.posOpeningShift?.name || "";

if (!profileName) {
showError(__("POS Profile is not set. Please close and reopen the shift."));
return;
}

if (!shiftName) {
showError(__("No open shift found. Please open a shift first."));
return;
}

try {
const data = cartStore.getInvoiceData(profileName, shiftName);
data.pos_awaiting_settlement = true;

if (!data.customer) {
const bootCustomer = (window.xpos?.boot as Record<string, unknown>)?.sysdefaults as
| Record<string, string>
| undefined;
data.customer = bootCustomer?.customer || "";
}

if (!data.customer) {
showError(__("Please select a customer before sending to cashier."));
return;
}

if (!data.items || data.items.length === 0) {
showError(__("No items in cart to send."));
return;
}

if (isElectron() && window.electronAPI?.db) {
const result = await window.electronAPI.db.addPendingInvoice({
data: { ...data, is_draft: true, pos_opening_shift_local_id: shiftName },
customer_name: cartStore.customerName || data.customer,
grand_total: cartStore.grandTotal || 0,
});
if (posStore.printBackupReceipt && result?.id) {
await printInvoiceLocal(result.id);
}
cartStore.clearAll();
showSuccess(__("Sent to cashier"));
} else {
const serverData = { ...data };
if (serverData.pos_opening_shift && /^\d+$/.test(String(serverData.pos_opening_shift))) {
delete serverData.pos_opening_shift;
}
if (offlineStore.isOnline) {
const result = await call<{ name: string }>("xpos.api.invoices.save_draft_invoice", {
data: JSON.stringify(serverData),
});
if (posStore.printBackupReceipt && result?.name) {
await printInvoice(result.name);
}
cartStore.clearAll();
showSuccess(__("Sent to cashier"));
} else {
await offlineStore.saveOffline(
{ ...serverData, is_draft: true } as Parameters<typeof offlineStore.saveOffline>[0],
cartStore.customerName || data.customer,
cartStore.grandTotal || 0,
);
cartStore.clearAll();
showSuccess(__("No connection – will sync to cashier when back online"));
}
}
} catch (error: unknown) {
showError(__("Failed to send to cashier: {0}", [extractErrorMessage(error)]));
}
}

function formatPrice(price: number | string) {
return parseFloat(String(price) || "0").toFixed(2);
}
Expand Down
61 changes: 2 additions & 59 deletions frontend/src/components/dialogs/PaymentDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,7 @@ import { usePaymentStore } from "@/stores/paymentStore";
import { call, showSuccess, showError, showInfo, isNetworkError } from "@/services/api";
import { useOfflineStore } from "@/stores/offlineStore";
import { __ } from "@/lib/translate";
import { usePrintInvoice } from "@/composables/usePrintInvoice";
import { isElectron } from "@/services/electronBridge";
import {
Dialog,
Expand Down Expand Up @@ -592,6 +593,7 @@ import {
} from "@/components/dialogs/paymentDialogShortcuts";

const posStore = usePosStore();
const { printInvoice, printInvoiceLocal } = usePrintInvoice();
const cartStore = useCartStore();
const paymentStore = usePaymentStore();
const offlineStore = useOfflineStore();
Expand Down Expand Up @@ -1140,65 +1142,6 @@ async function submitPayment(withPrint: boolean = true) {
}
}

async function printInvoice(invoiceName: string) {
try {
const printFormat = posStore?.defaultPrintFormat || "XPOS Thermal Receipt";
const letterHead = posStore.printSettings?.letter_head || "";

const usePosInvoice = xpos.boot?.pos_settings?.invoice_type === "POS Invoice";
const doctype = usePosInvoice ? "POS Invoice" : "Sales Invoice";

const baseUrl = window.location.origin;
const printUrl = `${baseUrl}/printview?doctype=${doctype}&name=${invoiceName}&format=${printFormat}&no_letterhead=${letterHead ? "0" : "1"}`;
const printWindow = window.open(printUrl, "_blank");

if (printWindow) {
printWindow.onload = () => {
printWindow.onafterprint = () => {
printWindow.close();
};
setTimeout(() => {
printWindow.print();
}, 500);
};
} else {
window.open(printUrl, "_blank");
}
} catch (error) {
console.error("Print error:", error);
showError(__("Failed to print invoice"));
}
}

async function printInvoiceLocal(localId: number) {
try {
if (!window.electronAPI?.db || !window.electronAPI?.print) {
showError(__("Print not available"));
return;
}

const invoice = await window.electronAPI.db.getPendingInvoice(localId);
if (!invoice) {
showError(__("Invoice not found for printing"));
return;
}

await window.electronAPI.print.printInvoice({
localId,
data: invoice.data,
customerName: invoice.customer_name || "",
grandTotal: invoice.grand_total,
isReturn: invoice.is_return,
printFormat: posStore.printSettings?.print_format || "POS Invoice",
letterHead: posStore.printSettings?.letter_head || "",
companyName: posStore.posProfile?.company || "",
});
} catch (error) {
console.error("Local print error:", error);
showError(__("Failed to print invoice locally"));
}
}

function close() {
cartStore.closePaymentDialog();
}
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/composables/useKeyboardShortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,16 @@ export function useKeyboardShortcuts() {
router.push("/reports");
},
},
{
id: "goto-cashier",
keys: ["alt", "0"],
description: "Go to Cashier",
category: "Navigation",
global: true,
action: () => {
router.push("/cashier");
},
},
{
id: "focus-cart",
keys: ["f3"],
Expand Down
85 changes: 85 additions & 0 deletions frontend/src/composables/usePrintInvoice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { usePosStore } from "@/stores/posStore";
import { showError } from "@/services/api";
import { __ } from "@/lib/translate";

export interface PrintInvoiceOptions {
/** Override the print format (defaults to the POS profile's default thermal receipt). */
format?: string;
/** Override the resolved doctype. */
doctype?: "Sales Invoice" | "POS Invoice";
}

/**
* Shared invoice printing helpers used by the payment dialog (genuine receipt),
* the terminal backup receipt, and the cashier settlement screen.
*/
export function usePrintInvoice() {
const posStore = usePosStore();

function resolveDoctype(): "Sales Invoice" | "POS Invoice" {
return xpos.boot?.pos_settings?.invoice_type === "POS Invoice"
? "POS Invoice"
: "Sales Invoice";
}

async function printInvoice(invoiceName: string, options: PrintInvoiceOptions = {}) {
try {
const printFormat = options.format || posStore?.defaultPrintFormat || "XPOS Thermal Receipt";
const letterHead = posStore.printSettings?.letter_head || "";
const doctype = options.doctype || resolveDoctype();

const baseUrl = window.location.origin;
const printUrl = `${baseUrl}/printview?doctype=${encodeURIComponent(doctype)}&name=${encodeURIComponent(
invoiceName,
)}&format=${encodeURIComponent(printFormat)}&no_letterhead=${letterHead ? "0" : "1"}`;
const printWindow = window.open(printUrl, "_blank");

if (printWindow) {
printWindow.onload = () => {
printWindow.onafterprint = () => {
printWindow.close();
};
setTimeout(() => {
printWindow.print();
}, 500);
};
} else {
window.open(printUrl, "_blank");
}
} catch (error) {
console.error("Print error:", error);
showError(__("Failed to print invoice"));
}
}

async function printInvoiceLocal(localId: number) {
try {
if (!window.electronAPI?.db || !window.electronAPI?.print) {
showError(__("Print not available"));
return;
}

const invoice = await window.electronAPI.db.getPendingInvoice(localId);
if (!invoice) {
showError(__("Invoice not found for printing"));
return;
}

await window.electronAPI.print.printInvoice({
localId,
data: invoice.data,
customerName: invoice.customer_name || "",
grandTotal: invoice.grand_total,
isReturn: invoice.is_return,
printFormat: posStore.printSettings?.print_format || "POS Invoice",
letterHead: posStore.printSettings?.letter_head || "",
companyName: posStore.posProfile?.company || "",
});
} catch (error) {
console.error("Local print error:", error);
showError(__("Failed to print invoice locally"));
}
}

return { printInvoice, printInvoiceLocal };
}
Loading
Loading