Skip to content
Open
105 changes: 105 additions & 0 deletions POS/src/composables/useKeyboardShortcuts.js
Original file line number Diff line number Diff line change
@@ -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<null|Object>
* @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/<profile-name> 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);
});
}
32 changes: 26 additions & 6 deletions POS/src/pages/POSSale.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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([]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
};
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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"),
Expand Down Expand Up @@ -192,25 +204,33 @@ 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,
}

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)
Expand Down Expand Up @@ -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"],
}