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,53 +1,35 @@
import { ref, computed, onMounted } from 'vue';
import { ref, computed } from 'vue';
import axios from 'axios';
import { UserTypes } from '../../utils/UserTypes.js';
import { UserTypes, ADMIN_ROLES, STAFF_ROLES } from '../../utils/UserTypes.js';
import { useUserStore } from '../../stores/user.js';
// Global reactive state to persist throughout the SPA session
const globalRole = ref(sessionStorage.getItem('user_acct_type') || UserTypes.PUBLIC);
const isFetching = ref(false);
/**
* Fetches the user account type from the server.
* Ensures only one request is made per session.
*/
async function fetchRole() {
if (isFetching.value) return;
// If we already have a specialized role in sessionStorage, don't fetch again
const cached = sessionStorage.getItem('user_acct_type');
if (cached && cached !== UserTypes.PUBLIC) {
return;
}
if (cached && cached !== UserTypes.PUBLIC) return;
isFetching.value = true;
try {
const response = await axios.get('/get/user/acct-type');
let acctType = response.data?.acct_type;
// Handle case where acct_type might be an object (Enum serialization)
if (acctType && typeof acctType === 'object' && acctType.value) {
acctType = acctType.value;
}
if (acctType && typeof acctType === 'object' && acctType.value) acctType = acctType.value;
if (acctType) {
globalRole.value = acctType;
sessionStorage.setItem('user_acct_type', acctType);
}
} catch (error) {
// If 401, we are likely a guest
if (error.response?.status === 401) {
globalRole.value = UserTypes.PUBLIC;
sessionStorage.setItem('user_acct_type', UserTypes.PUBLIC);
} else {
console.warn('Failed to fetch user acct type from server:', error);
}
} finally {
isFetching.value = false;
}
}
// Initial fetch attempt if we don't have a definitive role
if (!sessionStorage.getItem('user_acct_type') || sessionStorage.getItem('user_acct_type') === UserTypes.PUBLIC) {
fetchRole();
}
@@ -58,90 +40,79 @@ export function resetRole() {
sessionStorage.removeItem('user_acct_type');
}
/**
* Composable for managing user roles and permissions in the frontend.
* @param {Object} [user] - Optional user object for legacy support or specific overrides
*/
export function useAuth(user = null) {
const userStore = useUserStore();
// Priority: Explicitly passed user prop > user store > sessionStorage fallback
const currentUser = computed(() => {
if (user?.value ?? user) return user?.value ?? user;
const storeUser = userStore.user;
if (storeUser && Object.keys(storeUser).length > 0) return storeUser;
try {
const stored = sessionStorage.getItem('currentUser');
return stored ? JSON.parse(stored) : null;
} catch (e) {
} catch {
return null;
}
});
const role = computed(() => {
// Priority 1: User object passed directly or from store
const localRole = userStore.acctType || currentUser.value?.acct_type;
if (localRole) {
if (typeof localRole === 'object' && localRole.value) return localRole.value;
return localRole;
}
// Priority 2: Global fetched role
return globalRole.value;
});
const hasRole = (targetRole) => {
if (Array.isArray(targetRole)) {
return targetRole.some(r => role.value === r);
}
if (Array.isArray(targetRole)) return targetRole.some(r => role.value === r);
return role.value === targetRole;
};
// Role-specific helpers
const isUltimate = computed(() => role.value === UserTypes.ULTIMATE);
const isSuperOperator = computed(() => role.value === UserTypes.SUPER_OPERATOR);
const isOperator = computed(() => role.value === UserTypes.OPERATOR);
const isCoordinator = computed(() => role.value === UserTypes.COORDINATOR);
const isStoreOwner = computed(() => role.value === UserTypes.STORE_OWNER);
const isStoreManager = computed(() => role.value === UserTypes.STORE_MANAGER);
const isRider = computed(() => role.value === UserTypes.RIDER);
const isSupplier = computed(() => role.value === UserTypes.SUPPLIER);
const isSupplierOverseer = computed(() => role.value === UserTypes.SUPPLIER_OVERSEER);
const isWholesaleBuyer = computed(() => role.value === UserTypes.WHOLESALE_BUYER);
const isAudit = computed(() => role.value === UserTypes.AUDIT);
const isUser = computed(() => role.value === UserTypes.USER);
const isCoopOfficer = computed(() => role.value === UserTypes.COOP_OFFICER);
const isCoopMember = computed(() => role.value === UserTypes.COOP_MEMBER);
const isPOSTerminal = computed(() => role.value === UserTypes.POS_TERMINAL);
const isPublic = computed(() => role.value === UserTypes.PUBLIC || !userStore.isLoggedIn);
const isLoggedIn = computed(() => userStore.isLoggedIn);
// Barangay-specific role helpers
const isSuperAdmin = computed(() => role.value === UserTypes.SUPER_ADMIN);
const isPunongBarangay = computed(() => role.value === UserTypes.PUNONG_BARANGAY);
const isKagawad = computed(() => role.value === UserTypes.KAGAWAD);
const isSecretary = computed(() => role.value === UserTypes.SECRETARY);
const isTreasurer = computed(() => role.value === UserTypes.TREASURER);
const isSkChairperson = computed(() => role.value === UserTypes.SK_CHAIRPERSON);
const isSkCouncilor = computed(() => role.value === UserTypes.SK_COUNCILOR);
const isTanod = computed(() => role.value === UserTypes.TANOD);
const isBhw = computed(() => role.value === UserTypes.BHW);
const isDaycareWorker = computed(() => role.value === UserTypes.DAYCARE_WORKER);
const isStaff = computed(() => role.value === UserTypes.STAFF);
const isResident = computed(() => role.value === UserTypes.RESIDENT);
const isAudit = computed(() => role.value === UserTypes.AUDIT);
const isPublic = computed(() => role.value === UserTypes.PUBLIC || !userStore.isLoggedIn);
const isLoggedIn = computed(() => userStore.isLoggedIn);
// Group helpers
const isAdmin = computed(() => ADMIN_ROLES.includes(role.value));
const isBarangayStaff = computed(() => STAFF_ROLES.includes(role.value));
return {
user: currentUser,
role,
hasRole,
isUltimate,
isSuperOperator,
isOperator,
isCoordinator,
isStoreOwner,
isStoreManager,
isRider,
isSupplier,
isSupplierOverseer,
isWholesaleBuyer,
isSuperAdmin,
isPunongBarangay,
isKagawad,
isSecretary,
isTreasurer,
isSkChairperson,
isSkCouncilor,
isTanod,
isBhw,
isDaycareWorker,
isStaff,
isResident,
isAudit,
isUser,
isCoopOfficer,
isCoopMember,
isPOSTerminal,
isPublic,
isLoggedIn,
isAdmin,
isBarangayStaff,
UserTypes,
refreshRole: fetchRole,
// Expose the user store for direct access to user data
userStore
userStore,
};
}

View File

@@ -1,162 +0,0 @@
import { ref, onMounted, watch } from 'vue';
import { usePosStore } from '../../stores/pos';
import { useUIStore } from '../../stores/ui';
import { useOfflineStore } from '../useOfflineStore';
import { useNetworkStore } from '../../stores/network';
export function usePosSession(props) {
const posStore = usePosStore();
const uiStore = useUIStore();
const offlineStore = useOfflineStore();
const networkStore = useNetworkStore();
const storeHash = ref(null);
const showSuccessAnimation = ref(false);
const isOfflineMode = ref(false);
// Helper: Access key from URL or LocalStorage
const getStoredAccessKey = () => {
try {
return localStorage.getItem('pos_access_key');
} catch {
return null;
}
};
const initialize = async () => {
// Check for existing session in URL or Storage
const urlParams = new URLSearchParams(window.location.search);
const accessKey = props.access_key || urlParams.get('key') || getStoredAccessKey();
const hashkey = props.target;
if (accessKey) {
localStorage.setItem('pos_access_key', accessKey);
}
// 1. Load Session or treat as Store Hash
if (hashkey) {
// Try loading as session
await posStore.loadSession(hashkey, accessKey);
if (!posStore.activeSession) {
// Not a session? Treat it as a direct link to a store terminal
storeHash.value = hashkey;
posStore.error = null;
} else if (posStore.activeSession?.store?.hashkey) {
storeHash.value = posStore.activeSession.store.hashkey;
}
}
// 2. Fetch products (Always synchronized with access key/store hash)
await posStore.fetchProducts(accessKey, storeHash.value);
// 3. Fallback: If no direct session yet, try to load one by access key
if (!posStore.activeSession && accessKey) {
await posStore.loadSession(null, accessKey);
}
// 4. Synchronization: ensure products are loaded if session was found later
if (posStore.activeSession && posStore.products.length === 0) {
await posStore.fetchProducts(accessKey, storeHash.value);
}
// 5. Restore offline cart if no server session was found
if (!posStore.activeSession) {
try {
const savedRaw = localStorage.getItem('pos_cart_session')
const saved = savedRaw ? JSON.parse(savedRaw) : null
if (saved?.offline && saved.cart?.length > 0) {
posStore.activeSession = { hashkey: saved.sessionHashkey, offline: true, transactions: [] }
posStore.cart = saved.cart
posStore.isOfflineMode = true
isOfflineMode.value = true
}
} catch {}
}
// 6. Load terminal stats
await posStore.fetchTodayStats(storeHash.value);
};
const completeTransaction = async (customerName) => {
const currentStoreHash = storeHash.value || posStore.activeSession?.store?.hashkey;
// Try Online First (skip if session is a local offline-only session)
let success = false;
const isOfflineSession = posStore.activeSession?.offline === true;
if (networkStore.isOnline && !isOfflineSession) {
success = await posStore.completeTransaction(customerName);
}
if (!success) {
// Offline Fallback
const txnData = {
store_hash: currentStoreHash,
customer_name: customerName,
items: posStore.cart.map(item => ({
product_hashkey: item.product?.hashkey,
quantity: item.quantity,
price_at_sale: item.price_at_sale
})),
total: posStore.totalAmount,
received: posStore.receivedAmount,
change: posStore.changeAmount,
method: posStore.paymentMethod
};
const id = await offlineStore.storeTransactionOffline(txnData);
if (id) {
success = true;
isOfflineMode.value = true;
}
}
if (success) {
showSuccessAnimation.value = true;
// Clean up state
posStore.resetSession();
// Delayed re-initialization (gives time for animation)
setTimeout(async () => {
showSuccessAnimation.value = false;
if (networkStore.isOnline) {
await posStore.startNewSession(currentStoreHash, '', getStoredAccessKey());
await Promise.all([
posStore.fetchTodayStats(currentStoreHash),
posStore.fetchPosSessions(currentStoreHash, 1)
]);
isOfflineMode.value = false;
}
}, 2500);
return true;
}
return false;
};
const startNewSessionSilently = async () => {
const accessKey = getStoredAccessKey();
if (!storeHash.value && !accessKey) {
posStore.error = 'No store selected. Open the POS from a store page or use an access key.';
return false;
}
return await posStore.startNewSession(storeHash.value, '', accessKey);
};
// Keep storeHash in sync with active session if it changes
watch(() => posStore.activeSession, (session) => {
if (session?.store?.hashkey) {
storeHash.value = session.store.hashkey;
}
}, { immediate: true });
return {
storeHash,
showSuccessAnimation,
initialize,
completeTransaction,
startNewSessionSilently,
getStoredAccessKey,
isOfflineMode
};
}

View File

@@ -1,29 +0,0 @@
import { computed, onMounted } from 'vue';
import { useGlobalTransactionStore } from '../stores/globalTransaction';
export function useGlobalTransactions(filters = null) {
const store = useGlobalTransactionStore();
const transactions = computed(() => store.transactions);
const isLoading = computed(() => store.isLoading);
const error = computed(() => store.error);
const fetchTransactions = async (newFilters = null) => {
return await store.fetchTransactions(newFilters || filters || {});
};
const getProductTransactions = (productHash) => {
return computed(() => store.getTransactionsByProduct(productHash));
};
const precache = () => store.precache();
return {
transactions,
isLoading,
error,
fetchTransactions,
getProductTransactions,
precache
};
}

View File

@@ -1,56 +0,0 @@
import { ref } from 'vue';
import axios from 'axios';
import { useFileBlobCache } from './useFileBlobCache';
/**
* Composable for fetching and managing a list of photos for a specific entity.
*/
export function usePhotoList() {
const { getFile, preCacheFiles, blobCache } = useFileBlobCache();
const photos = ref([]);
const loading = ref(false);
const error = ref(null);
/**
* Fetch photos for a target hash and type.
*
* @param {string} targetHash
* @param {string} type - 'StoreMarket', 'ProductMarket', 'User'
*/
const fetchPhotos = async (targetHash, type = 'StoreMarket') => {
if (!targetHash) return;
loading.value = true;
error.value = null;
try {
const response = await axios.post(`/Request/Photos/${type}`, {
target: targetHash
});
if (response.data && Array.isArray(response.data)) {
photos.value = response.data;
// Pre-cache all blobs for these hashes
await preCacheFiles(photos.value);
} else {
photos.value = [];
}
} catch (err) {
console.error('Error fetching photos:', err);
error.value = 'Failed to load photos.';
photos.value = [];
} finally {
loading.value = false;
}
};
return {
photos,
loading,
error,
fetchPhotos,
blobCache,
getFile
};
}

View File

@@ -1,204 +0,0 @@
import { ref } from 'vue';
import axios from 'axios';
export function useUltimate() {
const loading = ref(false);
const stats = ref(null);
const queryResults = ref(null);
const affectedRows = ref(0);
const commandOutput = ref('');
const getStats = async () => {
loading.value = true;
try {
const response = await axios.post('/admin/ultimate/stats');
if (response.data.success) {
stats.value = response.data.data;
}
return response.data;
} catch (error) {
console.error('Failed to fetch stats:', error);
throw error;
} finally {
loading.value = false;
}
};
const runQuery = async (query) => {
loading.value = true;
try {
const response = await axios.post('/admin/ultimate/query', { query });
if (response.data.success) {
queryResults.value = response.data.data || null;
affectedRows.value = response.data.affected || 0;
}
return response.data;
} catch (error) {
console.error('Failed to run query:', error);
throw error;
} finally {
loading.value = false;
}
};
const toggleMaintenance = async (enabled) => {
loading.value = true;
try {
const response = await axios.post('/admin/ultimate/maintenance/toggle', { enabled });
if (response.data.success && stats.value) {
stats.value.maintenance_mode = response.data.maintenance_mode;
}
return response.data;
} catch (error) {
console.error('Failed to toggle maintenance:', error);
throw error;
} finally {
loading.value = false;
}
};
const sendGlobalMessage = async (message, type = 'info') => {
loading.value = true;
try {
return await axios.post('/admin/ultimate/global-message', { message, type });
} catch (error) {
console.error('Failed to send global message:', error);
throw error;
} finally {
loading.value = false;
}
};
const flushData = async (target) => {
loading.value = true;
try {
return await axios.post('/admin/ultimate/flush', { target });
} catch (error) {
console.error('Failed to flush data:', error);
throw error;
} finally {
loading.value = false;
}
};
const testNotification = async (userHash) => {
loading.value = true;
try {
return await axios.post('/admin/ultimate/test-notification', { user_hash: userHash });
} catch (error) {
console.error('Failed to test notification:', error);
throw error;
} finally {
loading.value = false;
}
};
const batchManage = async (action, ids, data = {}) => {
loading.value = true;
try {
return await axios.post('/admin/ultimate/batch', { action, ids, data });
} catch (error) {
console.error('Failed to run batch operation:', error);
throw error;
} finally {
loading.value = false;
}
};
const runCommand = async (command) => {
loading.value = true;
try {
const response = await axios.post('/admin/ultimate/command', { command });
if (response.data.success) {
commandOutput.value = response.data.output;
}
return response.data;
} catch (error) {
console.error('Failed to run command:', error);
throw error;
} finally {
loading.value = false;
}
};
const getLogs = async (type = 'database') => {
loading.value = true;
try {
const response = await axios.post('/admin/ultimate/logs', { type });
return response.data;
} catch (error) {
console.error('Failed to fetch logs:', error);
throw error;
} finally {
loading.value = false;
}
};
const downloadBackup = () => {
// Simple window location change to trigger GET download
window.location.href = '/admin/ultimate/backup/download';
};
const getBackups = async () => {
loading.value = true;
try {
const response = await axios.post('/admin/ultimate/backups/list');
return response.data;
} catch (error) {
console.error('Failed to fetch backups:', error);
throw error;
} finally {
loading.value = false;
}
};
const downloadBackupByHash = (hash) => {
window.location.href = `/admin/ultimate/backup/download/hash?hash=${hash}`;
};
const renameBackup = async (hash, name) => {
loading.value = true;
try {
return await axios.post('/admin/ultimate/backup/rename', { hash, name });
} catch (error) {
console.error('Failed to rename backup:', error);
throw error;
} finally {
loading.value = false;
}
};
const deleteBackup = async (hash) => {
loading.value = true;
try {
return await axios.post('/admin/ultimate/backup/delete', { hash });
} catch (error) {
console.error('Failed to delete backup:', error);
throw error;
} finally {
loading.value = false;
}
};
return {
loading,
stats,
queryResults,
affectedRows,
commandOutput,
getStats,
runQuery,
toggleMaintenance,
sendGlobalMessage,
flushData,
testNotification,
batchManage,
runCommand,
getLogs,
downloadBackup,
getBackups,
downloadBackupByHash,
renameBackup,
deleteBackup
};
}

View File

@@ -1,131 +0,0 @@
import { ref, onMounted } from 'vue';
import axios from 'axios';
export function useUserAdditionalDetails() {
const details = ref({
settings: {},
details: {}
});
const isLoading = ref(false);
const joinedCooperatives = ref([]);
const fetchUserDetails = async () => {
isLoading.value = true;
try {
const response = await axios.post('/UserAdditionalDetails/Get');
if (response.data.success) {
details.value = response.data.data;
}
} catch (error) {
console.error('Failed to fetch user additional details:', error);
} finally {
isLoading.value = false;
}
};
const joinCooperative = async (cooperativeHash) => {
isLoading.value = true;
try {
const response = await axios.post('/UserAdditionalDetails/UpdateCooperatives', {
cooperative_hash: cooperativeHash,
action: 'add'
});
if (response.data.success) {
// If it was successful, update the local settings to reflect it
if (!details.value.settings.cooperatives) {
details.value.settings.cooperatives = [];
}
if (!details.value.settings.cooperatives.includes(cooperativeHash)) {
details.value.settings.cooperatives.push(cooperativeHash);
}
return true;
}
} catch (error) {
console.error('Failed to join cooperative:', error);
} finally {
isLoading.value = false;
}
return false;
};
const leaveCooperative = async (cooperativeHash) => {
isLoading.value = true;
try {
const response = await axios.post('/UserAdditionalDetails/UpdateCooperatives', {
cooperative_hash: cooperativeHash,
action: 'remove'
});
if (response.data.success) {
if (details.value.settings.cooperatives) {
details.value.settings.cooperatives = details.value.settings.cooperatives.filter(h => h !== cooperativeHash);
}
return true;
}
} catch (error) {
console.error('Failed to leave cooperative:', error);
} finally {
isLoading.value = false;
}
return false;
};
const fetchJoinedCooperatives = async (userHash = null) => {
isLoading.value = true;
try {
const response = await axios.post('/UserAdditionalDetails/GetCooperatives', {
user_hash: userHash
});
if (response.data.success) {
joinedCooperatives.value = response.data.data;
return joinedCooperatives.value;
}
} catch (error) {
console.error('Failed to fetch user cooperatives:', error);
} finally {
isLoading.value = false;
}
return [];
};
const searchUsersByCooperative = async (cooperativeHash) => {
isLoading.value = true;
try {
const response = await axios.post('/UserAdditionalDetails/SearchByCooperative', {
cooperative_hash: cooperativeHash
});
if (response.data.success) {
return response.data.data;
}
} catch (error) {
console.error('Failed to search users by cooperative:', error);
} finally {
isLoading.value = false;
}
return [];
};
const getJoinedCooperativeHashes = () => {
return details.value.settings.cooperatives || [];
};
const hasJoinedCooperative = (cooperativeHash) => {
return getJoinedCooperativeHashes().includes(cooperativeHash);
};
onMounted(() => {
fetchUserDetails();
});
return {
details,
isLoading,
joinedCooperatives,
fetchUserDetails,
joinCooperative,
leaveCooperative,
fetchJoinedCooperatives,
searchUsersByCooperative,
getJoinedCooperativeHashes,
hasJoinedCooperative
};
}