3131 </div >
3232
3333 <!-- Offers List -->
34- <div v-else class =" space-y-3 max-h-[500px] overflow-y-auto pr-2" >
35- <div
36- v-for =" offer in eligibleOffers"
37- :key =" offer.name"
38- :class =" [
39- 'relative rounded-xl p-4 transition-all duration-200 border-2',
40- appliedOffer?.code === offer.name
41- ? 'bg-green-50 border-green-500 shadow-md'
42- : 'bg-gradient-to-r from-green-50 to-emerald-50 border-green-200 hover:border-green-400 hover:shadow-lg cursor-pointer'
43- ]"
44- >
34+ <div v-else >
35+ <div class =" space-y-3 max-h-[500px] overflow-y-auto pr-2" >
36+ <div
37+ v-for =" offer in eligibleOffers"
38+ :key =" offer.name"
39+ :class =" [
40+ 'relative rounded-xl p-4 transition-all duration-200 border-2',
41+ isOfferApplied(offer)
42+ ? 'bg-green-50 border-green-500 shadow-md'
43+ : 'bg-gradient-to-r from-green-50 to-emerald-50 border-green-200 hover:border-green-400 hover:shadow-lg cursor-pointer'
44+ ]"
45+ >
4546 <!-- Applied Badge -->
4647 <div
47- v-if =" appliedOffer?.code === offer.name "
48+ v-if =" isOfferApplied( offer) "
4849 class =" absolute top-2 right-2 bg-green-600 text-white text-[10px] font-bold px-2 py-1 rounded-full flex items-center space-x-1"
4950 >
5051 <svg class =" w-3 h-3" fill =" currentColor" viewBox =" 0 0 20 20" >
131132 </div >
132133 </div >
133134
134- <!-- Progress Bar for Min Amount -->
135- <div v-if =" offer.min_amt && subtotal < offer.min_amt" class =" mt-3" >
135+ <!-- Progress Bar for Min Amount (only shown if not eligible) -->
136+ <div v-if =" offer.min_amt && offersStore.cartSnapshot. subtotal < offer.min_amt" class =" mt-3" >
136137 <div class =" flex items-center justify-between text-xs mb-1" >
137138 <span class =" text-gray-600" >Subtotal (before tax)</span >
138139 <span class =" text-gray-900 font-semibold" >
139- {{ formatCurrency(subtotal) }} / {{ formatCurrency(offer.min_amt) }}
140+ {{ formatCurrency(offersStore.cartSnapshot. subtotal) }} / {{ formatCurrency(offer.min_amt) }}
140141 </span >
141142 </div >
142143 <div class =" w-full bg-gray-200 rounded-full h-2" >
143144 <div
144145 class =" bg-green-600 h-2 rounded-full transition-all"
145- :style =" { width: `${Math.min((subtotal / offer.min_amt) * 100, 100)}%` }"
146+ :style =" { width: `${Math.min((offersStore.cartSnapshot. subtotal / offer.min_amt) * 100, 100)}%` }"
146147 ></div >
147148 </div >
148149 <p class =" text-xs text-orange-600 mt-1 font-medium" >
149- Add {{ formatCurrency(offer.min_amt - subtotal ) }} more to unlock
150+ Add {{ formatCurrency(offersStore.getUnlockAmount(offer) ) }} more to unlock
150151 </p >
151152 </div >
152153
157158 :disabled =" applyingOffer"
158159 :class =" [
159160 'mt-3 w-full py-2 px-4 rounded-lg font-semibold text-sm transition-all',
160- appliedOffer?.code === offer.name
161+ isOfferApplied( offer)
161162 ? 'bg-red-600 hover:bg-red-700 text-white'
162163 : 'bg-green-600 hover:bg-green-700 text-white shadow-md hover:shadow-lg',
163164 applyingOffer ? 'opacity-70 cursor-not-allowed' : ''
164165 ]"
165166 >
166- {{ appliedOffer?.code === offer.name ? 'Remove Offer' : 'Apply Offer' }}
167+ {{ isOfferApplied( offer) ? 'Remove Offer' : 'Apply Offer' }}
167168 </button >
168169 </div >
169170 </div >
170171 </div >
172+ </div >
171173 </template >
172174 <template #actions >
173175 <div class =" flex justify-end w-full" >
181183
182184<script setup>
183185import { ref , watch , computed } from ' vue'
184- import { Dialog , Button , createResource , toast } from ' frappe-ui'
186+ import { Dialog , Button } from ' frappe-ui'
185187import { formatCurrency as formatCurrencyUtil } from ' @/utils/currency'
188+ import { usePOSOffersStore } from ' @/stores/posOffers'
189+
190+ // Use Pinia stores
191+ const offersStore = usePOSOffersStore ()
186192
187193const props = defineProps ({
188194 modelValue: Boolean ,
@@ -199,64 +205,31 @@ const props = defineProps({
199205 type: String ,
200206 default: ' USD'
201207 },
202- appliedOffer : {
203- type: Object ,
204- default: null
208+ appliedOffers : {
209+ type: Array ,
210+ default : () => []
205211 }
206212})
207213
208214const emit = defineEmits ([' update:modelValue' , ' apply-offer' , ' remove-offer' ])
209215
210216const show = ref (props .modelValue )
211- const allOffers = ref ([])
212- const loading = ref (false )
213217const applyingOffer = ref (false )
214-
215- // Resource to load offers
216- const offersResource = createResource ({
217- url: ' pos_next.api.offers.get_offers' ,
218- makeParams () {
219- return {
220- pos_profile: props .posProfile
221- }
222- },
223- auto: false ,
224- onSuccess (data ) {
225- allOffers .value = data? .message || data || []
226- loading .value = false
227- },
228- onError (error ) {
229- console .error (' Error loading offers:' , error)
230- loading .value = false
231- toast ({
232- title: ' Error' ,
233- text: ' Failed to load offers' ,
234- icon: ' x' ,
235- iconClasses: ' text-red-600'
236- })
237- }
218+ const appliedOfferCodes = computed (() => {
219+ return new Set ((props .appliedOffers || []).map (entry => entry? .code ).filter (Boolean ))
238220})
239221
240- // Computed eligible offers - only show offers the user is eligible for
241- const eligibleOffers = computed (() => {
242- if (! allOffers .value ) return []
222+ // Use ALL eligible offers from store (includes both auto and manual offers)
223+ const eligibleOffers = computed (() => offersStore .allEligibleOffersSorted )
243224
244- // Filter to only show eligible, non-coupon offers
245- return allOffers .value
246- .filter (offer => ! offer .coupon_based && checkOfferEligibility (offer))
247- .sort ((a , b ) => {
248- // Sort by discount value (higher discounts first)
249- const aValue = a .discount_percentage || a .discount_amount || 0
250- const bValue = b .discount_percentage || b .discount_amount || 0
251- return bValue - aValue
252- })
225+ // Loading state - check if offers are being loaded
226+ const loading = computed (() => {
227+ return ! offersStore .hasFetched && eligibleOffers .value .length === 0
253228})
254229
255230watch (() => props .modelValue , (val ) => {
256231 show .value = val
257- if (val) {
258- loadOffers ()
259- }
232+ // No need to load offers - they're already in the store
260233})
261234
262235watch (show, (val ) => {
@@ -267,46 +240,16 @@ watch(show, (val) => {
267240 }
268241})
269242
270- function checkOfferEligibility (offer ) {
271- // Check minimum amount (on subtotal before tax)
272- if (offer .min_amt && props .subtotal < offer .min_amt ) {
273- return false
274- }
275- // Check maximum amount (on subtotal before tax)
276- if (offer .max_amt && props .subtotal > offer .max_amt ) {
277- return false
278- }
279- // Check minimum quantity
280- if (offer .min_qty ) {
281- const totalQty = props .items .reduce ((sum , item ) => sum + item .quantity , 0 )
282- if (totalQty < offer .min_qty ) {
283- return false
284- }
285- }
286- return true
287- }
288-
289- async function loadOffers () {
290- if (! props .posProfile ) return
291- loading .value = true
292- try {
293- await offersResource .reload ()
294- } catch (error) {
295- console .error (' Error loading offers:' , error)
296- loading .value = false
297- }
298- }
299-
300243async function handleApplyOffer (offer ) {
301244 if (applyingOffer .value ) {
302245 return
303246 }
304247
305248 // Toggle offer - if already applied, remove it
306- if (props . appliedOffer ? . code === offer . name ) {
249+ if (isOfferApplied ( offer) ) {
307250 applyingOffer .value = true
308251 try {
309- emit (' remove-offer' )
252+ emit (' remove-offer' , offer )
310253 // Close dialog after successful removal
311254 await new Promise (resolve => setTimeout (resolve, 500 ))
312255 show .value = false
@@ -335,6 +278,10 @@ function resetApplyingState() {
335278// Expose for parent component
336279defineExpose ({ resetApplyingState })
337280
281+ function isOfferApplied (offer ) {
282+ return appliedOfferCodes .value .has (offer? .name )
283+ }
284+
338285function formatCurrency (amount ) {
339286 return formatCurrencyUtil (parseFloat (amount || 0 ), props .currency )
340287}
0 commit comments