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
194 lines
9.3 KiB
Vue
194 lines
9.3 KiB
Vue
<script setup>
|
|
import { ref, onMounted } from 'vue';
|
|
import { usePageTitle } from '../../composables/Core/usePageTitle';
|
|
import { executeRequest } from '../../utils/executeRequest.js';
|
|
|
|
usePageTitle('Barangay Projects');
|
|
|
|
const projects = ref([]);
|
|
const summary = ref(null);
|
|
const loading = ref(false);
|
|
const statusFilter = ref('');
|
|
const typeFilter = ref('');
|
|
const showModal = ref(false);
|
|
const editingItem = ref(null);
|
|
|
|
const blankForm = () => ({
|
|
project_name: '', description: '', type: 'INFRASTRUCTURE',
|
|
budget: '', fund_source: 'GENERAL_FUND',
|
|
start_date: '', end_date: '',
|
|
implementing_office: '', contractor: '', location: '', beneficiaries_count: '',
|
|
});
|
|
const form = ref(blankForm());
|
|
|
|
const projectTypes = ['INFRASTRUCTURE', 'LIVELIHOOD', 'HEALTH', 'EDUCATION', 'ENVIRONMENT', 'OTHERS'];
|
|
const fundSources = ['GENERAL_FUND', 'SK', 'PROVINCE', 'NATIONAL', 'OTHERS'];
|
|
const statusOptions = ['PLANNED', 'ONGOING', 'COMPLETED', 'SUSPENDED', 'CANCELLED'];
|
|
|
|
const statusColors = {
|
|
PLANNED: 'bg-blue-100 text-blue-700', ONGOING: 'bg-yellow-100 text-yellow-700',
|
|
COMPLETED: 'bg-green-100 text-green-700', SUSPENDED: 'bg-orange-100 text-orange-700',
|
|
CANCELLED: 'bg-red-100 text-red-700',
|
|
};
|
|
|
|
const loadData = async () => {
|
|
loading.value = true;
|
|
const params = new URLSearchParams();
|
|
if (statusFilter.value) params.append('status', statusFilter.value);
|
|
if (typeFilter.value) params.append('type', typeFilter.value);
|
|
const [projRes, sumRes] = await Promise.all([
|
|
executeRequest(`/projects?${params}`),
|
|
executeRequest('/projects/summary'),
|
|
]);
|
|
if (projRes.success) projects.value = projRes.data.data ?? projRes.data;
|
|
if (sumRes.success) summary.value = sumRes.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 saveProject = async () => {
|
|
const url = editingItem.value ? '/projects/update' : '/projects/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 updateStatus = async (item, status) => {
|
|
await executeRequest('/projects/status', 'POST', { target: item.hashkey, status });
|
|
loadData();
|
|
};
|
|
|
|
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">Barangay Projects</h1>
|
|
<button @click="openCreate" class="btn-primary">+ Add Project</button>
|
|
</div>
|
|
|
|
<!-- Summary Cards -->
|
|
<div v-if="summary" class="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-5">
|
|
<div class="bg-white rounded-lg shadow p-3 text-center">
|
|
<div class="text-2xl font-bold">{{ summary.total }}</div>
|
|
<div class="text-xs text-gray-500">Total</div>
|
|
</div>
|
|
<div class="bg-yellow-50 rounded-lg shadow p-3 text-center">
|
|
<div class="text-2xl font-bold text-yellow-600">{{ summary.ongoing }}</div>
|
|
<div class="text-xs text-gray-500">Ongoing</div>
|
|
</div>
|
|
<div class="bg-green-50 rounded-lg shadow p-3 text-center">
|
|
<div class="text-2xl font-bold text-green-600">{{ summary.completed }}</div>
|
|
<div class="text-xs text-gray-500">Completed</div>
|
|
</div>
|
|
<div class="bg-blue-50 rounded-lg shadow p-3 text-center">
|
|
<div class="text-xl font-bold text-blue-600">₱{{ Number(summary.total_budget ?? 0).toLocaleString() }}</div>
|
|
<div class="text-xs text-gray-500">Total Budget</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-2 mb-4">
|
|
<select v-model="statusFilter" @change="loadData" class="input w-36">
|
|
<option value="">All Status</option>
|
|
<option v-for="s in statusOptions" :key="s" :value="s">{{ s }}</option>
|
|
</select>
|
|
<select v-model="typeFilter" @change="loadData" class="input w-40">
|
|
<option value="">All Types</option>
|
|
<option v-for="t in projectTypes" :key="t" :value="t">{{ t }}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div v-if="loading" class="text-center py-8 text-gray-400">Loading...</div>
|
|
<div v-else class="grid gap-3">
|
|
<div v-for="p in projects" :key="p.id" class="bg-white rounded-lg shadow p-4">
|
|
<div class="flex justify-between items-start">
|
|
<div>
|
|
<h3 class="font-semibold">{{ p.project_name }}</h3>
|
|
<div class="text-xs text-gray-500 mt-1">{{ p.type }} | {{ p.fund_source?.replace('_', ' ') }}</div>
|
|
<div class="text-sm text-gray-700 mt-1">Budget: <strong>₱{{ Number(p.budget).toLocaleString() }}</strong></div>
|
|
<div class="text-xs text-gray-400 mt-1">{{ p.start_date }} — {{ p.end_date || 'TBD' }}</div>
|
|
</div>
|
|
<div class="flex flex-col items-end gap-2">
|
|
<span :class="`px-2 py-0.5 rounded-full text-xs font-medium ${statusColors[p.status] ?? 'bg-gray-100'}`">
|
|
{{ p.status }}
|
|
</span>
|
|
<div class="flex gap-1">
|
|
<button @click="openEdit(p)" class="btn-sm">Edit</button>
|
|
<select @change="e => updateStatus(p, e.target.value)" class="text-xs border rounded px-1">
|
|
<option value="">Change Status</option>
|
|
<option v-for="s in statusOptions" :key="s" :value="s">{{ s }}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p v-if="p.description" class="text-sm text-gray-600 mt-2 line-clamp-2">{{ p.description }}</p>
|
|
</div>
|
|
<p v-if="!projects.length" class="text-center text-gray-400 py-8">No projects found.</p>
|
|
</div>
|
|
|
|
<!-- Modal -->
|
|
<div v-if="showModal" class="modal-overlay" @click.self="showModal = false">
|
|
<div class="modal-box max-w-2xl">
|
|
<h2 class="text-lg font-bold mb-4">{{ editingItem ? 'Edit Project' : 'New Project' }}</h2>
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div class="col-span-2">
|
|
<label class="label">Project Name *</label>
|
|
<input v-model="form.project_name" class="input" required />
|
|
</div>
|
|
<div>
|
|
<label class="label">Type *</label>
|
|
<select v-model="form.type" class="input">
|
|
<option v-for="t in projectTypes" :key="t" :value="t">{{ t }}</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="label">Fund Source *</label>
|
|
<select v-model="form.fund_source" class="input">
|
|
<option v-for="f in fundSources" :key="f" :value="f">{{ f.replace('_', ' ') }}</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="label">Budget (₱) *</label>
|
|
<input v-model="form.budget" type="number" class="input" />
|
|
</div>
|
|
<div>
|
|
<label class="label">Beneficiaries Count</label>
|
|
<input v-model="form.beneficiaries_count" type="number" class="input" />
|
|
</div>
|
|
<div>
|
|
<label class="label">Start Date *</label>
|
|
<input v-model="form.start_date" type="date" class="input" />
|
|
</div>
|
|
<div>
|
|
<label class="label">Target End Date</label>
|
|
<input v-model="form.end_date" type="date" class="input" />
|
|
</div>
|
|
<div>
|
|
<label class="label">Implementing Office</label>
|
|
<input v-model="form.implementing_office" class="input" />
|
|
</div>
|
|
<div>
|
|
<label class="label">Contractor</label>
|
|
<input v-model="form.contractor" class="input" />
|
|
</div>
|
|
<div class="col-span-2">
|
|
<label class="label">Location</label>
|
|
<input v-model="form.location" class="input" />
|
|
</div>
|
|
<div class="col-span-2">
|
|
<label class="label">Description</label>
|
|
<textarea v-model="form.description" class="input h-20"></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-end gap-2 mt-4">
|
|
<button @click="showModal = false" class="btn-secondary">Cancel</button>
|
|
<button @click="saveProject" class="btn-primary">Save</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|