feat: implement barangay system phases 2-14
Some checks failed
tests / PHP 8.2 (swoole-5.1.6) (push) Has been cancelled
tests / PHP 8.3 (swoole-5.1.6) (push) Has been cancelled
tests / PHP 8.4 (swoole-6.0) (push) Has been cancelled

Complete adaptation from BukidBountyApp to Philippine barangay governance:

- Barangay models: Resident, Household, HouseholdMember, Blotter, BlotterHearing,
  DocumentRequest, RequestPayment, RequestType, BarangayProject, BarangayBudget
- Controllers: ResidentController, HouseholdController, BlotterController,
  BlotterHearingController, DocumentRequestController, RequestTypeController,
  ProjectController, BudgetController, QRPHController, AdminConsoleController,
  UserController, FileController, ChapterController, LoginController
- Vue pages: Home, ManageResidents, ResidentProfile, ManageHouseholds, ManageBlotters,
  BlotterDetail, RequestDocument, ManageDocumentRequests, DocumentRequestDetail,
  ManageRequestTypes, ManageProjects, BudgetLedger, AdminConsole
- Barangay roles: PunongBarangay, Kagawad, Secretary, Treasurer, SK, Tanod, BHW, Staff, Resident
- UserPermissions matrix rewritten with barangay-specific permission mappings
- VueRouteMap replaced with barangay SPA routes
- UserActions enum references corrected across all controllers
- Removed all market/cooperative/POS/subscription code and models
This commit is contained in:
Jonathan Sykes
2026-06-07 03:09:09 +08:00
parent 19fec0933b
commit fbb7e3ff37
234 changed files with 5582 additions and 39457 deletions

View File

@@ -1,58 +0,0 @@
import { defineStore } from 'pinia';
import axios from 'axios';
export const useGlobalTransactionStore = defineStore('globalTransaction', {
state: () => ({
transactions: [],
isLoading: false,
error: null,
lastFetched: null,
}),
getters: {
getTransactionsByProduct: (state) => (productHash) => {
return state.transactions.filter(tx => tx.product_hash === productHash);
},
getTransactionsByStore: (state) => (storeHash) => {
return state.transactions.filter(tx => tx.store_hash === storeHash);
}
},
actions: {
async fetchTransactions(filters = {}) {
try {
this.isLoading = true;
this.error = null;
const response = await axios.post('/admin/transactions/list', filters);
if (Array.isArray(response.data)) {
this.transactions = response.data;
this.lastFetched = Date.now();
}
return this.transactions;
} catch (err) {
this.error = 'Failed to load transactions';
console.error('Error fetching transactions:', err);
throw err;
} finally {
this.isLoading = false;
}
},
async precache() {
// Avoid refetching if data is fresh (less than 5 minutes old)
if (this.transactions.length > 0 && this.lastFetched && (Date.now() - this.lastFetched < 300000)) {
return;
}
await this.fetchTransactions();
},
clearCache() {
this.transactions = [];
this.lastFetched = null;
}
}
});

View File

@@ -1,439 +0,0 @@
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();
}
}
}
}
})

View File

@@ -1,218 +0,0 @@
import { defineStore } from 'pinia'
import axios from 'axios'
import { useOPFS } from '../composables/useOPFS'
const opfs = useOPFS()
const CACHE_KEY_PRODUCTS = 'cached_products.json'
export const useProductStore = defineStore('product', {
state: () => ({
products: [],
currentProduct: null,
categories: [],
subcategories: [],
loading: false,
error: null,
detailsCache: {} // Cache for full product details keyed by hashkey
}),
actions: {
async fetchProducts(force = false) {
if (!force && this.products.length > 0) {
return;
}
this.loading = true
this.error = null
// Attempt to load from cache first for immediate UI update
try {
const cachedProducts = await opfs.loadJSON(CACHE_KEY_PRODUCTS)
if (cachedProducts && Array.isArray(cachedProducts)) {
this.products = cachedProducts
}
} catch (e) {
console.warn('Failed to load products from cache:', e)
}
try {
const response = await axios.post('/Market/Products/List')
const newProducts = response.data || []
// Merge or replace
this.products = newProducts
// Save to cache
await opfs.saveJSON(CACHE_KEY_PRODUCTS, this.products)
} catch (error) {
console.error('Failed to fetch products:', error)
this.error = error.message
} finally {
this.loading = false
}
},
syncFromSSE(data) {
// 1. Full Market List
if (data.products_market && data.products_market.length > 0) {
console.log('[ProductStore] Syncing full market list from SSE');
this.products = data.products_market;
// Optionally cache to OPFS
opfs.saveJSON(CACHE_KEY_PRODUCTS, this.products).catch(() => {});
}
// 2. Deltas (Inventory/Price/Details)
if (data.inventory_deltas && data.inventory_deltas.length > 0) {
data.inventory_deltas.forEach(delta => {
const index = this.products.findIndex(p => p.hashkey === delta.hashkey);
if (index !== -1) {
this.products[index] = {
...this.products[index],
...delta
};
} else {
// New product added
this.products.unshift(delta);
}
// Also update currentProduct if it matches
if (this.currentProduct && (this.currentProduct.hashkey === delta.hashkey)) {
this.currentProduct = {
...this.currentProduct,
...delta
};
}
});
}
// 3. New detailed product data from SSE (if added)
if (data.product_details && typeof data.product_details === 'object') {
Object.entries(data.product_details).forEach(([hash, details]) => {
this.detailsCache[hash] = details;
if (this.currentProduct && this.currentProduct.hashkey === hash) {
this.currentProduct = { ...this.currentProduct, ...details };
}
});
}
},
async fetchProductById(hashkey, storeHash = null) {
if (this.detailsCache[hashkey]) {
this.currentProduct = this.detailsCache[hashkey];
// return early but still fetch in background if needed (optional)
// for now just return it
return;
}
this.loading = true
this.error = null
try {
const response = await axios.post('/View/Product/Details/data', {
target: hashkey,
data: { store_hash: storeHash }
})
if (response.data && response.data.success && response.data.data) {
this.currentProduct = response.data.data
this.detailsCache[hashkey] = this.currentProduct
}
} catch (error) {
console.error('Failed to fetch product:', error)
this.error = error.message
} finally {
this.loading = false
}
},
async fetchCategories() {
try {
const response = await axios.get('/api/categories')
this.categories = response.data || []
} catch (error) {
console.error('Failed to fetch categories:', error)
}
},
async fetchSubcategories(categoryId) {
try {
const response = await axios.get(`/api/subcategories?category_id=${categoryId}`)
this.subcategories = response.data || []
} catch (error) {
console.error('Failed to fetch subcategories:', error)
}
},
async createProduct(data) {
this.loading = true
this.error = null
try {
const response = await axios.post('/api/products', data)
if (response.data && response.data.success) {
this.products.push(response.data.product)
}
return response.data
} catch (error) {
console.error('Failed to create product:', error)
this.error = error.message
throw error
} finally {
this.loading = false
}
},
async updateProduct(id, data) {
this.loading = true
this.error = null
try {
const response = await axios.put(`/api/products/${id}`, data)
if (response.data && response.data.success) {
const index = this.products.findIndex(p => p.id === id)
if (index !== -1) {
this.products[index] = response.data.product
}
}
return response.data
} catch (error) {
console.error('Failed to update product:', error)
this.error = error.message
throw error
} finally {
this.loading = false
}
},
async deleteProduct(id) {
this.loading = true
this.error = null
try {
const response = await axios.delete(`/api/products/${id}`)
if (response.data && response.data.success) {
this.products = this.products.filter(p => p.id !== id)
}
return response.data
} catch (error) {
console.error('Failed to delete product:', error)
this.error = error.message
throw error
} finally {
this.loading = false
}
},
async updateProductStatus(productId, status) {
try {
const response = await axios.post('/api/products/status', {
product_id: productId,
status: status
})
return response.data
} catch (error) {
console.error('Failed to update product status:', error)
throw error
}
},
resetCurrentProduct() {
this.currentProduct = null
}
}
})

View File

@@ -1,160 +0,0 @@
import { defineStore } from 'pinia'
import axios from 'axios'
import { useOPFS } from '../composables/useOPFS'
const opfs = useOPFS()
const CACHE_KEY_STORES = 'cached_stores.json'
export const useStoreStore = defineStore('store', {
state: () => ({
stores: [],
currentStore: null,
storeProducts: [],
loading: false,
error: null
}),
actions: {
async fetchStores() {
this.loading = true
this.error = null
// Load from cache first
try {
const cachedStores = await opfs.loadJSON(CACHE_KEY_STORES)
if (cachedStores && Array.isArray(cachedStores)) {
this.stores = cachedStores
}
} catch (e) {
console.warn('Failed to load stores from cache:', e)
}
try {
const response = await axios.get('/api/stores')
this.stores = response.data || []
// Save to cache
await opfs.saveJSON(CACHE_KEY_STORES, this.stores)
} catch (error) {
console.error('Failed to fetch stores:', error)
this.error = error.message
} finally {
this.loading = false
}
},
async fetchStoreByHash(hash) {
this.loading = true
this.error = null
try {
const response = await axios.get(`/api/stores/${hash}`)
if (response.data && response.data.success) {
this.currentStore = response.data.store
}
} catch (error) {
console.error('Failed to fetch store:', error)
this.error = error.message
} finally {
this.loading = false
}
},
async createStore(data) {
this.loading = true
this.error = null
try {
const response = await axios.post('/api/stores', data)
if (response.data && response.data.success) {
this.stores.push(response.data.store)
}
return response.data
} catch (error) {
console.error('Failed to create store:', error)
this.error = error.message
throw error
} finally {
this.loading = false
}
},
async updateStore(id, data) {
this.loading = true
this.error = null
try {
const response = await axios.put(`/api/stores/${id}`, data)
if (response.data && response.data.success) {
const index = this.stores.findIndex(s => s.id === id)
if (index !== -1) {
this.stores[index] = response.data.store
}
}
return response.data
} catch (error) {
console.error('Failed to update store:', error)
this.error = error.message
throw error
} finally {
this.loading = false
}
},
async deleteStore(id) {
this.loading = true
this.error = null
try {
const response = await axios.delete(`/api/stores/${id}`)
if (response.data && response.data.success) {
this.stores = this.stores.filter(s => s.id !== id)
}
return response.data
} catch (error) {
console.error('Failed to delete store:', error)
this.error = error.message
throw error
} finally {
this.loading = false
}
},
async fetchStoreProducts(storeId) {
try {
const response = await axios.get(`/api/stores/${storeId}/products`)
this.storeProducts = response.data || []
return this.storeProducts
} catch (error) {
console.error('Failed to fetch store products:', error)
throw error
}
},
async addProductToStore(storeId, productId, quantity, price) {
try {
const response = await axios.post(`/api/stores/${storeId}/products`, {
product_id: productId,
quantity: quantity,
price: price
})
return response.data
} catch (error) {
console.error('Failed to add product to store:', error)
throw error
}
},
async removeProductFromStore(storeId, productId) {
try {
const response = await axios.delete(`/api/stores/${storeId}/products/${productId}`)
if (response.data && response.data.success) {
this.storeProducts = this.storeProducts.filter(p => p.product_id !== productId)
}
return response.data
} catch (error) {
console.error('Failed to remove product from store:', error)
throw error
}
},
resetCurrentStore() {
this.currentStore = null
}
}
})