Files
BarangaySystem/resources/js/Pages/Barangay/Reports.vue
Jonathan Sykes bee4a1f5ab
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
feat: complete all plan phases — reports, cleanup market fragments
- Add Reports page with population/household/document/blotter/budget/project views
- Add ReportsController with year-filtered queries for all report types
- Add /reports module to config/modules.php
- Register /barangay/reports in VueRouteMap and web.php
- Remove unused market Home fragments (HomeCoopMember, HomeStoreOwner, etc.)
- Remove leftover market Components/Market/ directory
- Add Reports card to Home.vue admin quick access
2026-06-07 03:15:04 +08:00

337 lines
19 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import { ref, onMounted, computed } from 'vue';
import { usePageTitle } from '../../composables/Core/usePageTitle';
import { executeRequest } from '../../utils/executeRequest.js';
usePageTitle('Barangay Reports');
const loading = ref(false);
const activeReport = ref('population');
const selectedYear = ref(new Date().getFullYear());
const data = ref({});
const years = computed(() => {
const current = new Date().getFullYear();
return Array.from({ length: 5 }, (_, i) => current - i);
});
const reports = [
{ key: 'population', label: 'Population Summary', icon: '👥' },
{ key: 'households', label: 'Household Statistics', icon: '🏠' },
{ key: 'documents', label: 'Document Requests', icon: '📋' },
{ key: 'blotters', label: 'Blotter/Incident Log', icon: '⚖️' },
{ key: 'budget', label: 'Budget & Finance', icon: '💰' },
{ key: 'projects', label: 'Projects Summary', icon: '🏗️' },
];
const loadReport = async () => {
loading.value = true;
try {
const res = await executeRequest('/reports/generate', 'POST', {
type: activeReport.value,
year: selectedYear.value,
});
if (res.success) data.value = res.data;
} catch (e) { /* silent */ }
loading.value = false;
};
const printReport = () => window.print();
// Population computed
const genderBreakdown = computed(() => data.value?.gender_breakdown ?? {});
const civilStatusBreakdown = computed(() => data.value?.civil_status_breakdown ?? {});
const ageBreakdown = computed(() => data.value?.age_breakdown ?? {});
const purokBreakdown = computed(() => data.value?.purok_breakdown ?? {});
// Document computed
const docByType = computed(() => data.value?.by_type ?? []);
const docByStatus = computed(() => data.value?.by_status ?? {});
// Blotter computed
const blotterByStatus = computed(() => data.value?.by_status ?? {});
const blotterByType = computed(() => data.value?.by_incident_type ?? {});
// Budget computed
const budgetSummary = computed(() => data.value?.summary ?? {});
const budgetBySource = computed(() => data.value?.by_source ?? []);
// Projects computed
const projectsByStatus = computed(() => data.value?.by_status ?? {});
const projectsByType = computed(() => data.value?.by_type ?? []);
onMounted(loadReport);
</script>
<template>
<div class="p-4 max-w-5xl mx-auto">
<!-- Header -->
<div class="flex items-center justify-between mb-4 print:hidden">
<h1 class="text-2xl font-bold">Barangay Reports</h1>
<button @click="printReport" class="btn-secondary text-sm">🖨 Print</button>
</div>
<!-- Report selector -->
<div class="flex flex-wrap gap-2 mb-4 print:hidden">
<button
v-for="r in reports" :key="r.key"
@click="activeReport = r.key; loadReport()"
:class="`px-3 py-1.5 rounded-lg text-sm font-medium border transition ${activeReport === r.key ? 'bg-blue-500 text-white border-blue-500' : 'bg-white text-gray-600 border-gray-200 hover:border-blue-300'}`">
{{ r.icon }} {{ r.label }}
</button>
</div>
<!-- Year selector -->
<div class="flex items-center gap-2 mb-5 print:hidden">
<label class="text-sm text-gray-600">Year:</label>
<select v-model="selectedYear" @change="loadReport" class="input w-28 py-1 text-sm">
<option v-for="y in years" :key="y" :value="y">{{ y }}</option>
</select>
</div>
<!-- Print title -->
<div class="hidden print:block mb-6">
<h1 class="text-xl font-bold">{{ reports.find(r => r.key === activeReport)?.label }} {{ selectedYear }}</h1>
<p class="text-sm text-gray-500">Generated: {{ new Date().toLocaleDateString('en-PH') }}</p>
</div>
<div v-if="loading" class="text-center py-12 text-gray-400">Generating report...</div>
<div v-else>
<!-- Population Report -->
<div v-if="activeReport === 'population'" class="space-y-4">
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3">
<div class="bg-blue-50 rounded-lg p-4 text-center">
<div class="text-3xl font-bold text-blue-600">{{ data.total_residents ?? 0 }}</div>
<div class="text-sm text-gray-500 mt-1">Total Residents</div>
</div>
<div class="bg-green-50 rounded-lg p-4 text-center">
<div class="text-3xl font-bold text-green-600">{{ data.active_residents ?? 0 }}</div>
<div class="text-sm text-gray-500 mt-1">Active Residents</div>
</div>
<div class="bg-purple-50 rounded-lg p-4 text-center">
<div class="text-3xl font-bold text-purple-600">{{ data.registered_voters ?? 0 }}</div>
<div class="text-sm text-gray-500 mt-1">Registered Voters</div>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="bg-white rounded-lg shadow p-4">
<h3 class="font-semibold text-gray-700 mb-3">By Gender</h3>
<div v-for="(count, gender) in genderBreakdown" :key="gender" class="flex justify-between py-1 border-b text-sm">
<span class="text-gray-600 capitalize">{{ gender.toLowerCase() }}</span>
<span class="font-semibold">{{ count }}</span>
</div>
</div>
<div class="bg-white rounded-lg shadow p-4">
<h3 class="font-semibold text-gray-700 mb-3">By Civil Status</h3>
<div v-for="(count, status) in civilStatusBreakdown" :key="status" class="flex justify-between py-1 border-b text-sm">
<span class="text-gray-600 capitalize">{{ status.toLowerCase() }}</span>
<span class="font-semibold">{{ count }}</span>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow p-4">
<h3 class="font-semibold text-gray-700 mb-3">By Purok</h3>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
<div v-for="(count, purok) in purokBreakdown" :key="purok" class="bg-gray-50 rounded p-2 text-sm flex justify-between">
<span>Purok {{ purok }}</span>
<span class="font-semibold">{{ count }}</span>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow p-4">
<h3 class="font-semibold text-gray-700 mb-3">Age Groups</h3>
<div class="space-y-1">
<div v-for="(count, group) in ageBreakdown" :key="group" class="flex items-center gap-2 text-sm">
<span class="text-gray-600 w-28">{{ group }}</span>
<div class="flex-1 bg-gray-100 rounded-full h-4 overflow-hidden">
<div class="bg-blue-400 h-4 rounded-full" :style="`width: ${data.total_residents ? Math.round(count/data.total_residents*100) : 0}%`"></div>
</div>
<span class="font-semibold w-8 text-right">{{ count }}</span>
</div>
</div>
</div>
</div>
<!-- Households Report -->
<div v-else-if="activeReport === 'households'" class="space-y-4">
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3">
<div class="bg-blue-50 rounded-lg p-4 text-center">
<div class="text-3xl font-bold text-blue-600">{{ data.total_households ?? 0 }}</div>
<div class="text-sm text-gray-500 mt-1">Total Households</div>
</div>
<div class="bg-yellow-50 rounded-lg p-4 text-center">
<div class="text-3xl font-bold text-yellow-600">{{ data.avg_members ?? 0 }}</div>
<div class="text-sm text-gray-500 mt-1">Avg. Members</div>
</div>
<div class="bg-green-50 rounded-lg p-4 text-center">
<div class="text-3xl font-bold text-green-600">{{ data.with_electricity ?? 0 }}</div>
<div class="text-sm text-gray-500 mt-1">With Electricity</div>
</div>
</div>
<div class="bg-white rounded-lg shadow p-4">
<h3 class="font-semibold text-gray-700 mb-3">By Ownership Type</h3>
<div v-for="(count, type) in (data.by_ownership ?? {})" :key="type" class="flex justify-between py-1 border-b text-sm">
<span class="capitalize">{{ type.toLowerCase() }}</span>
<span class="font-semibold">{{ count }}</span>
</div>
</div>
</div>
<!-- Document Requests Report -->
<div v-else-if="activeReport === 'documents'" class="space-y-4">
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3">
<div class="bg-blue-50 rounded-lg p-4 text-center">
<div class="text-3xl font-bold text-blue-600">{{ data.total ?? 0 }}</div>
<div class="text-sm text-gray-500 mt-1">Total Requests</div>
</div>
<div class="bg-green-50 rounded-lg p-4 text-center">
<div class="text-3xl font-bold text-green-600">{{ Number(data.total_revenue ?? 0).toLocaleString() }}</div>
<div class="text-sm text-gray-500 mt-1">Total Revenue</div>
</div>
<div class="bg-teal-50 rounded-lg p-4 text-center">
<div class="text-3xl font-bold text-teal-600">{{ data.claimed ?? 0 }}</div>
<div class="text-sm text-gray-500 mt-1">Claimed</div>
</div>
</div>
<div class="bg-white rounded-lg shadow p-4">
<h3 class="font-semibold text-gray-700 mb-3">By Document Type</h3>
<table class="w-full text-sm">
<thead class="text-left text-gray-500 border-b">
<tr><th class="pb-2">Type</th><th class="pb-2">Count</th><th class="pb-2">Revenue</th></tr>
</thead>
<tbody>
<tr v-for="t in docByType" :key="t.type" class="border-b">
<td class="py-1.5">{{ t.name }}</td>
<td class="py-1.5">{{ t.count }}</td>
<td class="py-1.5">{{ Number(t.revenue ?? 0).toLocaleString() }}</td>
</tr>
</tbody>
</table>
</div>
<div class="bg-white rounded-lg shadow p-4">
<h3 class="font-semibold text-gray-700 mb-3">By Status</h3>
<div v-for="(count, status) in docByStatus" :key="status" class="flex justify-between py-1 border-b text-sm">
<span class="capitalize">{{ status.toLowerCase() }}</span>
<span class="font-semibold">{{ count }}</span>
</div>
</div>
</div>
<!-- Blotter Report -->
<div v-else-if="activeReport === 'blotters'" class="space-y-4">
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3">
<div class="bg-blue-50 rounded-lg p-4 text-center">
<div class="text-3xl font-bold text-blue-600">{{ data.total ?? 0 }}</div>
<div class="text-sm text-gray-500 mt-1">Total Cases</div>
</div>
<div class="bg-green-50 rounded-lg p-4 text-center">
<div class="text-3xl font-bold text-green-600">{{ data.resolved ?? 0 }}</div>
<div class="text-sm text-gray-500 mt-1">Resolved</div>
</div>
<div class="bg-orange-50 rounded-lg p-4 text-center">
<div class="text-3xl font-bold text-orange-600">{{ data.pending ?? 0 }}</div>
<div class="text-sm text-gray-500 mt-1">Pending</div>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="bg-white rounded-lg shadow p-4">
<h3 class="font-semibold text-gray-700 mb-3">By Status</h3>
<div v-for="(count, status) in blotterByStatus" :key="status" class="flex justify-between py-1 border-b text-sm">
<span class="capitalize">{{ status.toLowerCase().replace('_', ' ') }}</span>
<span class="font-semibold">{{ count }}</span>
</div>
</div>
<div class="bg-white rounded-lg shadow p-4">
<h3 class="font-semibold text-gray-700 mb-3">By Incident Type</h3>
<div v-for="(count, type) in blotterByType" :key="type" class="flex justify-between py-1 border-b text-sm">
<span class="capitalize">{{ type.toLowerCase() }}</span>
<span class="font-semibold">{{ count }}</span>
</div>
</div>
</div>
</div>
<!-- Budget Report -->
<div v-else-if="activeReport === 'budget'" class="space-y-4">
<div class="grid grid-cols-3 gap-3">
<div class="bg-green-50 rounded-lg p-4 text-center">
<div class="text-2xl font-bold text-green-600">{{ Number(budgetSummary.total_income ?? 0).toLocaleString() }}</div>
<div class="text-sm text-gray-500 mt-1">Total Income</div>
</div>
<div class="bg-red-50 rounded-lg p-4 text-center">
<div class="text-2xl font-bold text-red-600">{{ Number(budgetSummary.total_expense ?? 0).toLocaleString() }}</div>
<div class="text-sm text-gray-500 mt-1">Total Expense</div>
</div>
<div class="bg-blue-50 rounded-lg p-4 text-center">
<div class="text-2xl font-bold text-blue-600">{{ Number(budgetSummary.balance ?? 0).toLocaleString() }}</div>
<div class="text-sm text-gray-500 mt-1">Balance</div>
</div>
</div>
<div class="bg-white rounded-lg shadow p-4">
<h3 class="font-semibold text-gray-700 mb-3">By Source / Category</h3>
<table class="w-full text-sm">
<thead class="text-left text-gray-500 border-b">
<tr><th class="pb-2">Source</th><th class="pb-2">Type</th><th class="pb-2">Amount</th></tr>
</thead>
<tbody>
<tr v-for="row in budgetBySource" :key="`${row.source}-${row.category}`" class="border-b">
<td class="py-1.5">{{ row.source }}</td>
<td class="py-1.5">
<span :class="`text-xs px-1.5 py-0.5 rounded ${row.category === 'INCOME' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`">
{{ row.category }}
</span>
</td>
<td class="py-1.5 font-medium">{{ Number(row.amount).toLocaleString() }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Projects Report -->
<div v-else-if="activeReport === 'projects'" class="space-y-4">
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div class="bg-blue-50 rounded-lg p-4 text-center">
<div class="text-3xl font-bold text-blue-600">{{ data.total ?? 0 }}</div>
<div class="text-sm text-gray-500 mt-1">Total Projects</div>
</div>
<div class="bg-yellow-50 rounded-lg p-4 text-center">
<div class="text-3xl font-bold text-yellow-600">{{ data.ongoing ?? 0 }}</div>
<div class="text-sm text-gray-500 mt-1">Ongoing</div>
</div>
<div class="bg-green-50 rounded-lg p-4 text-center">
<div class="text-3xl font-bold text-green-600">{{ data.completed ?? 0 }}</div>
<div class="text-sm text-gray-500 mt-1">Completed</div>
</div>
<div class="bg-purple-50 rounded-lg p-4 text-center">
<div class="text-2xl font-bold text-purple-600">{{ Number(data.total_budget ?? 0).toLocaleString() }}</div>
<div class="text-sm text-gray-500 mt-1">Total Budget</div>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="bg-white rounded-lg shadow p-4">
<h3 class="font-semibold text-gray-700 mb-3">By Status</h3>
<div v-for="(count, status) in projectsByStatus" :key="status" class="flex justify-between py-1 border-b text-sm">
<span class="capitalize">{{ status.toLowerCase() }}</span>
<span class="font-semibold">{{ count }}</span>
</div>
</div>
<div class="bg-white rounded-lg shadow p-4">
<h3 class="font-semibold text-gray-700 mb-3">By Type</h3>
<div v-for="row in projectsByType" :key="row.type" class="flex justify-between py-1 border-b text-sm">
<span class="capitalize">{{ row.type.toLowerCase() }}</span>
<span class="font-semibold">{{ row.count }}</span>
</div>
</div>
</div>
</div>
<p v-if="!loading && Object.keys(data).length === 0" class="text-center text-gray-400 py-8">No data available for this report.</p>
</div>
</div>
</template>