From 4637dd55188df7fcf8234f4d2ba27ddbea5428dd Mon Sep 17 00:00:00 2001 From: AbleKSaju <126228406+AbleKSaju@users.noreply.github.com> Date: Tue, 3 Dec 2024 14:44:15 +0530 Subject: [PATCH 1/8] feat: add modal names type --- src/components/POS/types.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/components/POS/types.ts b/src/components/POS/types.ts index 0a127ca00..ea31a0f46 100644 --- a/src/components/POS/types.ts +++ b/src/components/POS/types.ts @@ -8,15 +8,18 @@ export type ItemSerialNumbers = { [item: string]: string }; export type DiscountType = 'percent' | 'amount'; -export type ModalName = - | 'Keyboard' - | 'Payment' - | 'ShiftClose' - | 'LoyaltyProgram' - | 'SavedInvoice' - | 'Alert' - | 'CouponCode' - | 'PriceList'; +export const modalNames = [ + 'Keyboard', + 'Payment', + 'ShiftClose', + 'LoyaltyProgram', + 'SavedInvoice', + 'Alert', + 'CouponCode', + 'PriceList', +] as const; + +export type ModalName = typeof modalNames[number]; export type PosEmits = | 'addItem' From ce1831502c6e649c52754431fe77efa1b00dafc9 Mon Sep 17 00:00:00 2001 From: AbleKSaju <126228406+AbleKSaju@users.noreply.github.com> Date: Tue, 3 Dec 2024 14:45:18 +0530 Subject: [PATCH 2/8] feat: added Cmd+Shift shortcut --- src/utils/shortcuts.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/utils/shortcuts.ts b/src/utils/shortcuts.ts index 7a11034f0..67e960cbf 100644 --- a/src/utils/shortcuts.ts +++ b/src/utils/shortcuts.ts @@ -224,6 +224,12 @@ export class Shortcuts { return this; } + get pmodShift() { + this.modMap['meta'] = true; + this.modMap['shift'] = true; + return this; + } + get repeat() { this.modMap['repeat'] = true; return this; From 7b019f294c704d9185b281721cc1d3b51a219a70 Mon Sep 17 00:00:00 2001 From: AbleKSaju <126228406+AbleKSaju@users.noreply.github.com> Date: Tue, 3 Dec 2024 14:46:26 +0530 Subject: [PATCH 3/8] feat: added keyboard shortcuts in POS --- src/pages/POS/POS.vue | 84 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 81 insertions(+), 3 deletions(-) diff --git a/src/pages/POS/POS.vue b/src/pages/POS/POS.vue index f5527d6e9..8ff1a8507 100644 --- a/src/pages/POS/POS.vue +++ b/src/pages/POS/POS.vue @@ -104,14 +104,15 @@ import ModernPOS from './ModernPOS.vue'; import ClassicPOS from './ClassicPOS.vue'; import { ModelNameEnum } from 'models/types'; import Button from 'src/components/Button.vue'; -import { computed, defineComponent } from 'vue'; import { showToast } from 'src/utils/interactive'; import { Item } from 'models/baseModels/Item/Item'; -import { ModalName } from 'src/components/POS/types'; import { Shipment } from 'models/inventory/Shipment'; import { routeTo, toggleSidebar } from 'src/utils/ui'; +import { shortcutsKey } from 'src/utils/injectionKeys'; import PageHeader from 'src/components/PageHeader.vue'; +import { computed, defineComponent, inject } from 'vue'; import { Payment } from 'models/baseModels/Payment/Payment'; +import { ModalName, modalNames } from 'src/components/POS/types'; import { InvoiceItem } from 'models/baseModels/InvoiceItem/InvoiceItem'; import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice'; import { SalesInvoiceItem } from 'models/baseModels/SalesInvoiceItem/SalesInvoiceItem'; @@ -138,6 +139,8 @@ import { ItemSerialNumbers, } from 'src/components/POS/types'; +const COMPONENT_NAME = 'POS'; + export default defineComponent({ name: 'POS', components: { @@ -163,6 +166,11 @@ export default defineComponent({ transferClearanceDate: computed(() => this.transferClearanceDate), }; }, + setup() { + return { + shortcuts: inject(shortcutsKey), + }; + }, data() { return { tableView: true, @@ -242,10 +250,13 @@ export default defineComponent({ this.setCouponCodeDoc(); this.setSinvDoc(); this.setDefaultCustomer(); + this.setShortcuts(); + await this.setItemQtyMap(); await this.setItems(); }, deactivated() { + this.shortcuts?.delete(COMPONENT_NAME); toggleSidebar(true); }, methods: { @@ -265,6 +276,71 @@ export default defineComponent({ this.loyaltyProgram = party[0]?.loyaltyProgram as string; this.loyaltyPoints = party[0]?.loyaltyPoints as number; }, + setShortcuts() { + this.shortcuts?.shift.set(COMPONENT_NAME, ['KeyS'], async () => { + this.routeToSinvList(); + }); + + this.shortcuts?.shift.set(COMPONENT_NAME, ['KeyV'], async () => { + this.toggleView(); + }); + + this.shortcuts?.shift.set(COMPONENT_NAME, ['KeyP'], async () => { + this.toggleModal('PriceList'); + }); + + this.shortcuts?.pmodShift.set(COMPONENT_NAME, ['KeyH'], async () => { + this.toggleModal('SavedInvoice'); + }); + + this.shortcuts?.pmodShift.set(COMPONENT_NAME, ['Backspace'], async () => { + let anyModalClosed = false; + + modalNames.forEach((modal: ModalName) => { + if (modal && this[`open${modal}Modal`]) { + this[`open${modal}Modal`] = false; + anyModalClosed = true; + } + }); + + if (!anyModalClosed) { + this.clearValues(); + } + }); + + this.shortcuts?.pmodShift.set(COMPONENT_NAME, ['KeyP'], async () => { + if (!this.disablePayButton) { + this.toggleModal('Payment'); + } + }); + + this.shortcuts?.pmodShift.set(COMPONENT_NAME, ['KeyS'], async () => { + if (this.sinvDoc.party && this.sinvDoc.items?.length) { + this.saveOrder(); + } + }); + + this.shortcuts?.shift.set(COMPONENT_NAME, ['KeyL'], async () => { + if ( + this.fyo.singles.AccountingSettings?.enablePriceList && + this.loyaltyPoints && + this.sinvDoc.party && + this.sinvDoc.items?.length + ) { + this.toggleModal('LoyaltyProgram', true); + } + }); + + this.shortcuts?.shift.set(COMPONENT_NAME, ['KeyC'], async () => { + if ( + this.fyo.singles.AccountingSettings?.enableCouponCode && + this.sinvDoc?.party && + this.sinvDoc?.items?.length + ) { + this.toggleModal('CouponCode'); + } + }); + }, async saveOrder() { try { await this.validate(); @@ -482,6 +558,7 @@ export default defineComponent({ await this.applyPricingRule(); await this.sinvDoc.runFormulas(); }, + async createTransaction(shouldPrint = false) { try { await this.validate(); @@ -640,9 +717,10 @@ export default defineComponent({ if (!hasPricingRules || !hasPricingRules.length) { this.sinvDoc.pricingRuleDetail = undefined; this.sinvDoc.isPricingRuleApplied = false; - removeFreeItems(this.sinvDoc as SalesInvoice); + removeFreeItems(this.sinvDoc as SalesInvoice); await this.sinvDoc.applyProductDiscount(); + return; } From f4a3307767012f1237e96a9f43b7493ba0f76a2f Mon Sep 17 00:00:00 2001 From: AbleKSaju <126228406+AbleKSaju@users.noreply.github.com> Date: Tue, 3 Dec 2024 14:48:49 +0530 Subject: [PATCH 4/8] feat: add shortcut descriptions --- src/components/ShortcutsHelper.vue | 46 ++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/components/ShortcutsHelper.vue b/src/components/ShortcutsHelper.vue index 2af6490d5..188ee36bf 100644 --- a/src/components/ShortcutsHelper.vue +++ b/src/components/ShortcutsHelper.vue @@ -191,6 +191,52 @@ export default defineComponent({ }, ], }, + { + label: t`POS`, + description: t`Applicable when POS is open`, + collapsed: false, + shortcuts: [ + { + shortcut: [ShortcutKey.shift, 'V'], + description: t`Toggle between grid and list view`, + }, + { + shortcut: [ShortcutKey.shift, 'S'], + description: t`Navigate to Sales Invoice`, + }, + { + shortcut: [ShortcutKey.shift, 'L'], + description: t`Set Loyalty Program`, + }, + { + shortcut: [ShortcutKey.shift, 'C'], + description: t`Set Coupon Code`, + }, + { + shortcut: [ShortcutKey.shift, 'P'], + description: t`Set Price List`, + }, + { + shortcut: [ShortcutKey.pmod, ShortcutKey.shift, 'H'], + description: t`Open Saved or Submitted Invoice list.`, + }, + { + shortcut: [ShortcutKey.pmod, ShortcutKey.shift, 'S'], + description: t`Save Invoice.`, + }, + { + shortcut: [ShortcutKey.pmod, ShortcutKey.shift, 'P'], + description: t`Set Payment.`, + }, + { + shortcut: [ShortcutKey.pmod, ShortcutKey.shift, ShortcutKey.delete], + description: [ + t`If any modal is open, your entry will be canceled.`, + t`If no modals are open, the selected items will be removed.`, + ].join(' '), + }, + ], + }, ]; }, }); From fcaa2927f38bbeba95794f238185de505d54a9da Mon Sep 17 00:00:00 2001 From: AbleKSaju <126228406+AbleKSaju@users.noreply.github.com> Date: Wed, 4 Dec 2024 15:20:08 +0530 Subject: [PATCH 5/8] feat: keyboard-friendly functionalities in POS --- src/components/Controls/AutoComplete.vue | 18 +++++++++++ src/components/Controls/Base.vue | 19 ++++++++++- src/components/Controls/Link.vue | 8 +++++ src/pages/POS/CouponCodeModal.vue | 21 ++++++++----- src/pages/POS/LoyaltyProgramModal.vue | 40 ++++++++++++------------ src/pages/POS/POS.vue | 23 ++++++++------ src/pages/POS/PriceListModal.vue | 28 ++++++++++------- 7 files changed, 106 insertions(+), 51 deletions(-) diff --git a/src/components/Controls/AutoComplete.vue b/src/components/Controls/AutoComplete.vue index a478fe2ce..948bf38e9 100644 --- a/src/components/Controls/AutoComplete.vue +++ b/src/components/Controls/AutoComplete.vue @@ -107,6 +107,7 @@ export default { return { showQuickView: false, linkValue: '', + focInp: false, isLoading: false, suggestions: [], highlightedIndex: -1, @@ -187,6 +188,15 @@ export default { const route = getFormRoute(this.linkSchemaName, name); await routeTo(route); }, + async focusInputTag() { + this.focInp = true; + if (this.linkValue) { + return; + } + + await this.$nextTick(); + this.$refs.input.focus(); + }, setLinkValue(value) { this.linkValue = value; }, @@ -282,6 +292,14 @@ export default { return; } + if (!e.target.value || this.focInp) { + e.target.value = null; + this.focInp = false; + this.toggleDropdown(false); + + return; + } + this.toggleDropdown(true); this.updateSuggestions(e.target.value); }, diff --git a/src/components/Controls/Base.vue b/src/components/Controls/Base.vue index 461a40141..db08b00b0 100644 --- a/src/components/Controls/Base.vue +++ b/src/components/Controls/Base.vue @@ -21,6 +21,7 @@ @blur="onBlur" @focus="(e) => !isReadOnly && $emit('focus', e)" @input="(e) => !isReadOnly && $emit('input', e)" + @keydown.enter="setLoyaltyPoints" /> @@ -49,6 +50,7 @@ export default defineComponent({ border: { type: Boolean, default: false }, size: { type: String, default: 'large' }, placeholder: String, + focusInput: Boolean, showLabel: { type: Boolean, default: false }, containerStyles: { type: Object, default: () => ({}) }, textRight: { @@ -64,6 +66,15 @@ export default defineComponent({ default: null, }, }, + async created() { + if (this.focusInput) { + await this.$nextTick(); + (this.$refs.input as HTMLInputElement).focus(); + if (this.value == 0) { + this.triggerChange(''); + } + } + }, emits: ['focus', 'input', 'change'], computed: { doc(): Doc | undefined { @@ -191,6 +202,12 @@ export default defineComponent({ }, }, methods: { + setLoyaltyPoints() { + const inputElement = this.$refs.input as HTMLInputElement; + if (inputElement && inputElement?.value) { + this.$emit('change', inputElement.value); + } + }, onBlur(e: FocusEvent) { const target = e.target; if (!(target instanceof HTMLInputElement)) { @@ -227,7 +244,7 @@ export default defineComponent({ triggerChange(value: unknown): void { value = this.parse(value); - if (value === '') { + if (value === '' || value == 0) { value = null; } diff --git a/src/components/Controls/Link.vue b/src/components/Controls/Link.vue index fe48501ce..f5889d431 100644 --- a/src/components/Controls/Link.vue +++ b/src/components/Controls/Link.vue @@ -26,6 +26,14 @@ export default { this.setLinkValue(); } }, + props: { + focusInput: Boolean, + }, + async created() { + if (this.focusInput) { + this.focusInputTag(); + } + }, methods: { async setLinkValue(newValue, isInput) { if (isInput) { diff --git a/src/pages/POS/CouponCodeModal.vue b/src/pages/POS/CouponCodeModal.vue index d7785d278..527eeae59 100644 --- a/src/pages/POS/CouponCodeModal.vue +++ b/src/pages/POS/CouponCodeModal.vue @@ -63,6 +63,7 @@ :show-label="true" :border="true" :value="couponCode" + :focus-input="true" :df="coupons.fieldMap.coupons" @change="updateCouponCode" /> @@ -161,15 +162,18 @@ export default defineComponent({ }, }, methods: { - updateCouponCode(value: string) { - (this.validationError = false), (this.couponCode = value); - }, - async setCouponCode() { + async updateCouponCode(value: string | Event) { try { - if (!this.couponCode) { - throw new Error(t`Must be select a coupon code`); + if (!value) { + return; + } + this.validationError = false; + + if ((value as Event).type === 'keydown') { + value = ((value as Event).target as HTMLInputElement).value; } + this.couponCode = value as string; const appliedCouponCodes = this.fyo.doc.getNewDoc( ModelNameEnum.AppliedCouponCodes ); @@ -183,8 +187,6 @@ export default defineComponent({ await this.sinvDoc.append('coupons', { coupons: this.couponCode }); this.$emit('applyPricingRule'); - this.$emit('toggleModal', 'CouponCode'); - this.couponCode = ''; this.validationError = false; } catch (error) { @@ -195,6 +197,9 @@ export default defineComponent({ message: t`${error as string}`, }); } + }, + setCouponCode() { + this.$emit('toggleModal', 'CouponCode'); }, async removeAppliedCoupon(coupon: AppliedCouponCodes) { this.sinvDoc?.items?.map((item: InvoiceItem) => { diff --git a/src/pages/POS/LoyaltyProgramModal.vue b/src/pages/POS/LoyaltyProgramModal.vue index a913168fd..a2934fe11 100644 --- a/src/pages/POS/LoyaltyProgramModal.vue +++ b/src/pages/POS/LoyaltyProgramModal.vue @@ -20,13 +20,15 @@
{{ loyaltyPoints }}
- @@ -52,7 +54,7 @@