Skip to content

Commit 3ee6eb4

Browse files
author
Ahmed Osama
committed
perf: optimize cart operations and item rendering performance
- Remove blocking toast notifications on item add for instant UI response - Add v-memo directive to item cards to prevent unnecessary re-renders - Cache tax rate calculations to avoid repeated loops - Optimize offer snapshot sync with memoization and nextTick - Improve offer reapplication with hash-based change detection - Increase debounce delay to 800ms for better batching
1 parent f6544ef commit 3ee6eb4

4 files changed

Lines changed: 88 additions & 43 deletions

File tree

POS/src/components/sale/ItemsSelector.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@
180180
<div
181181
v-for="item in paginatedItems"
182182
:key="item.item_code"
183+
v-memo="[item.item_code, item.actual_qty, item.stock_qty, item.rate, item.price_list_rate]"
183184
@click="handleItemClick(item)"
184185
class="relative bg-white border border-gray-200 rounded-lg p-1.5 sm:p-2.5 cursor-pointer hover:border-blue-400 hover:shadow-lg transition-all touch-manipulation active:scale-95 active:shadow-xl"
185186
>
@@ -310,6 +311,7 @@
310311
<tr
311312
v-for="item in paginatedItems"
312313
:key="item.item_code"
314+
v-memo="[item.item_code, item.actual_qty, item.stock_qty, item.rate, item.price_list_rate]"
313315
@click="handleItemClick(item)"
314316
class="cursor-pointer hover:bg-blue-50 transition-colors touch-manipulation active:bg-blue-100"
315317
>

POS/src/composables/useInvoice.js

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,36 @@ export function useInvoice() {
271271
})
272272
}
273273

274+
// Performance: Cache tax calculation to avoid repeated loops
275+
let cachedTaxRate = 0
276+
let taxRulesCacheKey = ''
277+
278+
function calculateTotalTaxRate() {
279+
// Create cache key from tax rules
280+
const currentKey = JSON.stringify(taxRules.value)
281+
282+
// Return cached value if tax rules haven't changed
283+
if (currentKey === taxRulesCacheKey && cachedTaxRate !== 0) {
284+
return cachedTaxRate
285+
}
286+
287+
// Calculate total tax rate
288+
let totalRate = 0
289+
if (taxRules.value && taxRules.value.length > 0) {
290+
for (const taxRule of taxRules.value) {
291+
if (taxRule.charge_type === "On Net Total" || taxRule.charge_type === "On Previous Row Total") {
292+
totalRate += (taxRule.rate || 0)
293+
}
294+
}
295+
}
296+
297+
// Cache the result
298+
cachedTaxRate = totalRate
299+
taxRulesCacheKey = currentKey
300+
301+
return totalRate
302+
}
303+
274304
function recalculateItem(item) {
275305
// Step 1: Calculate base amount (rate * quantity)
276306
const baseAmount = item.quantity * item.rate
@@ -290,15 +320,9 @@ export function useInvoice() {
290320
// Step 3: Calculate net amount (after discount, before tax)
291321
const netAmount = baseAmount - discountAmount
292322

293-
// Step 4: Calculate tax on net amount
294-
let taxAmount = 0
295-
if (taxRules.value && taxRules.value.length > 0) {
296-
for (const taxRule of taxRules.value) {
297-
if (taxRule.charge_type === "On Net Total" || taxRule.charge_type === "On Previous Row Total") {
298-
taxAmount += (netAmount * (taxRule.rate || 0)) / 100
299-
}
300-
}
301-
}
323+
// Step 4: Calculate tax on net amount (optimized with cached tax rate)
324+
const totalTaxRate = calculateTotalTaxRate()
325+
const taxAmount = (netAmount * totalTaxRate) / 100
302326
item.tax_amount = taxAmount
303327

304328
// Step 5: Calculate final item amount (net amount + tax)

POS/src/pages/POSSale.vue

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,9 @@ const logoutAfterClose = ref(false)
644644
// Debounce timer for offer reapplication
645645
const offerReapplyTimer = ref(null)
646646
647+
// Performance: Cache previous cart state to avoid unnecessary reapplications
648+
let previousCartHash = ''
649+
647650
// Promotion dialog
648651
const showPromotionManagement = ref(false)
649652
@@ -742,22 +745,37 @@ watch(() => shiftStore.hasOpenShift, value => {
742745
}
743746
})
744747
745-
// Watch for cart changes to re-apply offers (optimized - uses computed key instead of deep watch + debounced)
746-
// Tracks: item_code, quantity, rate, discount_percentage, discount_amount to catch all pricing changes
748+
// Watch for cart changes to re-apply offers (optimized - only watch length and defer expensive calculations)
749+
// Performance: Only recalculate hash if cart length changed, then check if content actually changed
747750
watch(
748-
() => cartStore.invoiceItems.map(i =>
749-
`${i.item_code}-${i.quantity}-${i.rate}-${i.discount_percentage || 0}-${i.discount_amount || 0}`
750-
).join(','),
751+
() => cartStore.invoiceItems.length,
751752
() => {
753+
// Only proceed if there are applied offers
754+
if (cartStore.appliedOffers.length === 0) {
755+
return
756+
}
757+
758+
// Calculate hash only when length changes
759+
const currentHash = cartStore.invoiceItems.map(i =>
760+
`${i.item_code}-${i.quantity}-${i.rate}-${i.discount_percentage || 0}-${i.discount_amount || 0}`
761+
).join(',')
762+
763+
// Skip if cart content hasn't actually changed
764+
if (currentHash === previousCartHash) {
765+
return
766+
}
767+
768+
previousCartHash = currentHash
769+
752770
// Clear existing timer to prevent multiple API calls
753771
if (offerReapplyTimer.value) {
754772
clearTimeout(offerReapplyTimer.value)
755773
}
756774
757-
// Set new timer - reapply offers after 500ms of no changes
775+
// Set new timer - reapply offers after 800ms of no changes (increased for better performance)
758776
offerReapplyTimer.value = setTimeout(async () => {
759777
await cartStore.reapplyOffer(shiftStore.currentProfile)
760-
}, 500)
778+
}, 800)
761779
}
762780
)
763781

POS/src/stores/posCart.js

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { defineStore } from 'pinia'
2-
import { ref, computed, watch } from 'vue'
2+
import { ref, computed, watch, nextTick } from 'vue'
33
import { useInvoice } from '@/composables/useInvoice'
44
import { toast } from 'frappe-ui'
55
import { parseError } from '@/utils/errorHandler'
@@ -74,23 +74,8 @@ export const usePOSCartStore = defineStore('posCart', () => {
7474
}
7575
}
7676

77+
// Add item to cart - no toast notification for performance
7778
addItemToInvoice(item, qty)
78-
79-
if (autoAdd) {
80-
toast.create({
81-
title: "✓ Auto-Added to Cart",
82-
text: `${item.item_name} added to cart`,
83-
icon: "check",
84-
iconClasses: "text-blue-600",
85-
})
86-
} else {
87-
toast.create({
88-
title: "Item Added",
89-
text: `${item.item_name} added to cart`,
90-
icon: "check",
91-
iconClasses: "text-green-600",
92-
})
93-
}
9479
}
9580

9681
function clearCart() {
@@ -564,28 +549,44 @@ export const usePOSCartStore = defineStore('posCart', () => {
564549
}
565550
}
566551

552+
// Performance: Cache previous item codes hash to avoid unnecessary recalculations
553+
let previousItemCodesHash = ''
554+
let cachedItemCodes = []
555+
let cachedItemGroups = []
556+
let cachedBrands = []
557+
567558
function syncOfferSnapshot() {
568559
// Only sync if values are initialized
569560
if (subtotal.value !== undefined && invoiceItems.value) {
570-
// Extract item codes, groups, and brands from cart
571-
const itemCodes = invoiceItems.value.map(item => item.item_code)
572-
const itemGroups = [...new Set(invoiceItems.value.map(item => item.item_group).filter(Boolean))]
573-
const brands = [...new Set(invoiceItems.value.map(item => item.brand).filter(Boolean))]
561+
// Create hash for item codes to detect actual changes
562+
const currentHash = invoiceItems.value.map(item => item.item_code).join(',')
563+
564+
// Only recalculate expensive operations if items actually changed
565+
if (currentHash !== previousItemCodesHash) {
566+
cachedItemCodes = invoiceItems.value.map(item => item.item_code)
567+
cachedItemGroups = [...new Set(invoiceItems.value.map(item => item.item_group).filter(Boolean))]
568+
cachedBrands = [...new Set(invoiceItems.value.map(item => item.brand).filter(Boolean))]
569+
previousItemCodesHash = currentHash
570+
}
574571

575572
offersStore.updateCartSnapshot({
576573
subtotal: subtotal.value,
577574
itemCount: invoiceItems.value.length,
578-
itemCodes,
579-
itemGroups,
580-
brands,
575+
itemCodes: cachedItemCodes,
576+
itemGroups: cachedItemGroups,
577+
brands: cachedBrands,
581578
})
582579
}
583580
}
584581

585582
// Watch for cart changes to update offer snapshot (min/max thresholds etc.)
586-
watch([subtotal, () => invoiceItems.value.length, () => invoiceItems.value.map(i => i.item_code).join(',')], () => {
587-
syncOfferSnapshot()
588-
}, { immediate: true })
583+
// Optimized: Only watch length and subtotal, calculate hash inside the watcher
584+
watch([subtotal, () => invoiceItems.value.length], () => {
585+
// Defer to next tick to prevent blocking UI
586+
nextTick(() => {
587+
syncOfferSnapshot()
588+
})
589+
}, { immediate: true, flush: 'post' })
589590

590591
return {
591592
// State

0 commit comments

Comments
 (0)