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
179 lines
8.3 KiB
Vue
179 lines
8.3 KiB
Vue
<script setup>
|
|
import { ref, onMounted, computed } from 'vue';
|
|
import { usePageTitle } from '../../composables/Core/usePageTitle';
|
|
import { executeRequest } from '../../utils/executeRequest.js';
|
|
|
|
usePageTitle('Budget & Finance');
|
|
|
|
const entries = ref([]);
|
|
const summary = ref(null);
|
|
const fiscalYears = ref([]);
|
|
const loading = ref(false);
|
|
const year = ref(new Date().getFullYear());
|
|
const categoryFilter = ref('');
|
|
const showModal = ref(false);
|
|
const editingItem = ref(null);
|
|
|
|
const blankForm = () => ({
|
|
fiscal_year: year.value, category: 'INCOME', source: '',
|
|
amount: '', description: '', date: '', reference: '',
|
|
});
|
|
const form = ref(blankForm());
|
|
|
|
const loadData = async () => {
|
|
loading.value = true;
|
|
const params = new URLSearchParams({ year: year.value });
|
|
if (categoryFilter.value) params.append('category', categoryFilter.value);
|
|
const [entriesRes, sumRes, yearsRes] = await Promise.all([
|
|
executeRequest(`/budget?${params}`),
|
|
executeRequest(`/budget/summary?year=${year.value}`),
|
|
executeRequest('/budget/fiscal-years'),
|
|
]);
|
|
if (entriesRes.success) entries.value = entriesRes.data.data ?? entriesRes.data;
|
|
if (sumRes.success) summary.value = sumRes.data;
|
|
if (yearsRes.success) fiscalYears.value = yearsRes.data;
|
|
loading.value = false;
|
|
};
|
|
|
|
const openCreate = () => { editingItem.value = null; form.value = blankForm(); showModal.value = true; };
|
|
const openEdit = (item) => { editingItem.value = item; form.value = { ...item }; showModal.value = true; };
|
|
|
|
const saveEntry = async () => {
|
|
const url = editingItem.value ? '/budget/update' : '/budget/create';
|
|
const payload = editingItem.value ? { target: editingItem.value.hashkey, ...form.value } : form.value;
|
|
const res = await executeRequest(url, 'POST', payload);
|
|
if (res.success) { showModal.value = false; loadData(); }
|
|
};
|
|
|
|
const deleteEntry = async (item) => {
|
|
if (!confirm('Delete this entry?')) return;
|
|
await executeRequest('/budget/delete', 'POST', { target: item.hashkey });
|
|
loadData();
|
|
};
|
|
|
|
const incomeTotal = computed(() => summary.value?.income ?? 0);
|
|
const expenseTotal = computed(() => summary.value?.expense ?? 0);
|
|
const balance = computed(() => summary.value?.balance ?? 0);
|
|
|
|
onMounted(loadData);
|
|
</script>
|
|
|
|
<template>
|
|
<div class="p-4 max-w-6xl mx-auto">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h1 class="text-2xl font-bold">Budget & Finance</h1>
|
|
<button @click="openCreate" class="btn-primary">+ Add Entry</button>
|
|
</div>
|
|
|
|
<!-- Summary -->
|
|
<div class="grid grid-cols-3 gap-3 mb-5">
|
|
<div class="bg-green-50 rounded-lg shadow p-4 text-center">
|
|
<div class="text-xs text-gray-500 mb-1">Total Income</div>
|
|
<div class="text-2xl font-bold text-green-600">₱{{ Number(incomeTotal).toLocaleString() }}</div>
|
|
</div>
|
|
<div class="bg-red-50 rounded-lg shadow p-4 text-center">
|
|
<div class="text-xs text-gray-500 mb-1">Total Expense</div>
|
|
<div class="text-2xl font-bold text-red-600">₱{{ Number(expenseTotal).toLocaleString() }}</div>
|
|
</div>
|
|
<div :class="`rounded-lg shadow p-4 text-center ${balance >= 0 ? 'bg-blue-50' : 'bg-orange-50'}`">
|
|
<div class="text-xs text-gray-500 mb-1">Net Balance</div>
|
|
<div :class="`text-2xl font-bold ${balance >= 0 ? 'text-blue-600' : 'text-orange-600'}`">
|
|
{{ balance >= 0 ? '' : '-' }}₱{{ Math.abs(Number(balance)).toLocaleString() }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-2 mb-4">
|
|
<select v-model="year" @change="loadData" class="input w-28">
|
|
<option v-for="y in fiscalYears" :key="y" :value="y">{{ y }}</option>
|
|
<option :value="new Date().getFullYear()">{{ new Date().getFullYear() }}</option>
|
|
</select>
|
|
<select v-model="categoryFilter" @change="loadData" class="input w-32">
|
|
<option value="">All</option>
|
|
<option value="INCOME">Income</option>
|
|
<option value="EXPENSE">Expense</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div v-if="loading" class="text-center py-8 text-gray-400">Loading...</div>
|
|
<div v-else>
|
|
<table class="w-full text-sm">
|
|
<thead class="bg-gray-100">
|
|
<tr>
|
|
<th class="text-left p-2">Date</th>
|
|
<th class="text-left p-2">Category</th>
|
|
<th class="text-left p-2">Source</th>
|
|
<th class="text-left p-2">Description</th>
|
|
<th class="text-right p-2">Amount</th>
|
|
<th class="text-left p-2">Reference</th>
|
|
<th class="p-2"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="e in entries" :key="e.id" class="border-b hover:bg-gray-50">
|
|
<td class="p-2 text-xs">{{ e.date }}</td>
|
|
<td class="p-2">
|
|
<span :class="`px-2 py-0.5 rounded text-xs font-medium ${e.category === 'INCOME' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`">
|
|
{{ e.category }}
|
|
</span>
|
|
</td>
|
|
<td class="p-2">{{ e.source }}</td>
|
|
<td class="p-2 text-gray-600">{{ e.description }}</td>
|
|
<td class="p-2 text-right font-medium">₱{{ Number(e.amount).toLocaleString() }}</td>
|
|
<td class="p-2 text-xs text-gray-400">{{ e.reference || '—' }}</td>
|
|
<td class="p-2 flex gap-1">
|
|
<button @click="openEdit(e)" class="btn-sm">Edit</button>
|
|
<button @click="deleteEntry(e)" class="btn-sm text-red-500">Del</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<p v-if="!entries.length" class="text-center text-gray-400 py-8">No entries for this period.</p>
|
|
</div>
|
|
|
|
<!-- Modal -->
|
|
<div v-if="showModal" class="modal-overlay" @click.self="showModal = false">
|
|
<div class="modal-box">
|
|
<h2 class="text-lg font-bold mb-4">{{ editingItem ? 'Edit Entry' : 'New Budget Entry' }}</h2>
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label class="label">Fiscal Year *</label>
|
|
<input v-model="form.fiscal_year" type="number" class="input" />
|
|
</div>
|
|
<div>
|
|
<label class="label">Category *</label>
|
|
<select v-model="form.category" class="input">
|
|
<option value="INCOME">Income</option>
|
|
<option value="EXPENSE">Expense</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="label">Source *</label>
|
|
<input v-model="form.source" class="input" placeholder="e.g. IRA, Barangay Tax" />
|
|
</div>
|
|
<div>
|
|
<label class="label">Amount (₱) *</label>
|
|
<input v-model="form.amount" type="number" step="0.01" class="input" />
|
|
</div>
|
|
<div>
|
|
<label class="label">Date *</label>
|
|
<input v-model="form.date" type="date" class="input" />
|
|
</div>
|
|
<div>
|
|
<label class="label">Reference / OR No.</label>
|
|
<input v-model="form.reference" class="input" />
|
|
</div>
|
|
<div class="col-span-2">
|
|
<label class="label">Description</label>
|
|
<input v-model="form.description" class="input" />
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-end gap-2 mt-4">
|
|
<button @click="showModal = false" class="btn-secondary">Cancel</button>
|
|
<button @click="saveEntry" class="btn-primary">Save</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|