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
This commit is contained in:
336
resources/js/Pages/Barangay/Reports.vue
Normal file
336
resources/js/Pages/Barangay/Reports.vue
Normal file
@@ -0,0 +1,336 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user