From 4e021f8a68833bd47d638eb0147754476e87da79 Mon Sep 17 00:00:00 2001 From: Osama Muhammed <42488643+osama1998H@users.noreply.github.com> Date: Thu, 4 Sep 2025 21:29:21 +0300 Subject: [PATCH] localization: add Arabic translation for quick quantity shortcut and update relevant components to emit selected row events --- .../POS/Classic/SelectedItemRow.vue | 11 +- .../POS/Classic/SelectedItemTable.vue | 6 +- src/components/ShortcutsHelper.vue | 4 + src/pages/POS/ClassicPOS.vue | 2 + src/pages/POS/ModernPOS.vue | 3 + src/pages/POS/POS.vue | 179 ++++++++++++++++++ translations/ar.csv | 3 +- 7 files changed, 202 insertions(+), 6 deletions(-) diff --git a/src/components/POS/Classic/SelectedItemRow.vue b/src/components/POS/Classic/SelectedItemRow.vue index c1f1469e..74282e88 100644 --- a/src/components/POS/Classic/SelectedItemRow.vue +++ b/src/components/POS/Classic/SelectedItemRow.vue @@ -5,7 +5,7 @@ @click="isExapanded = !isExapanded" /> -
+
+
-
+
@@ -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 @@
@@ -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 ثم اكتب الأرقام لتعيين كمية الصنف المحدد",