import { defineStore } from 'pinia' import axios from 'axios' import { db } from '../db' import { useNetworkStore } from './network' export const usePosStore = defineStore('pos', { state: () => ({ activeSession: null, cart: [], products: [], categories: [], loading: false, error: null, receivedAmount: 0, paymentMethod: 'cash', paymentField: '', todayStats: { count: 0, total: 0, store_name: null, store_photo: null }, customerSuggestions: [], cachedCustomers: JSON.parse(localStorage.getItem('pos_cached_customers') || '[]'), posSessions: [], posSessionsCount: 0, posSessionsPage: 1, isOfflineMode: false, lastSync: localStorage.getItem('pos_last_sync') || null, }), getters: { totalAmount: (state) => { return state.cart.reduce((sum, item) => sum + (item.total_price || 0), 0) }, changeAmount: (state) => { return Math.max(0, state.receivedAmount - state.totalAmount) }, itemsCount: (state) => { return state.cart.reduce((count, item) => count + (item.quantity || 0), 0) } }, actions: { async fetchProducts(accessKey = null, storeHash = null) { this.loading = true try { // Try Local DB first if offline if (this.isOfflineMode) { const localProducts = await db.products.toArray() if (localProducts.length > 0) { this.products = localProducts this.loading = false return } } const params = {} if (accessKey) params.access_key = accessKey if (storeHash) params.store_hash = storeHash if (this.activeSession) params.session_hash = this.activeSession.hashkey const response = await axios.post('/Market/Products/List', params) this.products = response.data || [] // Save to local DB for next time if (this.products.length > 0) { await db.products.clear() await db.products.bulkPut(this.products) this.lastSync = new Date().toISOString() localStorage.setItem('pos_last_sync', this.lastSync) } // Extract unique categories const cats = new Set(this.products.map(p => p.category).filter(Boolean)) this.categories = Array.from(cats) } catch (error) { console.error('Failed to fetch products:', error) // Fallback to local DB on network error const localProducts = await db.products.toArray() if (localProducts.length > 0) { this.products = localProducts } else if (this.products.length === 0) { // Only surface the error if nothing is already showing this.error = 'Failed to load products' } } finally { this.loading = false } }, async startNewSession(storeHash, customerName = '', accessKey = null) { if (this.loading) return this.loading = true try { const networkStore = useNetworkStore() const payload = { customer_name: customerName } if (!networkStore.isOnline) { const savedRaw = localStorage.getItem('pos_cart_session') const saved = savedRaw ? JSON.parse(savedRaw) : null if (saved?.offline && saved.cart?.length > 0) { this.activeSession = { hashkey: saved.sessionHashkey, offline: true, transactions: [] } this.cart = saved.cart } else { this.activeSession = { hashkey: 'offline-' + Date.now(), customer_name: customerName, offline: true, transactions: [] } this.cart = [] } return } if (accessKey) payload.access_key = accessKey if (storeHash) payload.store_hash = storeHash const response = await axios.post('/api/pos/start', payload) if (response.data && response.data.success) { await this.loadSession(response.data.data.hashkey) } } catch (error) { console.error('Failed to start session:', error) const serverMessage = error?.response?.data?.message this.error = serverMessage || 'Failed to start session' } finally { this.loading = false } }, async loadSession(hashkey = null, accessKey = null) { this.loading = true try { const params = {} if (hashkey) params.target = hashkey if (accessKey) params.access_key = accessKey const response = await axios.post('/api/pos/session', params) if (response.data && response.data.success) { this.activeSession = response.data.data this.cart = this.activeSession.transactions || [] if (this.cart.length === 0) { this._restoreCartFromStorage(this.activeSession.hashkey) } } else { this.error = 'Session not found or invalid' } } catch (error) { console.error('Failed to load session:', error) this.error = 'Failed to load session' } finally { this.loading = false } }, async addToCart(productHash, quantity = 1, customPrice = null) { if (!this.activeSession && !this.isOfflineMode) return // Offline Fallback if (this.isOfflineMode || !navigator.onLine) { const product = this.products.find(p => p.hashkey === productHash); if (!product) return; const price = customPrice !== null ? customPrice : (product.price || 0); const existingIndex = this.cart.findIndex(item => item.product?.hashkey === productHash); if (existingIndex !== -1) { if (quantity <= 0) { this.cart.splice(existingIndex, 1); } else { const item = this.cart[existingIndex]; item.quantity = quantity; item.price_at_sale = price; item.total_price = item.quantity * item.price_at_sale; } } else if (quantity > 0) { this.cart.push({ id: 'local-' + Date.now(), product: product, quantity: quantity, price_at_sale: price, total_price: price * quantity }); } this._saveCartToStorage() return; } try { const payload = { session_hash: this.activeSession.hashkey, product_hash: productHash, quantity: quantity } if (customPrice !== null) payload.price = customPrice const response = await axios.post('/api/pos/add-item', payload) if (response.data && response.data.success) { // Update session state directly from the response this.activeSession = response.data.data this.cart = this.activeSession.transactions || [] this._saveCartToStorage() } } catch (error) { console.error('Failed to add item:', error) this.error = 'Failed to add item to cart' } }, async removeFromCart(transactionId) { if (!this.activeSession) return if (this.isOfflineMode || !navigator.onLine) { this.cart = this.cart.filter(item => item.id !== transactionId); return; } try { const response = await axios.post('/api/pos/remove-item', { session_hash: this.activeSession.hashkey, transaction_id: transactionId }) if (response.data && response.data.success) { // Update session state directly from the response this.activeSession = response.data.data this.cart = this.activeSession.transactions || [] } } catch (error) { console.error('Failed to remove item:', error) } }, async completeTransaction(customerName = '') { if (!this.activeSession || this.loading) return this.loading = true try { const response = await axios.post('/api/pos/complete', { session_hash: this.activeSession.hashkey, received_amount: this.receivedAmount, payment_method: this.paymentMethod, payment_field: this.paymentField, customer_name: customerName }) if (response.data && response.data.success) { this.activeSession = response.data.data this._clearCartStorage() // Add customer to cache if provided if (customerName) { this.updateCustomerCache([{ name: customerName, hashkey: 'new-' + Date.now() // Temporary hash until next fetch }]); } return true } } catch (error) { console.error('Failed to complete transaction:', error) this.error = 'Payment failed' } finally { this.loading = false } return false }, _saveCartToStorage() { if (!this.activeSession) return try { localStorage.setItem('pos_cart_session', JSON.stringify({ sessionHashkey: this.activeSession.hashkey, offline: this.activeSession.offline || false, cart: this.cart })) } catch {} }, _restoreCartFromStorage(sessionHashkey) { try { const raw = localStorage.getItem('pos_cart_session') if (!raw) return false const saved = JSON.parse(raw) if (saved.sessionHashkey === sessionHashkey && saved.cart?.length > 0) { this.cart = saved.cart return true } } catch {} return false }, _clearCartStorage() { localStorage.removeItem('pos_cart_session') }, resetSession() { this.activeSession = null this.cart = [] this.receivedAmount = 0 this.paymentField = '' this._clearCartStorage() // Keep the access key as it defines the terminal session }, async fetchTodayStats(storeHash = null) { try { const response = await axios.post('/api/pos/stats', { store_hash: storeHash }) if (response.data && response.data.success) { this.todayStats = response.data.data } } catch (error) { console.error('Failed to fetch today stats:', error) } }, async fetchCustomerSuggestions(query = '', storeHash = null) { // 1. Show cached results immediately if available if (this.cachedCustomers.length > 0) { const q = query.toLowerCase(); this.customerSuggestions = query ? this.cachedCustomers.filter(c => c.name.toLowerCase().includes(q)).slice(0, 10) : this.cachedCustomers.slice(-3).reverse(); // Show last 3 most recent if no query } // 2. Background fetch to get current/complete list try { const response = await axios.post('/api/pos/get-customers', { query: query, store_hash: storeHash }) if (response.data && response.data.success) { const newSuggestions = response.data.data; this.customerSuggestions = newSuggestions; // Update persistent cache this.updateCustomerCache(newSuggestions); } } catch (error) { console.error('Failed to fetch customer suggestions:', error) } }, updateCustomerCache(newCustomers) { const merged = [...this.cachedCustomers]; newCustomers.forEach(newCust => { const index = merged.findIndex(c => c.name.toLowerCase() === newCust.name.toLowerCase()); if (index === -1) { merged.push(newCust); } else { // Update existing if new one has real hashkey if (!merged[index].hashkey.startsWith('new-') || newCust.hashkey) { merged[index] = newCust; } } }); // Limit cache to 200 most recent this.cachedCustomers = merged.slice(-200); localStorage.setItem('pos_cached_customers', JSON.stringify(this.cachedCustomers)); }, async fetchPosSessions(storeHash, page = 1) { this.loading = true try { const response = await axios.post('/api/pos/sessions/list', { store_hash: storeHash, page: page, per_page: 10 }) if (response.data && response.data.success) { const { sessions, total_count, page: currentPage } = response.data.data if (page === 1) { this.posSessions = sessions } else { // Avoid duplicates const existingHashes = new Set(this.posSessions.map(s => s.hashkey)) const newSessions = sessions.filter(s => !existingHashes.has(s.hashkey)) this.posSessions = [...this.posSessions, ...newSessions] } this.posSessionsCount = total_count this.posSessionsPage = currentPage } } catch (error) { console.error('Failed to fetch POS sessions:', error) this.error = 'Failed to load POS history' } finally { this.loading = false } }, syncFromSSE(data) { // 1. Sync Today's Stats if (data.pos_stats) { // Merge into existing stats to preserve store_name/photo if SSE payload is partial this.todayStats = { ...this.todayStats, ...data.pos_stats }; } // 2. Sync Customers (Merge into search suggestions and persistent cache) if (data.customers && data.customers.length > 0) { this.updateCustomerCache(data.customers); // If currently showing recent customers, refresh the view if (!this.loading && this.customerSuggestions.length > 0) { // Minor delay to avoid flickering if user is typing const recent = this.cachedCustomers.slice(-3).reverse(); this.customerSuggestions = recent; } } // 3. Sync Inventory Deltas (Stock/Price changes + newly assigned products) if (data.inventory_deltas && data.inventory_deltas.length > 0) { let hasNewProduct = false; data.inventory_deltas.forEach(delta => { const index = this.products.findIndex(p => p.hashkey === delta.hashkey || p.id === delta.id); if (index !== -1) { this.products[index] = { ...this.products[index], ...delta }; } else if (this.products.length > 0) { // Product not in list — newly assigned to this store hasNewProduct = true; } }); // Trigger a full reload so store-specific price/available are fetched correctly. // Requires an active session so the backend resolves the correct store. if (hasNewProduct && this.activeSession) { this.fetchProducts(); } } } } })