diff --git a/POS/src/composables/useKeyboardShortcuts.js b/POS/src/composables/useKeyboardShortcuts.js new file mode 100644 index 00000000..4cb3d2b9 --- /dev/null +++ b/POS/src/composables/useKeyboardShortcuts.js @@ -0,0 +1,105 @@ +import { onMounted, onUnmounted } from "vue"; + +/** + * useKeyboardShortcuts + * + * Registers POS-scoped keyboard shortcuts with strict guards: + * Ctrl+Shift+O → View Shift (only when shift is open) + * Ctrl+Shift+D → Draft Invoices + * Ctrl+Shift+H → Invoice History + * Ctrl+Shift+C → Close Shift (only when shift is open) + * Ctrl+Shift+N → Create Customer + * Ctrl+Alt+R → Return Invoice + * + * @param {Object} options + * @param {import('@/stores/posUI').POSUIStore} options.uiStore + * @param {import('@/stores/posShift').POSShiftStore} options.shiftStore + * @param {import('vue').Ref} options.editCustomer – ref + * @param {() => boolean} [options.isLocalOverlayOpen] – optional callback + * returning true when a non-uiStore overlay (Promotion, Settings, + * StockLookup, InvoiceManagement, InvoiceDetail) is open + */ +export function useKeyboardShortcuts({ + uiStore, + shiftStore, + editCustomer, + isLocalOverlayOpen = () => false, +}) { + function isEditableTarget(el) { + if (!el) return false; + const tag = el.tagName; + if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true; + if (el.isContentEditable) return true; + return false; + } + + function isOnPOSPage() { + // Match /pos or /pos/ but NOT /reports/sales-pos etc. + return /\/pos(\/|$)/.test(window.location.pathname); + } + + function handler(e) { + // ── Route guard ──────────────────────────────────────────────────────── + if (!isOnPOSPage()) return; + + // ── Input-field guard ────────────────────────────────────────────────── + if (isEditableTarget(e.target)) return; + + // ── Dialog / overlay guard ───────────────────────────────────────────── + if (uiStore.isAnyDialogOpen) return; + if (isLocalOverlayOpen()) return; + + const key = e.key.toLowerCase(); + + // ── Ctrl + Shift shortcuts (Alt must NOT be pressed) ─────────────────── + if (e.ctrlKey && e.shiftKey && !e.altKey && !e.metaKey) { + switch (key) { + case "o": // View Shift + if (shiftStore.hasOpenShift) { + e.preventDefault(); + uiStore.showOpenShiftDialog = true; + } + break; + + case "d": // Draft Invoices + e.preventDefault(); + uiStore.showDraftDialog = true; + break; + + case "h": // Invoice History + e.preventDefault(); + uiStore.showHistoryDialog = true; + break; + + case "c": // Close Shift + if (shiftStore.hasOpenShift) { + e.preventDefault(); + uiStore.showCloseShiftDialog = true; + } + break; + + case "n": // Create Customer + e.preventDefault(); + editCustomer.value = null; + uiStore.setInitialCustomerName(""); + uiStore.showCreateCustomerDialog = true; + break; + } + } + + // ── Ctrl + Alt + R → Return Invoice ───────────────────────────────── + // (Alt must be pressed, Shift must NOT be pressed to avoid Ctrl+Shift+Alt+R) + if (e.ctrlKey && e.altKey && !e.shiftKey && !e.metaKey && key === "r") { + e.preventDefault(); + uiStore.showReturnDialog = true; + } + } + + onMounted(() => { + document.addEventListener("keydown", handler); + }); + + onUnmounted(() => { + document.removeEventListener("keydown", handler); + }); +} diff --git a/POS/src/pages/POSSale.vue b/POS/src/pages/POSSale.vue index 20a7664b..bf0ad40f 100644 --- a/POS/src/pages/POSSale.vue +++ b/POS/src/pages/POSSale.vue @@ -996,6 +996,8 @@ import { useRealtimeStock } from "@/composables/useRealtimeStock"; import { useSessionLock } from "@/composables/useSessionLock"; import { usePOSEvents } from "@/composables/usePOSEvents"; import { useLocale } from "@/composables/useLocale"; +import { useKeyboardShortcuts } from "@/composables/useKeyboardShortcuts"; +import { registerDialog } from "@/composables/useDialogState"; import { session } from "@/data/session"; import { useUserData } from "@/data/user"; import { parseError } from "@/utils/errorHandler"; @@ -1092,22 +1094,40 @@ function computeCartHash() { .join("|"); } -// Promotion dialog -const showPromotionManagement = ref(false); +// Promotion dialog — registered so isAnyDialogOpen sees it +const showPromotionManagement = registerDialog(ref(false), "promotionManagement"); // Settings dialog -const showPOSSettings = ref(false); +const showPOSSettings = registerDialog(ref(false), "posSettings"); // Stock Lookup dialog (Products menu) -const showStockLookup = ref(false); +const showStockLookup = registerDialog(ref(false), "stockLookup"); // Invoice Management dialog -const showInvoiceManagement = ref(false); +const showInvoiceManagement = registerDialog(ref(false), "invoiceManagement"); // Invoice Detail dialog -const showInvoiceDetail = ref(false); +const showInvoiceDetail = registerDialog(ref(false), "invoiceDetail"); const selectedInvoiceForView = ref(null); +// ================================ +// Keyboard Shortcuts Registration +// ================================ +// Must be called after all dialog refs are declared so isLocalOverlayOpen +// can safely reference them inside the keydown event handler. +useKeyboardShortcuts({ + uiStore, + shiftStore, + editCustomer, + // Block shortcuts when local overlays (not tracked by uiStore) are open + isLocalOverlayOpen: () => + showPromotionManagement.value || + showPOSSettings.value || + showStockLookup.value || + showInvoiceManagement.value || + showInvoiceDetail.value, +}); + // Invoice history data (used by InvoiceManagement component) const invoiceHistoryData = ref([]); diff --git a/pos_next/pos_next/report/payments_and_cash_control_report/payments_and_cash_control_report.js b/pos_next/pos_next/report/payments_and_cash_control_report/payments_and_cash_control_report.js index 3d5723fa..69e3c9a2 100644 --- a/pos_next/pos_next/report/payments_and_cash_control_report/payments_and_cash_control_report.js +++ b/pos_next/pos_next/report/payments_and_cash_control_report/payments_and_cash_control_report.js @@ -34,6 +34,12 @@ frappe.query_reports["Payments and Cash Control Report"] = { "label": __("Cashier"), "fieldtype": "Link", "options": "User" + }, + { + "fieldname": "mode_of_payment", + "label": __("Mode of Payment"), + "fieldtype": "Link", + "options": "Mode of Payment" } ] }; diff --git a/pos_next/pos_next/report/payments_and_cash_control_report/payments_and_cash_control_report.py b/pos_next/pos_next/report/payments_and_cash_control_report/payments_and_cash_control_report.py index 24549cb0..3dae6e4c 100644 --- a/pos_next/pos_next/report/payments_and_cash_control_report/payments_and_cash_control_report.py +++ b/pos_next/pos_next/report/payments_and_cash_control_report/payments_and_cash_control_report.py @@ -76,6 +76,12 @@ def get_columns(payment_methods): for method in payment_methods: safe = method.lower().replace(" ", "_") columns.extend([ + { + "fieldname": f"{safe}_opening", + "label": _(f"{method} Opening"), + "fieldtype": "Currency", + "width": 130 + }, { "fieldname": f"{safe}_expected", "label": _(f"{method} Expected"), @@ -97,6 +103,12 @@ def get_columns(payment_methods): ]) columns.extend([ + { + "fieldname": "total_opening", + "label": _("Total Opening"), + "fieldtype": "Currency", + "width": 130 + }, { "fieldname": "total_expected", "label": _("Total Expected"), @@ -192,6 +204,7 @@ def get_data(filters): "shift_end": r.shift_end, "shift_hours": shift_hours, "total_transactions": transaction_map.get(r.shift, 0), + "total_opening": 0, "total_expected": 0, "total_closing": 0, "total_difference": 0, @@ -199,18 +212,25 @@ def get_data(filters): row = shifts[r.shift] safe = r.payment_method.lower().replace(" ", "_") - row[f"{safe}_expected"] = flt(r.expected_amount, 2) - row[f"{safe}_closing"] = flt(r.closing_amount, 2) - row[f"{safe}_diff"] = flt(r.difference, 2) - - row["total_expected"] += flt(r.expected_amount, 2) - row["total_closing"] += flt(r.closing_amount, 2) - row["total_difference"] += flt(r.difference, 2) + opening = flt(r.opening_amount, 2) + expected = flt(r.expected_amount, 2) + closing = flt(r.closing_amount, 2) + diff = flt(closing - expected, 2) + row[f"{safe}_opening"] = opening + row[f"{safe}_expected"] = expected + row[f"{safe}_closing"] = closing + row[f"{safe}_diff"] = diff + + row["total_opening"] += opening + row["total_expected"] += expected + row["total_closing"] += closing + row["total_difference"] += diff # Build final data list and determine status data = [] for shift_name in shift_order: row = shifts[shift_name] + row["total_opening"] = flt(row["total_opening"], 2) row["total_expected"] = flt(row["total_expected"], 2) row["total_closing"] = flt(row["total_closing"], 2) row["total_difference"] = flt(row["total_difference"], 2) @@ -274,29 +294,34 @@ def get_conditions(filters): if filters.get("shift"): conditions.append("pcs.name = %(shift)s") + if filters.get("mode_of_payment"): + conditions.append("pr.mode_of_payment = %(mode_of_payment)s") + return " AND " + " AND ".join(conditions) if conditions else "" def get_chart_data(data, payment_methods): - """Generate chart showing payment method breakdown""" - if not data or not payment_methods: + """Generate chart showing opening, closing, and difference per shift.""" + if not data: return None - # Aggregate expected amounts by payment method across all shifts - datasets = [] - for method in payment_methods: - safe = method.lower().replace(" ", "_") - values = [flt(row.get(f"{safe}_expected", 0)) for row in data] - datasets.append({ - "name": method, - "values": values - }) + labels = [row.get("shift") for row in data] + opening_values = [flt(row.get("total_opening", 0), 2) for row in data] + expected_values = [flt(row.get("total_expected", 0), 2) for row in data] + closing_values = [flt(row.get("total_closing", 0), 2) for row in data] + diff_values = [flt(row.get("total_difference", 0), 2) for row in data] return { "data": { - "labels": [row.get("shift") for row in data], - "datasets": datasets + "labels": labels, + "datasets": [ + {"name": _("Opening"), "values": opening_values}, + {"name": _("Expected"), "values": expected_values}, + {"name": _("Closing"), "values": closing_values}, + {"name": _("Difference"), "values": diff_values}, + ] }, "type": "bar", - "barOptions": {"stacked": True} + "fieldtype": "Currency", + "colors": ["#318AD8", "#F5A623", "#48BB74", "#F56B6B"], }