initial: bootstrap from BukidBountyApp base
This commit is contained in:
439
resources/js/stores/pos.js
Normal file
439
resources/js/stores/pos.js
Normal file
@@ -0,0 +1,439 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user