+
-
+
@@ -94,7 +95,7 @@ export default defineComponent({
isExapanded: false,
};
},
- emits: ['applyPricingRule'],
+ emits: ['applyPricingRule', 'selectedRow'],
computed: {
ratio() {
return [0.1, 0.9, 0.8, 0.8, 0.8, 0.8, 0.2];
@@ -158,6 +159,9 @@ export default defineComponent({
async runSinvFormulas() {
await this.sinvDoc.runFormulas();
},
+ selectedItemRow(row: SalesInvoiceItem) {
+ this.$emit('selectedRow', row);
+ },
isNumeric,
},
});
diff --git a/src/components/ShortcutsHelper.vue b/src/components/ShortcutsHelper.vue
index f50667da..6aca860e 100644
--- a/src/components/ShortcutsHelper.vue
+++ b/src/components/ShortcutsHelper.vue
@@ -216,6 +216,10 @@ export default defineComponent({
shortcut: [ShortcutKey.shift, 'P'],
description: t`Set Price List`,
},
+ {
+ shortcut: ['Q', '0-9'],
+ description: t`Hold Q and type digits to set selected item quantity`,
+ },
{
shortcut: [ShortcutKey.pmod, ShortcutKey.shift, 'H'],
description: t`Open Saved or Submitted Invoice List.`,
diff --git a/src/pages/POS/ClassicPOS.vue b/src/pages/POS/ClassicPOS.vue
index d4dd89f9..5612488f 100644
--- a/src/pages/POS/ClassicPOS.vue
+++ b/src/pages/POS/ClassicPOS.vue
@@ -180,6 +180,7 @@
$emit('selectedRow', row)"
/>
@@ -503,6 +504,7 @@ export default defineComponent({
'setTransferClearanceDate',
'saveAndContinue',
'handlePaymentAction',
+ 'selectedRow',
],
data() {
return {
diff --git a/src/pages/POS/ModernPOS.vue b/src/pages/POS/ModernPOS.vue
index 1d402d08..2a542e23 100644
--- a/src/pages/POS/ModernPOS.vue
+++ b/src/pages/POS/ModernPOS.vue
@@ -499,6 +499,7 @@ export default defineComponent({
'selectedReturnInvoice',
'setTransferClearanceDate',
'saveAndContinue',
+ 'selectedRow',
],
data() {
return {
@@ -524,6 +525,8 @@ export default defineComponent({
selectedRow(row: SalesInvoiceItem, field: string) {
this.selectedItemRow = row;
this.selectedItemField = field;
+ // Bubble up to POS to allow keyboard shortcuts to target this row
+ this.$emit('selectedRow', row);
},
getItem,
},
diff --git a/src/pages/POS/POS.vue b/src/pages/POS/POS.vue
index a828bd96..5d4be2b9 100644
--- a/src/pages/POS/POS.vue
+++ b/src/pages/POS/POS.vue
@@ -63,6 +63,7 @@
@set-transfer-clearance-date="setTransferClearanceDate"
@save-and-continue="handleSaveAndContinue"
@handle-payment-action="handlePaymentAction"
+ @selected-row="setQuickQtySelectedRow"
/>
@@ -240,6 +242,13 @@ export default defineComponent({
itemQtyMap: {} as ItemQtyMap,
coupons: {} as AppliedCouponCodes,
itemSerialNumbers: {} as ItemSerialNumbers,
+
+ // Quick Quantity via holding 'Q'
+ quickQtyActive: false,
+ quickQtyBuffer: '' as string,
+ quickQtyRow: null as SalesInvoiceItem | null,
+ quickQtyKeyDownHandler: null as ((e: KeyboardEvent) => void) | null,
+ quickQtyKeyUpHandler: null as ((e: KeyboardEvent) => void) | null,
};
},
computed: {
@@ -281,6 +290,7 @@ export default defineComponent({
this.setSinvDoc();
this.setDefaultCustomer();
this.setShortcuts();
+ this.addQuickQtyListeners();
await this.setItemQtyMap();
await this.setItems();
@@ -288,8 +298,177 @@ export default defineComponent({
deactivated() {
this.shortcuts?.delete(COMPONENT_NAME);
toggleSidebar(true);
+ this.removeQuickQtyListeners();
},
methods: {
+ setQuickQtySelectedRow(row: SalesInvoiceItem) {
+ this.quickQtyRow = row;
+ },
+ addQuickQtyListeners() {
+ this.quickQtyKeyDownHandler = (e: KeyboardEvent) =>
+ this.onQuickQtyKeyDown(e);
+ this.quickQtyKeyUpHandler = (e: KeyboardEvent) => this.onQuickQtyKeyUp(e);
+ window.addEventListener(
+ 'keydown',
+ this.quickQtyKeyDownHandler as EventListener
+ );
+ window.addEventListener(
+ 'keyup',
+ this.quickQtyKeyUpHandler as EventListener
+ );
+ },
+ removeQuickQtyListeners() {
+ if (this.quickQtyKeyDownHandler) {
+ window.removeEventListener(
+ 'keydown',
+ this.quickQtyKeyDownHandler as EventListener
+ );
+ this.quickQtyKeyDownHandler = null;
+ }
+ if (this.quickQtyKeyUpHandler) {
+ window.removeEventListener(
+ 'keyup',
+ this.quickQtyKeyUpHandler as EventListener
+ );
+ this.quickQtyKeyUpHandler = null;
+ }
+ },
+ hasAnyOpenModal(): boolean {
+ return (
+ this.openAlertModal ||
+ this.openPaymentModal ||
+ this.openKeyboardModal ||
+ this.openPriceListModal ||
+ this.openItemEnquiryModal ||
+ this.openCouponCodeModal ||
+ this.openShiftCloseModal ||
+ this.openSavedInvoiceModal ||
+ this.openLoyaltyProgramModal ||
+ this.openAppliedCouponsModal ||
+ this.openReturnSalesInvoiceModal
+ );
+ },
+ onQuickQtyKeyDown(e: KeyboardEvent) {
+ // Ignore if focus is in an input/contentEditable without modifiers
+ const notMods = !(e.altKey || e.metaKey || e.ctrlKey);
+ const target = e.target as HTMLElement | null;
+ if (
+ target &&
+ notMods &&
+ ((target instanceof HTMLInputElement && target.type !== 'button') ||
+ target instanceof HTMLTextAreaElement ||
+ target.isContentEditable)
+ ) {
+ return;
+ }
+
+ // Only active on POS page with no modal open
+ if (this.hasAnyOpenModal()) {
+ return;
+ }
+
+ if (e.code === 'KeyQ' && !this.quickQtyActive) {
+ this.quickQtyActive = true;
+ this.quickQtyBuffer = '';
+ return;
+ }
+
+ if (!this.quickQtyActive) {
+ return;
+ }
+
+ // While holding Q, collect digits; support both main digits and numpad
+ if (/^Digit[0-9]$/.test(e.code)) {
+ this.quickQtyBuffer += e.code.replace('Digit', '');
+ e.preventDefault();
+ return;
+ }
+
+ if (/^Numpad[0-9]$/.test(e.code)) {
+ this.quickQtyBuffer += e.code.replace('Numpad', '');
+ e.preventDefault();
+ return;
+ }
+
+ if (e.code === 'Backspace') {
+ this.quickQtyBuffer = this.quickQtyBuffer.slice(0, -1);
+ e.preventDefault();
+ return;
+ }
+ },
+ async onQuickQtyKeyUp(e: KeyboardEvent) {
+ if (e.code !== 'KeyQ' || !this.quickQtyActive) {
+ return;
+ }
+
+ this.quickQtyActive = false;
+
+ const buffer = this.quickQtyBuffer;
+ this.quickQtyBuffer = '';
+
+ if (!buffer || !buffer.length) {
+ return;
+ }
+
+ const qty = Number(buffer);
+ if (!Number.isFinite(qty)) {
+ return;
+ }
+
+ // Determine target row: prefer explicitly selected row; else fallback to last non-free item
+ let row: SalesInvoiceItem | null = this.quickQtyRow;
+ if (!row || !(this.sinvDoc.items || []).includes(row)) {
+ const items = (this.sinvDoc.items || []).filter((r) => !r.isFreeItem);
+ row = items.length
+ ? (items[items.length - 1] as SalesInvoiceItem)
+ : null;
+ }
+
+ if (!row) {
+ return;
+ }
+
+ // Validate and recalculate similar to keyboard modal quantity change
+ const prevQty = row.quantity ?? 1;
+
+ if (!row.isReturn && qty <= 0) {
+ showToast({
+ type: 'error',
+ message: t`Quantity must be greater than zero.`,
+ duration: 'short',
+ });
+ return;
+ }
+
+ try {
+ await row.set('quantity', qty);
+
+ const existingItems =
+ (this.sinvDoc.items || []).filter(
+ (invoiceItem: InvoiceItem) =>
+ invoiceItem.item === row!.item && !invoiceItem.isFreeItem
+ ) || [];
+
+ await validateQty(
+ this.sinvDoc as SalesInvoice,
+ row,
+ existingItems as unknown as InvoiceItem[]
+ );
+ } catch (error) {
+ await row.set('quantity', prevQty);
+ showToast({
+ type: 'error',
+ message: t`${error as string}`,
+ duration: 'short',
+ });
+ return;
+ }
+
+ if (!row.isFreeItem) {
+ await this.applyPricingRule();
+ await this.sinvDoc.runFormulas();
+ }
+ },
async setCustomer(value: string) {
if (!value) {
this.sinvDoc.party = '';
diff --git a/translations/ar.csv b/translations/ar.csv
index 7dc968a7..391acdcd 100644
--- a/translations/ar.csv
+++ b/translations/ar.csv
@@ -1098,4 +1098,5 @@ Transfer,تحويل,
"Qty","الكمية",
"Held","معلّقة",
"Grid View","عرض الشبكة",
-"Buy","شراء",
\ No newline at end of file
+"Buy","شراء",
+"Hold Q and type digits to set selected item quantity","استمر بالضغط على Q ثم اكتب الأرقام لتعيين كمية الصنف المحدد",