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

@@ -0,0 +1,152 @@
<script setup>
import { ref, onMounted } from 'vue';
import { usePageTitle } from '../../composables/Core/usePageTitle';
import { executeRequest } from '../../utils/executeRequest.js';
usePageTitle('Blotter Detail');
const blotter = ref(null);
const hearings = ref([]);
const loading = ref(false);
const showHearingModal = ref(false);
const hearingForm = ref({ hearing_date: '', officer_id: '', notes: '' });
const urlParams = new URLSearchParams(window.location.search);
const target = urlParams.get('target');
const statusLabels = {
FILED: 'Filed', FOR_HEARING: 'For Hearing', SETTLED: 'Settled',
RESOLVED: 'Resolved', DISMISSED: 'Dismissed', ENDORSED: 'Endorsed',
};
const statusColors = {
FILED: 'bg-blue-100 text-blue-700', FOR_HEARING: 'bg-yellow-100 text-yellow-700',
SETTLED: 'bg-green-100 text-green-700', RESOLVED: 'bg-teal-100 text-teal-700',
DISMISSED: 'bg-gray-100 text-gray-700', ENDORSED: 'bg-purple-100 text-purple-700',
};
const loadData = async () => {
loading.value = true;
const [bRes, hRes] = await Promise.all([
executeRequest('/blotters/show', 'POST', { target }),
executeRequest('/blotters/hearings', 'POST', { blotter: target }),
]);
if (bRes.success) blotter.value = bRes.data;
if (hRes.success) hearings.value = hRes.data;
loading.value = false;
};
const updateStatus = async (status) => {
if (!confirm(`Change status to ${statusLabels[status]}?`)) return;
await executeRequest('/blotters/status', 'POST', { target, status });
loadData();
};
const scheduleHearing = async () => {
await executeRequest('/blotters/hearings/schedule', 'POST', { blotter: target, ...hearingForm.value });
showHearingModal.value = false;
hearingForm.value = { hearing_date: '', officer_id: '', notes: '' };
loadData();
};
const getStatus = () => blotter.value?.status?.value ?? blotter.value?.status;
onMounted(loadData);
</script>
<template>
<div class="p-4 max-w-4xl mx-auto">
<div v-if="loading" class="text-center py-8 text-gray-400">Loading...</div>
<div v-else-if="blotter">
<div class="flex items-start justify-between mb-4">
<div>
<h1 class="text-2xl font-bold">Blotter {{ blotter.blotter_no }}</h1>
<p class="text-gray-500 text-sm">Filed: {{ blotter.complaint_date }}</p>
</div>
<span :class="`px-3 py-1 rounded-full text-sm font-medium ${statusColors[getStatus()] ?? 'bg-gray-100'}`">
{{ statusLabels[getStatus()] ?? getStatus() }}
</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
<div class="bg-white rounded-lg shadow p-4">
<h3 class="font-semibold text-gray-700 mb-2 border-b pb-1">Complainant</h3>
<p class="font-medium">{{ blotter.complainant_name }}</p>
<p class="text-sm text-gray-500">{{ blotter.complainant_contact }}</p>
<p class="text-sm text-gray-500">{{ blotter.complainant_address }}</p>
</div>
<div class="bg-white rounded-lg shadow p-4">
<h3 class="font-semibold text-gray-700 mb-2 border-b pb-1">Respondent</h3>
<p class="font-medium">{{ blotter.respondent_name }}</p>
<p class="text-sm text-gray-500">{{ blotter.respondent_contact }}</p>
<p class="text-sm text-gray-500">{{ blotter.respondent_address }}</p>
</div>
</div>
<div class="bg-white rounded-lg shadow p-4 mb-4">
<h3 class="font-semibold text-gray-700 mb-2">Incident Details</h3>
<div class="grid grid-cols-2 gap-2 text-sm mb-3">
<div><span class="text-gray-500">Type:</span> {{ blotter.incident_type }}</div>
<div><span class="text-gray-500">Date:</span> {{ blotter.incident_date }}</div>
<div class="col-span-2"><span class="text-gray-500">Location:</span> {{ blotter.incident_location }}</div>
</div>
<div class="bg-gray-50 rounded p-3 text-sm text-gray-700">{{ blotter.narrative }}</div>
</div>
<div v-if="blotter.resolution" class="bg-green-50 rounded-lg p-4 mb-4">
<h3 class="font-semibold text-green-700 mb-1">Resolution</h3>
<p class="text-sm">{{ blotter.resolution }}</p>
</div>
<!-- Hearings -->
<div class="bg-white rounded-lg shadow p-4 mb-4">
<div class="flex items-center justify-between mb-3">
<h3 class="font-semibold text-gray-700">Hearings ({{ hearings.length }})</h3>
<button @click="showHearingModal = true" class="btn-sm btn-primary">Schedule Hearing</button>
</div>
<div v-for="h in hearings" :key="h.id" class="border-b py-2 text-sm">
<div class="flex justify-between">
<span class="font-medium">{{ h.hearing_date }}</span>
<span :class="`text-xs px-2 py-0.5 rounded ${h.status === 'HELD' ? 'bg-green-100 text-green-700' : h.status === 'POSTPONED' ? 'bg-orange-100 text-orange-700' : 'bg-blue-100 text-blue-700'}`">{{ h.status }}</span>
</div>
<p v-if="h.notes" class="text-gray-500 mt-1">{{ h.notes }}</p>
</div>
<p v-if="!hearings.length" class="text-gray-400 text-sm">No hearings scheduled.</p>
</div>
<!-- Actions -->
<div class="bg-white rounded-lg shadow p-4">
<h3 class="font-semibold text-gray-700 mb-3">Update Status</h3>
<div class="flex flex-wrap gap-2">
<button v-if="getStatus() === 'FILED'" @click="updateStatus('FOR_HEARING')" class="btn-sm bg-yellow-500 text-white">Set For Hearing</button>
<button v-if="['FILED','FOR_HEARING'].includes(getStatus())" @click="updateStatus('SETTLED')" class="btn-sm bg-green-500 text-white">Mark Settled</button>
<button v-if="['FILED','FOR_HEARING','SETTLED'].includes(getStatus())" @click="updateStatus('RESOLVED')" class="btn-sm bg-teal-500 text-white">Mark Resolved</button>
<button v-if="['FILED','FOR_HEARING'].includes(getStatus())" @click="updateStatus('DISMISSED')" class="btn-sm bg-gray-500 text-white">Dismiss</button>
<button v-if="['FILED','FOR_HEARING'].includes(getStatus())" @click="updateStatus('ENDORSED')" class="btn-sm bg-purple-500 text-white">Endorse</button>
</div>
</div>
<!-- Schedule Hearing Modal -->
<div v-if="showHearingModal" class="modal-overlay" @click.self="showHearingModal = false">
<div class="modal-box">
<h2 class="text-lg font-bold mb-4">Schedule a Hearing</h2>
<div class="space-y-3">
<div>
<label class="label">Hearing Date & Time *</label>
<input v-model="hearingForm.hearing_date" type="datetime-local" class="input" />
</div>
<div>
<label class="label">Notes</label>
<textarea v-model="hearingForm.notes" class="input h-20"></textarea>
</div>
</div>
<div class="flex justify-end gap-2 mt-4">
<button @click="showHearingModal = false" class="btn-secondary">Cancel</button>
<button @click="scheduleHearing" class="btn-primary">Schedule</button>
</div>
</div>
</div>
</div>
<p v-else class="text-center text-gray-400 py-8">Blotter not found.</p>
</div>
</template>

View File

@@ -0,0 +1,178 @@
<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>

View File

@@ -0,0 +1,188 @@
<script setup>
import { ref, onMounted, computed } from 'vue';
import { usePageTitle } from '../../composables/Core/usePageTitle';
import { executeRequest } from '../../utils/executeRequest.js';
import { navigate } from '../../utils/navigate.js';
import { useAuth } from '../../composables/Core/useAuth.js';
usePageTitle('Document Request Detail');
const { isBarangayStaff } = useAuth();
const request = ref(null);
const loading = ref(false);
const urlParams = new URLSearchParams(window.location.search);
const target = urlParams.get('target');
const statusColors = {
PENDING: 'bg-yellow-100 text-yellow-700',
PAYMENT_PENDING: 'bg-orange-100 text-orange-700',
PAID: 'bg-blue-100 text-blue-700',
PROCESSING:'bg-indigo-100 text-indigo-700',
READY: 'bg-teal-100 text-teal-700',
CLAIMED: 'bg-green-100 text-green-700',
CANCELLED: 'bg-red-100 text-red-700',
};
const statusLabels = {
PENDING: 'Pending',
PAYMENT_PENDING: 'Awaiting Payment',
PAID: 'Paid',
PROCESSING:'Processing',
READY: 'Ready for Pickup',
CLAIMED: 'Claimed',
CANCELLED: 'Cancelled',
};
const getStatus = computed(() => request.value?.status?.value ?? request.value?.status ?? '');
const loadRequest = async () => {
loading.value = true;
const res = await executeRequest('/admin/documents/show', 'POST', { target });
if (res.success) request.value = res.data;
loading.value = false;
};
const confirmPayment = async () => {
if (!confirm('Confirm payment received?')) return;
const res = await executeRequest('/admin/documents/confirm-payment', 'POST', {
target,
amount_paid: request.value?.base_fee ?? 0,
payment_method: 'CASH',
});
if (res.success) loadRequest();
};
const markReady = async () => {
if (!confirm('Mark as ready for pickup?')) return;
const res = await executeRequest('/admin/documents/mark-ready', 'POST', { target });
if (res.success) loadRequest();
};
const markClaimed = async () => {
if (!confirm('Mark as claimed by resident?')) return;
const res = await executeRequest('/admin/documents/mark-claimed', 'POST', { target });
if (res.success) loadRequest();
};
const cancel = async () => {
if (!confirm('Cancel this request?')) return;
const res = await executeRequest('/documents/cancel', 'POST', { target });
if (res.success) loadRequest();
};
onMounted(loadRequest);
</script>
<template>
<div class="p-4 max-w-3xl mx-auto">
<button @click="navigate(isBarangayStaff ? '/barangay/managedocumentrequests' : '/barangay/requestdocument')"
class="text-sm text-blue-500 mb-4 inline-flex items-center gap-1">
Back
</button>
<div v-if="loading" class="text-center py-8 text-gray-400">Loading...</div>
<div v-else-if="request">
<!-- Header card -->
<div class="bg-white rounded-xl shadow p-5 mb-4">
<div class="flex justify-between items-start">
<div>
<div class="flex items-center gap-2 mb-1">
<span class="font-mono font-semibold text-blue-600 text-lg">{{ request.request_no }}</span>
<span :class="`text-xs px-2 py-0.5 rounded-full font-medium ${statusColors[getStatus] ?? 'bg-gray-100'}`">
{{ statusLabels[getStatus] ?? getStatus }}
</span>
</div>
<p class="text-xl font-bold text-gray-800">{{ request.document_type?.name ?? request.request_type }}</p>
<p class="text-sm text-gray-500 mt-1">Submitted: {{ request.created_at }}</p>
</div>
<div class="text-right">
<div class="text-2xl font-bold text-green-600">
{{ request.base_fee > 0 ? `${Number(request.base_fee).toFixed(2)}` : 'Free' }}
</div>
<div class="text-xs text-gray-400">Fee</div>
</div>
</div>
</div>
<!-- Requester info -->
<div class="bg-white rounded-xl shadow p-5 mb-4">
<h2 class="font-semibold text-gray-700 mb-3 border-b pb-2">Requester Information</h2>
<div class="grid grid-cols-2 gap-3 text-sm">
<div><span class="text-gray-400">Name:</span> {{ request.resident?.fullname ?? request.requester_name ?? '—' }}</div>
<div><span class="text-gray-400">Purok:</span> {{ request.resident?.purok ?? '—' }}</div>
<div class="col-span-2"><span class="text-gray-400">Purpose:</span> {{ request.purpose ?? '—' }}</div>
<div v-if="request.remarks" class="col-span-2">
<span class="text-gray-400">Remarks:</span> {{ request.remarks }}
</div>
</div>
</div>
<!-- Payment history -->
<div v-if="request.payments && request.payments.length" class="bg-white rounded-xl shadow p-5 mb-4">
<h2 class="font-semibold text-gray-700 mb-3 border-b pb-2">Payment Records</h2>
<div v-for="p in request.payments" :key="p.id" class="flex justify-between text-sm py-1 border-b last:border-0">
<span>{{ p.payment_method }} {{ p.created_at }}</span>
<span class="font-semibold text-green-600">{{ Number(p.amount_paid).toFixed(2) }}</span>
</div>
</div>
<!-- Staff actions -->
<div v-if="isBarangayStaff" class="bg-white rounded-xl shadow p-5">
<h2 class="font-semibold text-gray-700 mb-3">Actions</h2>
<div class="flex flex-wrap gap-2">
<button
v-if="['PENDING','PAYMENT_PENDING'].includes(getStatus)"
@click="confirmPayment"
class="btn-sm bg-green-500 text-white">
Confirm Payment
</button>
<button
v-if="['PAID','PROCESSING'].includes(getStatus)"
@click="markReady"
class="btn-sm bg-teal-500 text-white">
Mark Ready for Pickup
</button>
<button
v-if="getStatus === 'READY'"
@click="markClaimed"
class="btn-sm bg-blue-500 text-white">
Mark Claimed
</button>
<button
v-if="!['CLAIMED','CANCELLED'].includes(getStatus)"
@click="cancel"
class="btn-sm bg-red-100 text-red-600">
Cancel Request
</button>
<p v-if="['CLAIMED','CANCELLED'].includes(getStatus)" class="text-sm text-gray-400">
This request is {{ statusLabels[getStatus].toLowerCase() }}. No further actions available.
</p>
</div>
</div>
<!-- Resident view: status tracker -->
<div v-else class="bg-white rounded-xl shadow p-5">
<h2 class="font-semibold text-gray-700 mb-3">Request Progress</h2>
<div class="flex items-center gap-1 text-xs">
<template v-for="(step, i) in ['PENDING','PAID','PROCESSING','READY','CLAIMED']" :key="step">
<div :class="`px-2 py-1 rounded font-medium ${['PENDING','PAYMENT_PENDING','PAID','PROCESSING','READY','CLAIMED'].indexOf(getStatus) >= i ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-400'}`">
{{ statusLabels[step] ?? step }}
</div>
<div v-if="i < 4" class="flex-1 h-0.5 bg-gray-200"></div>
</template>
</div>
<p v-if="getStatus === 'READY'" class="mt-3 text-sm text-teal-600 font-medium">
Your document is ready for pickup at the barangay hall.
</p>
<p v-else-if="getStatus === 'CANCELLED'" class="mt-3 text-sm text-red-500">
This request has been cancelled.
</p>
</div>
</div>
<p v-else class="text-center text-gray-400 py-8">Request not found.</p>
</div>
</template>

View File

@@ -0,0 +1,180 @@
<script setup>
import { ref, onMounted } from 'vue';
import { usePageTitle } from '../../composables/Core/usePageTitle';
import { executeRequest } from '../../utils/executeRequest.js';
import { navigate } from '../../utils/navigate.js';
usePageTitle('Blotter Records');
const blotters = ref([]);
const loading = ref(false);
const statusFilter = ref('');
const typeFilter = ref('');
const search = ref('');
const showModal = ref(false);
const form = ref({
complainant_name: '', complainant_contact: '', complainant_address: '',
respondent_name: '', respondent_contact: '', respondent_address: '',
incident_type: 'AMICABLE', incident_date: '', incident_location: '', narrative: '',
});
const statusLabels = {
FILED: 'Filed', FOR_HEARING: 'For Hearing', SETTLED: 'Settled',
RESOLVED: 'Resolved', DISMISSED: 'Dismissed', ENDORSED: 'Endorsed',
};
const statusColors = {
FILED: 'bg-blue-100 text-blue-700', FOR_HEARING: 'bg-yellow-100 text-yellow-700',
SETTLED: 'bg-green-100 text-green-700', RESOLVED: 'bg-teal-100 text-teal-700',
DISMISSED: 'bg-gray-100 text-gray-700', ENDORSED: 'bg-purple-100 text-purple-700',
};
const loadBlotters = async () => {
loading.value = true;
const params = new URLSearchParams();
if (statusFilter.value) params.append('status', statusFilter.value);
if (typeFilter.value) params.append('incident_type', typeFilter.value);
if (search.value) params.append('search', search.value);
const res = await executeRequest(`/blotters?${params}`);
if (res.success) blotters.value = res.data.data ?? res.data;
loading.value = false;
};
const submitBlotter = async () => {
const res = await executeRequest('/blotters/create', 'POST', form.value);
if (res.success) {
showModal.value = false;
form.value = {
complainant_name: '', complainant_contact: '', complainant_address: '',
respondent_name: '', respondent_contact: '', respondent_address: '',
incident_type: 'AMICABLE', incident_date: '', incident_location: '', narrative: '',
};
loadBlotters();
}
};
const viewBlotter = (item) => navigate(`/Barangay/BlotterDetail?target=${item.hashkey}`);
onMounted(loadBlotters);
</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">Blotter Records</h1>
<button @click="showModal = true" class="btn-primary">+ File Blotter</button>
</div>
<div class="flex gap-2 mb-4">
<input v-model="search" @input="loadBlotters" placeholder="Search blotter no. or name..." class="input flex-1" />
<select v-model="statusFilter" @change="loadBlotters" class="input w-36">
<option value="">All Status</option>
<option v-for="(label, val) in statusLabels" :key="val" :value="val">{{ label }}</option>
</select>
<select v-model="typeFilter" @change="loadBlotters" class="input w-36">
<option value="">All Types</option>
<option value="AMICABLE">Amicable</option>
<option value="UNLAWFUL">Unlawful</option>
<option value="MINOR">Minor</option>
<option value="OTHER">Other</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">Blotter No.</th>
<th class="text-left p-2">Complainant</th>
<th class="text-left p-2">Respondent</th>
<th class="text-left p-2">Type</th>
<th class="text-left p-2">Date</th>
<th class="text-left p-2">Status</th>
<th class="p-2"></th>
</tr>
</thead>
<tbody>
<tr v-for="b in blotters" :key="b.id" class="border-b hover:bg-gray-50">
<td class="p-2 font-mono text-xs">{{ b.blotter_no }}</td>
<td class="p-2">{{ b.complainant_name }}</td>
<td class="p-2">{{ b.respondent_name }}</td>
<td class="p-2 capitalize">{{ b.incident_type?.toLowerCase() }}</td>
<td class="p-2">{{ b.incident_date }}</td>
<td class="p-2">
<span :class="`px-2 py-0.5 rounded-full text-xs font-medium ${statusColors[b.status?.value ?? b.status] ?? 'bg-gray-100'}`">
{{ statusLabels[b.status?.value ?? b.status] ?? b.status }}
</span>
</td>
<td class="p-2">
<button @click="viewBlotter(b)" class="btn-sm">View</button>
</td>
</tr>
</tbody>
</table>
<p v-if="!blotters.length" class="text-center text-gray-400 py-8">No blotter records found.</p>
</div>
<!-- File Blotter 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">File a Blotter</h2>
<div class="grid grid-cols-2 gap-3">
<div class="col-span-2 font-semibold text-gray-600 border-b pb-1">Complainant</div>
<div>
<label class="label">Name *</label>
<input v-model="form.complainant_name" class="input" required />
</div>
<div>
<label class="label">Contact</label>
<input v-model="form.complainant_contact" class="input" />
</div>
<div class="col-span-2">
<label class="label">Address</label>
<input v-model="form.complainant_address" class="input" />
</div>
<div class="col-span-2 font-semibold text-gray-600 border-b pb-1 mt-2">Respondent</div>
<div>
<label class="label">Name *</label>
<input v-model="form.respondent_name" class="input" required />
</div>
<div>
<label class="label">Contact</label>
<input v-model="form.respondent_contact" class="input" />
</div>
<div class="col-span-2">
<label class="label">Address</label>
<input v-model="form.respondent_address" class="input" />
</div>
<div class="col-span-2 font-semibold text-gray-600 border-b pb-1 mt-2">Incident Details</div>
<div>
<label class="label">Incident Type *</label>
<select v-model="form.incident_type" class="input">
<option value="AMICABLE">Amicable</option>
<option value="UNLAWFUL">Unlawful</option>
<option value="MINOR">Minor</option>
<option value="OTHER">Other</option>
</select>
</div>
<div>
<label class="label">Incident Date *</label>
<input v-model="form.incident_date" type="date" class="input" />
</div>
<div class="col-span-2">
<label class="label">Location</label>
<input v-model="form.incident_location" class="input" />
</div>
<div class="col-span-2">
<label class="label">Narrative *</label>
<textarea v-model="form.narrative" class="input h-24" placeholder="Describe the incident in detail..."></textarea>
</div>
</div>
<div class="flex justify-end gap-2 mt-4">
<button @click="showModal = false" class="btn-secondary">Cancel</button>
<button @click="submitBlotter" class="btn-primary">File Blotter</button>
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,113 @@
<script setup>
import { ref, onMounted } from 'vue';
import { usePageTitle } from '../../composables/Core/usePageTitle';
import { executeRequest } from '../../utils/executeRequest.js';
import { navigate } from '../../utils/navigate.js';
usePageTitle('Document Requests');
const requests = ref([]);
const loading = ref(false);
const statusFilter = ref('');
const search = ref('');
const statusLabels = {
DRAFT: 'Draft', PENDING_PAYMENT: 'Pending Payment', PAID: 'Paid',
PROCESSING: 'Processing', READY: 'Ready for Pickup', CLAIMED: 'Claimed', CANCELLED: 'Cancelled',
};
const statusColors = {
DRAFT: 'bg-gray-100 text-gray-700',
PENDING_PAYMENT: 'bg-yellow-100 text-yellow-700',
PAID: 'bg-blue-100 text-blue-700',
PROCESSING: 'bg-orange-100 text-orange-700',
READY: 'bg-teal-100 text-teal-700',
CLAIMED: 'bg-green-100 text-green-700',
CANCELLED: 'bg-red-100 text-red-700',
};
const loadRequests = async () => {
loading.value = true;
const params = new URLSearchParams();
if (statusFilter.value) params.append('status', statusFilter.value);
if (search.value) params.append('search', search.value);
const res = await executeRequest(`/admin/documents?${params}`);
if (res.success) requests.value = res.data.data ?? res.data;
loading.value = false;
};
const viewRequest = (item) => navigate(`/Barangay/DocumentRequestDetail?target=${item.hashkey}`);
const quickAction = async (hashkey, action) => {
const urlMap = {
confirm: '/admin/documents/confirm-payment',
ready: '/admin/documents/mark-ready',
claim: '/admin/documents/mark-claimed',
};
await executeRequest(urlMap[action], 'POST', { target: hashkey, method: 'CASH' });
loadRequests();
};
const getStatus = (item) => item.status?.value ?? item.status;
onMounted(loadRequests);
</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">Document Requests</h1>
<button @click="navigate('/Barangay/RequestDocument')" class="btn-primary">+ New Request</button>
</div>
<div class="flex gap-2 mb-4">
<input v-model="search" @input="loadRequests" placeholder="Search request no..." class="input flex-1" />
<select v-model="statusFilter" @change="loadRequests" class="input w-44">
<option value="">All Status</option>
<option v-for="(label, val) in statusLabels" :key="val" :value="val">{{ label }}</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">Request No.</th>
<th class="text-left p-2">Type</th>
<th class="text-left p-2">Resident</th>
<th class="text-left p-2">Fee</th>
<th class="text-left p-2">Payment</th>
<th class="text-left p-2">Status</th>
<th class="text-left p-2">Date</th>
<th class="p-2"></th>
</tr>
</thead>
<tbody>
<tr v-for="r in requests" :key="r.id" class="border-b hover:bg-gray-50">
<td class="p-2 font-mono text-xs">{{ r.request_no }}</td>
<td class="p-2">{{ r.request_type?.name }}</td>
<td class="p-2">{{ r.resident?.name }}</td>
<td class="p-2">{{ Number(r.fee_amount).toFixed(2) }}</td>
<td class="p-2 capitalize text-xs">
{{ (r.payment_status?.value ?? r.payment_status)?.toLowerCase() }}
</td>
<td class="p-2">
<span :class="`px-2 py-0.5 rounded-full text-xs font-medium ${statusColors[getStatus(r)] ?? 'bg-gray-100'}`">
{{ statusLabels[getStatus(r)] ?? getStatus(r) }}
</span>
</td>
<td class="p-2 text-xs text-gray-500">{{ r.created_at?.slice(0, 10) }}</td>
<td class="p-2 flex gap-1">
<button @click="viewRequest(r)" class="btn-sm">View</button>
<button v-if="getStatus(r) === 'PENDING_PAYMENT'" @click="quickAction(r.hashkey, 'confirm')" class="btn-sm bg-blue-500 text-white">Pay</button>
<button v-if="getStatus(r) === 'PROCESSING'" @click="quickAction(r.hashkey, 'ready')" class="btn-sm bg-teal-500 text-white">Ready</button>
<button v-if="getStatus(r) === 'READY'" @click="quickAction(r.hashkey, 'claim')" class="btn-sm bg-green-500 text-white">Claimed</button>
</td>
</tr>
</tbody>
</table>
<p v-if="!requests.length" class="text-center text-gray-400 py-8">No document requests found.</p>
</div>
</div>
</template>

View File

@@ -0,0 +1,212 @@
<script setup>
import { ref, onMounted } from 'vue';
import { usePageTitle } from '../../composables/Core/usePageTitle';
import { executeRequest } from '../../utils/executeRequest.js';
usePageTitle('Manage Households');
const households = ref([]);
const loading = ref(false);
const showModal = ref(false);
const showMemberModal = ref(false);
const editingItem = ref(null);
const selectedHousehold = ref(null);
const residentSearch = ref('');
const residentResults = ref([]);
const pagination = ref({});
const currentPage = ref(1);
const search = ref('');
const blankForm = () => ({
address: '', purok: '', ownership_type: 'OWNED',
has_electricity: false, has_water: false, notes: '',
});
const form = ref(blankForm());
const ownershipTypes = ['OWNED', 'RENTED', 'SHARED'];
const loadHouseholds = async (page = 1) => {
loading.value = true;
const params = new URLSearchParams({ page, search: search.value }).toString();
const res = await executeRequest(`/households?${params}`);
if (res.success) {
households.value = res.data.data ?? res.data;
pagination.value = res.data;
currentPage.value = page;
}
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 save = async () => {
const url = editingItem.value ? '/households/update' : '/households/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; loadHouseholds(); }
};
const openMembers = (household) => {
selectedHousehold.value = household;
residentSearch.value = '';
residentResults.value = [];
showMemberModal.value = true;
};
const searchResidents = async () => {
if (residentSearch.value.length < 2) return;
const res = await executeRequest(`/residents/search?q=${encodeURIComponent(residentSearch.value)}`);
if (res.success) residentResults.value = res.data;
};
const addMember = async (resident) => {
await executeRequest('/households/members/add', 'POST', {
household: selectedHousehold.value.hashkey,
resident: resident.hashkey,
});
residentResults.value = [];
residentSearch.value = '';
loadHouseholds();
};
const removeMember = async (household, residentHashkey) => {
if (!confirm('Remove this member from household?')) return;
await executeRequest('/households/members/remove', 'POST', {
household: household.hashkey,
resident: residentHashkey,
});
loadHouseholds();
};
onMounted(() => loadHouseholds());
</script>
<template>
<div class="p-4 max-w-5xl mx-auto">
<div class="flex items-center justify-between mb-4">
<h1 class="text-2xl font-bold">Households</h1>
<button @click="openCreate" class="btn-primary">+ New Household</button>
</div>
<div class="flex gap-2 mb-4">
<input v-model="search" @keyup.enter="loadHouseholds(1)" placeholder="Search address or household no..." class="input flex-1" />
<button @click="loadHouseholds(1)" class="btn-secondary">Search</button>
</div>
<div v-if="loading" class="text-center py-8 text-gray-400">Loading...</div>
<div v-else class="space-y-3">
<div v-for="h in households" :key="h.id" class="bg-white rounded-lg shadow p-4">
<div class="flex justify-between items-start">
<div>
<div class="flex items-center gap-2 mb-1">
<span class="font-mono text-sm font-semibold text-blue-600">{{ h.household_no }}</span>
<span class="text-xs bg-gray-100 px-2 py-0.5 rounded">{{ h.ownership_type }}</span>
<span v-if="h.has_electricity" class="text-xs bg-yellow-100 text-yellow-700 px-2 py-0.5 rounded"> Power</span>
<span v-if="h.has_water" class="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded">💧 Water</span>
</div>
<p class="text-gray-700">{{ h.address }}</p>
<p v-if="h.purok" class="text-sm text-gray-500">Purok {{ h.purok }}</p>
<p class="text-sm text-gray-400 mt-1">{{ h.members_count ?? 0 }} member(s)</p>
</div>
<div class="flex gap-2">
<button @click="openMembers(h)" class="btn-sm">Members</button>
<button @click="openEdit(h)" class="btn-sm">Edit</button>
</div>
</div>
<!-- Members list inline -->
<div v-if="h.members && h.members.length" class="mt-3 pt-3 border-t">
<div class="flex flex-wrap gap-2">
<span v-for="m in h.members" :key="m.id"
class="flex items-center gap-1 bg-gray-50 border rounded px-2 py-1 text-sm">
{{ m.fullname ?? `${m.first_name} ${m.last_name}` }}
<button @click="removeMember(h, m.hashkey)" class="text-red-400 hover:text-red-600 ml-1">&times;</button>
</span>
</div>
</div>
</div>
<p v-if="!households.length" class="text-center text-gray-400 py-8">No households found.</p>
</div>
<!-- Pagination -->
<div v-if="pagination.last_page > 1" class="flex justify-center gap-2 mt-4">
<button v-for="p in pagination.last_page" :key="p"
@click="loadHouseholds(p)"
:class="`btn-sm ${p === currentPage ? 'btn-primary' : ''}`">{{ p }}</button>
</div>
<!-- Create/Edit 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 Household' : 'New Household' }}</h2>
<div class="space-y-3">
<div>
<label class="label">Address *</label>
<input v-model="form.address" class="input" required />
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="label">Purok</label>
<input v-model="form.purok" class="input" placeholder="e.g. 1, 2A" />
</div>
<div>
<label class="label">Ownership Type</label>
<select v-model="form.ownership_type" class="input">
<option v-for="o in ownershipTypes" :key="o" :value="o">{{ o }}</option>
</select>
</div>
</div>
<div class="flex gap-4">
<label class="flex items-center gap-2 cursor-pointer">
<input v-model="form.has_electricity" type="checkbox" class="rounded" />
<span class="text-sm">Has Electricity</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input v-model="form.has_water" type="checkbox" class="rounded" />
<span class="text-sm">Has Water</span>
</label>
</div>
<div>
<label class="label">Notes</label>
<textarea v-model="form.notes" class="input h-16"></textarea>
</div>
</div>
<div class="flex justify-end gap-2 mt-4">
<button @click="showModal = false" class="btn-secondary">Cancel</button>
<button @click="save" class="btn-primary">Save</button>
</div>
</div>
</div>
<!-- Add Member Modal -->
<div v-if="showMemberModal" class="modal-overlay" @click.self="showMemberModal = false">
<div class="modal-box">
<h2 class="text-lg font-bold mb-3">Add Member to {{ selectedHousehold?.household_no }}</h2>
<div class="flex gap-2 mb-3">
<input v-model="residentSearch" @input="searchResidents" placeholder="Search resident name..." class="input flex-1" />
</div>
<div class="space-y-2 max-h-64 overflow-y-auto">
<div v-for="r in residentResults" :key="r.id"
class="flex justify-between items-center bg-gray-50 rounded p-2">
<span class="text-sm">{{ r.fullname ?? `${r.first_name} ${r.last_name}` }}</span>
<button @click="addMember(r)" class="btn-sm btn-primary text-xs">Add</button>
</div>
<p v-if="!residentResults.length && residentSearch.length >= 2" class="text-center text-gray-400 text-sm py-2">No results.</p>
</div>
<div class="flex justify-end mt-4">
<button @click="showMemberModal = false" class="btn-secondary">Close</button>
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,193 @@
<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>

View File

@@ -0,0 +1,116 @@
<script setup>
import { ref, onMounted } from 'vue';
import { usePageTitle } from '../../composables/Core/usePageTitle';
import { executeRequest } from '../../utils/executeRequest.js';
usePageTitle('Document Types & Fees');
const types = ref([]);
const loading = ref(false);
const showModal = ref(false);
const editingItem = ref(null);
const blankForm = () => ({
name: '', code: '', description: '',
base_fee: 0, processing_days: 1, requires_clearance: false,
});
const form = ref(blankForm());
const loadTypes = async () => {
loading.value = true;
const res = await executeRequest('/admin/request-types');
if (res.success) types.value = res.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 save = async () => {
const url = editingItem.value ? '/admin/request-types/update' : '/admin/request-types/create';
const payload = editingItem.value ? { target: editingItem.value.code, ...form.value } : form.value;
const res = await executeRequest(url, 'POST', payload);
if (res.success) { showModal.value = false; loadTypes(); }
};
const toggleActive = async (item) => {
await executeRequest('/admin/request-types/toggle', 'POST', { target: item.code });
loadTypes();
};
onMounted(loadTypes);
</script>
<template>
<div class="p-4 max-w-4xl mx-auto">
<div class="flex items-center justify-between mb-4">
<h1 class="text-2xl font-bold">Document Types & Fee Schedule</h1>
<button @click="openCreate" class="btn-primary">+ Add Type</button>
</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="t in types" :key="t.id" class="bg-white rounded-lg shadow p-4 flex justify-between items-center">
<div>
<div class="flex items-center gap-2">
<h3 class="font-semibold">{{ t.name }}</h3>
<span class="font-mono text-xs bg-gray-100 px-2 py-0.5 rounded">{{ t.code }}</span>
<span v-if="!t.is_active" class="text-xs text-red-500">Inactive</span>
</div>
<p class="text-sm text-gray-500 mt-0.5">{{ t.description }}</p>
<div class="flex gap-4 mt-1 text-sm">
<span class="font-semibold text-green-600">{{ t.base_fee > 0 ? `${t.base_fee}` : 'Free' }}</span>
<span class="text-gray-400">{{ t.processing_days }} day(s) processing</span>
<span v-if="t.requires_clearance" class="text-orange-500">Requires Clearance</span>
</div>
</div>
<div class="flex gap-2">
<button @click="openEdit(t)" class="btn-sm">Edit</button>
<button @click="toggleActive(t)" :class="`btn-sm ${t.is_active ? 'text-red-500' : 'text-green-500'}`">
{{ t.is_active ? 'Disable' : 'Enable' }}
</button>
</div>
</div>
<p v-if="!types.length" class="text-center text-gray-400 py-8">No document types defined.</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 Document Type' : 'New Document Type' }}</h2>
<div class="space-y-3">
<div>
<label class="label">Name *</label>
<input v-model="form.name" class="input" required />
</div>
<div>
<label class="label">Code *</label>
<input v-model="form.code" class="input" placeholder="e.g. CLEARANCE" :disabled="!!editingItem" />
</div>
<div>
<label class="label">Description</label>
<textarea v-model="form.description" class="input h-16"></textarea>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="label">Base Fee ()</label>
<input v-model="form.base_fee" type="number" step="0.01" class="input" />
</div>
<div>
<label class="label">Processing Days</label>
<input v-model="form.processing_days" type="number" min="1" class="input" />
</div>
</div>
<label class="flex items-center gap-2 cursor-pointer">
<input v-model="form.requires_clearance" type="checkbox" class="rounded" />
<span class="text-sm">Requires prior clearance</span>
</label>
</div>
<div class="flex justify-end gap-2 mt-4">
<button @click="showModal = false" class="btn-secondary">Cancel</button>
<button @click="save" class="btn-primary">Save</button>
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,213 @@
<script setup>
import { ref, onMounted, computed } from 'vue';
import { usePageTitle } from '../../composables/Core/usePageTitle';
import { executeRequest } from '../../utils/executeRequest.js';
import { navigate } from '../../utils/navigate.js';
usePageTitle('Residents');
const residents = ref([]);
const loading = ref(false);
const search = ref('');
const purokFilter = ref('');
const puroks = ref([]);
const page = ref(1);
const total = ref(0);
const perPage = 20;
const showModal = ref(false);
const editingItem = ref(null);
const blankForm = () => ({
firstname: '', middlename: '', lastname: '', suffix: '',
dob: '', birthplace: '', gender: 'MALE', civil_status: 'SINGLE',
citizenship: 'Filipino', religion: '', occupation: '',
monthly_income: '', blood_type: '',
voter_status: false, head_of_household: false,
purok: '', street: '', barangay: '', city: '', province: '', region: '',
philhealth_id: '', sss_id: '', gsis_id: '', tin: '',
emergency_contact_name: '', emergency_contact_phone: '', emergency_contact_address: '',
});
const form = ref(blankForm());
const loadResidents = async () => {
loading.value = true;
const params = new URLSearchParams({ page: page.value, per_page: perPage });
if (search.value) params.append('search', search.value);
if (purokFilter.value) params.append('purok', purokFilter.value);
const res = await executeRequest(`/residents?${params}`);
if (res.success) {
residents.value = res.data.data ?? res.data;
total.value = res.data.total ?? residents.value.length;
}
loading.value = false;
};
const loadPuroks = async () => {
const res = await executeRequest('/residents/puroks');
if (res.success) puroks.value = res.data;
};
const openCreate = () => {
editingItem.value = null;
form.value = blankForm();
showModal.value = true;
};
const openEdit = (item) => {
editingItem.value = item;
form.value = { ...item };
showModal.value = true;
};
const saveResident = async () => {
const url = editingItem.value ? '/residents/update' : '/residents/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;
loadResidents();
}
};
const viewProfile = (item) => navigate(`/Barangay/ResidentProfile?target=${item.hashkey}`);
onMounted(() => { loadResidents(); loadPuroks(); });
</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">Resident Records</h1>
<button @click="openCreate" class="btn-primary">+ Add Resident</button>
</div>
<div class="flex gap-2 mb-4">
<input v-model="search" @input="loadResidents" placeholder="Search by name..." class="input flex-1" />
<select v-model="purokFilter" @change="loadResidents" class="input w-40">
<option value="">All Puroks</option>
<option v-for="p in puroks" :key="p" :value="p">{{ p }}</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">Name</th>
<th class="text-left p-2">DOB</th>
<th class="text-left p-2">Gender</th>
<th class="text-left p-2">Purok</th>
<th class="text-left p-2">Status</th>
<th class="p-2"></th>
</tr>
</thead>
<tbody>
<tr v-for="r in residents" :key="r.id" class="border-b hover:bg-gray-50">
<td class="p-2 font-medium">{{ r.lastname }}, {{ r.firstname }} {{ r.middlename }}</td>
<td class="p-2">{{ r.dob }}</td>
<td class="p-2">{{ r.gender }}</td>
<td class="p-2">{{ r.purok || '—' }}</td>
<td class="p-2">
<span :class="r.is_active ? 'badge-green' : 'badge-red'">
{{ r.is_active ? 'Active' : 'Inactive' }}
</span>
</td>
<td class="p-2 flex gap-2">
<button @click="viewProfile(r)" class="btn-sm">View</button>
<button @click="openEdit(r)" class="btn-sm">Edit</button>
</td>
</tr>
</tbody>
</table>
<p v-if="!residents.length" class="text-center text-gray-400 py-8">No residents found.</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 Resident' : 'New Resident' }}</h2>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="label">First Name *</label>
<input v-model="form.firstname" class="input" required />
</div>
<div>
<label class="label">Middle Name</label>
<input v-model="form.middlename" class="input" />
</div>
<div>
<label class="label">Last Name *</label>
<input v-model="form.lastname" class="input" required />
</div>
<div>
<label class="label">Suffix</label>
<input v-model="form.suffix" class="input" placeholder="Jr., Sr., III" />
</div>
<div>
<label class="label">Date of Birth *</label>
<input v-model="form.dob" type="date" class="input" required />
</div>
<div>
<label class="label">Gender *</label>
<select v-model="form.gender" class="input">
<option value="MALE">Male</option>
<option value="FEMALE">Female</option>
<option value="OTHER">Other</option>
</select>
</div>
<div>
<label class="label">Civil Status</label>
<select v-model="form.civil_status" class="input">
<option value="SINGLE">Single</option>
<option value="MARRIED">Married</option>
<option value="WIDOWED">Widowed</option>
<option value="SEPARATED">Separated</option>
<option value="ANNULLED">Annulled</option>
</select>
</div>
<div>
<label class="label">Blood Type</label>
<select v-model="form.blood_type" class="input">
<option value=""></option>
<option v-for="bt in ['A+','A-','B+','B-','AB+','AB-','O+','O-']" :key="bt" :value="bt">{{ bt }}</option>
</select>
</div>
<div>
<label class="label">Occupation</label>
<input v-model="form.occupation" class="input" />
</div>
<div>
<label class="label">Monthly Income</label>
<input v-model="form.monthly_income" type="number" class="input" />
</div>
<div>
<label class="label">Purok</label>
<input v-model="form.purok" class="input" />
</div>
<div>
<label class="label">Street / Address</label>
<input v-model="form.street" class="input" />
</div>
<div class="col-span-2">
<label class="label">Emergency Contact</label>
<input v-model="form.emergency_contact_name" class="input" placeholder="Name" />
</div>
<div>
<input v-model="form.emergency_contact_phone" class="input" placeholder="Phone number" />
</div>
<div>
<input v-model="form.emergency_contact_address" class="input" placeholder="Address" />
</div>
</div>
<div class="flex justify-end gap-2 mt-4">
<button @click="showModal = false" class="btn-secondary">Cancel</button>
<button @click="saveResident" class="btn-primary">Save</button>
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,137 @@
<script setup>
import { ref, onMounted } from 'vue';
import { usePageTitle } from '../../composables/Core/usePageTitle';
import { executeRequest } from '../../utils/executeRequest.js';
import { navigate } from '../../utils/navigate.js';
usePageTitle('Request a Document');
const requestTypes = ref([]);
const myRequests = ref([]);
const loading = ref(false);
const submitting = ref(false);
const form = ref({ request_type_id: '', purpose: '' });
const selectedType = ref(null);
const submitted = ref(false);
const submittedDoc = ref(null);
const statusLabels = {
DRAFT: 'Draft', PENDING_PAYMENT: 'Pending Payment', PAID: 'Paid',
PROCESSING: 'Processing', READY: 'Ready for Pickup', CLAIMED: 'Claimed', CANCELLED: 'Cancelled',
};
const loadData = async () => {
loading.value = true;
const [typesRes, myRes] = await Promise.all([
executeRequest('/request-types'),
executeRequest('/documents/my'),
]);
if (typesRes.success) requestTypes.value = typesRes.data;
if (myRes.success) myRequests.value = myRes.data;
loading.value = false;
};
const onTypeSelect = () => {
selectedType.value = requestTypes.value.find(t => t.id == form.value.request_type_id);
};
const submit = async () => {
submitting.value = true;
const res = await executeRequest('/documents/submit', 'POST', form.value);
if (res.success) {
submittedDoc.value = res.data;
submitted.value = true;
loadData();
}
submitting.value = false;
};
const resetForm = () => {
submitted.value = false;
submittedDoc.value = null;
form.value = { request_type_id: '', purpose: '' };
selectedType.value = null;
};
const getStatus = (r) => r.status?.value ?? r.status;
onMounted(loadData);
</script>
<template>
<div class="p-4 max-w-3xl mx-auto">
<div class="mb-6">
<h1 class="text-2xl font-bold">Request a Document</h1>
<p class="text-gray-500 text-sm">Submit a request for a barangay certificate or clearance.</p>
</div>
<div v-if="loading" class="text-center py-8 text-gray-400">Loading...</div>
<div v-else-if="!submitted">
<div class="bg-white rounded-xl shadow p-5 mb-6">
<h2 class="font-semibold text-gray-700 mb-3">Select Document Type</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-4">
<div
v-for="t in requestTypes"
:key="t.id"
@click="form.request_type_id = t.id; onTypeSelect()"
:class="`border rounded-lg p-3 cursor-pointer transition ${form.request_type_id == t.id ? 'border-blue-500 bg-blue-50' : 'border-gray-200 hover:border-gray-300'}`"
>
<div class="font-medium text-sm">{{ t.name }}</div>
<div class="text-xs text-gray-500 mt-1">{{ t.description }}</div>
<div class="mt-2 flex justify-between text-xs">
<span class="font-semibold text-green-600">{{ t.base_fee > 0 ? `${t.base_fee}` : 'Free' }}</span>
<span class="text-gray-400">{{ t.processing_days }} day(s)</span>
</div>
</div>
</div>
<div v-if="selectedType" class="mt-2">
<label class="label">Purpose / Reason *</label>
<textarea v-model="form.purpose" class="input h-20" :placeholder="`State your purpose for requesting ${selectedType.name}...`"></textarea>
</div>
<div class="flex justify-end mt-4">
<button
@click="submit"
:disabled="!form.request_type_id || !form.purpose || submitting"
class="btn-primary disabled:opacity-50 disabled:cursor-not-allowed"
>
{{ submitting ? 'Submitting...' : 'Submit Request' }}
</button>
</div>
</div>
</div>
<div v-else class="bg-white rounded-xl shadow p-5 mb-6">
<div class="text-center">
<div class="text-4xl mb-2"></div>
<h2 class="text-xl font-bold text-green-600">Request Submitted!</h2>
<p class="text-gray-600 mt-1">Request No: <span class="font-mono font-bold">{{ submittedDoc?.request_no }}</span></p>
<p class="text-gray-500 text-sm mt-2">
{{ submittedDoc?.fee_amount > 0 ? `Please proceed to the barangay hall to pay ₱${submittedDoc.fee_amount}.` : 'Your request is free and will be processed shortly.' }}
</p>
<button @click="resetForm" class="btn-primary mt-4">Request Another</button>
</div>
</div>
<!-- My Requests -->
<div class="bg-white rounded-xl shadow p-5">
<h2 class="font-semibold text-gray-700 mb-3">My Requests</h2>
<div v-if="myRequests.length">
<div v-for="r in myRequests" :key="r.id" class="border-b py-3 flex justify-between items-center">
<div>
<div class="font-medium text-sm">{{ r.request_type?.name }}</div>
<div class="text-xs text-gray-400 font-mono">{{ r.request_no }}</div>
<div class="text-xs text-gray-500 mt-0.5">{{ r.created_at?.slice(0, 10) }}</div>
</div>
<span :class="`px-2 py-1 rounded-full text-xs font-medium ${getStatus(r) === 'CLAIMED' ? 'bg-green-100 text-green-700' : getStatus(r) === 'READY' ? 'bg-teal-100 text-teal-700' : 'bg-gray-100 text-gray-700'}`">
{{ statusLabels[getStatus(r)] ?? getStatus(r) }}
</span>
</div>
</div>
<p v-else class="text-gray-400 text-sm">No previous requests.</p>
</div>
</div>
</template>

View File

@@ -0,0 +1,238 @@
<script setup>
import { ref, onMounted, computed } from 'vue';
import { usePageTitle } from '../../composables/Core/usePageTitle';
import { executeRequest } from '../../utils/executeRequest.js';
import { navigate } from '../../utils/navigate.js';
usePageTitle('Resident Profile');
const resident = ref(null);
const loading = ref(false);
const editMode = ref(false);
const form = ref({});
const urlParams = new URLSearchParams(window.location.search);
const target = urlParams.get('target');
const statusColor = computed(() => {
if (!resident.value) return '';
return resident.value.is_active ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700';
});
const loadProfile = async () => {
loading.value = true;
const res = await executeRequest('/residents/show', 'POST', { target });
if (res.success) {
resident.value = res.data;
form.value = { ...res.data };
}
loading.value = false;
};
const startEdit = () => {
form.value = { ...resident.value };
editMode.value = true;
};
const cancelEdit = () => { editMode.value = false; };
const saveEdit = async () => {
const res = await executeRequest('/residents/update', 'POST', { target, ...form.value });
if (res.success) {
resident.value = res.data ?? { ...resident.value, ...form.value };
editMode.value = false;
loadProfile();
}
};
const fullname = computed(() => {
if (!resident.value) return '';
const r = resident.value;
return [r.first_name, r.middle_name, r.last_name, r.suffix].filter(Boolean).join(' ');
});
onMounted(loadProfile);
</script>
<template>
<div class="p-4 max-w-3xl mx-auto">
<button @click="navigate('/barangay/manageresidents')" class="text-sm text-blue-500 mb-4 inline-flex items-center gap-1">
Back to Residents
</button>
<div v-if="loading" class="text-center py-8 text-gray-400">Loading...</div>
<div v-else-if="resident">
<!-- Header -->
<div class="bg-white rounded-xl shadow p-5 mb-4">
<div class="flex justify-between items-start">
<div>
<h1 class="text-2xl font-bold text-gray-800">{{ fullname }}</h1>
<div class="flex items-center gap-2 mt-1">
<span :class="`text-xs px-2 py-0.5 rounded-full font-medium ${statusColor}`">
{{ resident.is_active ? 'Active' : 'Inactive' }}
</span>
<span v-if="resident.voter_status" class="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full">Registered Voter</span>
</div>
<p class="text-gray-500 text-sm mt-1">Purok {{ resident.purok ?? '—' }}</p>
</div>
<div class="flex gap-2">
<button v-if="!editMode" @click="startEdit" class="btn-secondary">Edit</button>
<template v-else>
<button @click="cancelEdit" class="btn-secondary">Cancel</button>
<button @click="saveEdit" class="btn-primary">Save</button>
</template>
</div>
</div>
</div>
<!-- View Mode -->
<div v-if="!editMode" class="space-y-4">
<div class="bg-white rounded-xl shadow p-5">
<h2 class="font-semibold text-gray-700 mb-3 border-b pb-2">Personal Information</h2>
<div class="grid grid-cols-2 gap-3 text-sm">
<div><span class="text-gray-400">Date of Birth:</span> {{ resident.date_of_birth ?? '—' }}</div>
<div><span class="text-gray-400">Gender:</span> {{ resident.gender ?? '—' }}</div>
<div><span class="text-gray-400">Civil Status:</span> {{ resident.civil_status ?? '—' }}</div>
<div><span class="text-gray-400">Blood Type:</span> {{ resident.blood_type ?? '—' }}</div>
<div><span class="text-gray-400">Nationality:</span> {{ resident.nationality ?? 'Filipino' }}</div>
<div><span class="text-gray-400">Religion:</span> {{ resident.religion ?? '—' }}</div>
<div class="col-span-2"><span class="text-gray-400">Address:</span> {{ resident.address ?? '—' }}</div>
<div><span class="text-gray-400">Mobile:</span> {{ resident.mobile_number ?? '—' }}</div>
<div><span class="text-gray-400">Email:</span> {{ resident.email ?? '—' }}</div>
<div><span class="text-gray-400">Occupation:</span> {{ resident.occupation ?? '—' }}</div>
<div><span class="text-gray-400">Educational Attainment:</span> {{ resident.educational_attainment ?? '—' }}</div>
</div>
</div>
<div class="bg-white rounded-xl shadow p-5">
<h2 class="font-semibold text-gray-700 mb-3 border-b pb-2">Government IDs & Voter Info</h2>
<div class="grid grid-cols-2 gap-3 text-sm">
<div><span class="text-gray-400">PhilHealth:</span> {{ resident.philhealth_id ?? '—' }}</div>
<div><span class="text-gray-400">SSS:</span> {{ resident.sss_id ?? '—' }}</div>
<div><span class="text-gray-400">GSIS:</span> {{ resident.gsis_id ?? '—' }}</div>
<div><span class="text-gray-400">TIN:</span> {{ resident.tin ?? '—' }}</div>
<div><span class="text-gray-400">Voter ID:</span> {{ resident.registered_voter_id ?? '—' }}</div>
<div><span class="text-gray-400">Voter Precinct:</span> {{ resident.voter_precinct ?? '—' }}</div>
</div>
</div>
<div class="bg-white rounded-xl shadow p-5">
<h2 class="font-semibold text-gray-700 mb-3 border-b pb-2">Emergency Contact</h2>
<div class="grid grid-cols-2 gap-3 text-sm">
<div><span class="text-gray-400">Name:</span> {{ resident.emergency_contact_name ?? '—' }}</div>
<div><span class="text-gray-400">Relationship:</span> {{ resident.emergency_contact_relationship ?? '—' }}</div>
<div class="col-span-2"><span class="text-gray-400">Mobile:</span> {{ resident.emergency_contact_mobile ?? '—' }}</div>
</div>
</div>
</div>
<!-- Edit Mode -->
<div v-else class="bg-white rounded-xl shadow p-5 space-y-4">
<h2 class="font-semibold text-gray-700 border-b pb-2">Edit Resident Information</h2>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="label">First Name *</label>
<input v-model="form.first_name" class="input" />
</div>
<div>
<label class="label">Middle Name</label>
<input v-model="form.middle_name" class="input" />
</div>
<div>
<label class="label">Last Name *</label>
<input v-model="form.last_name" class="input" />
</div>
<div>
<label class="label">Suffix</label>
<input v-model="form.suffix" class="input" placeholder="Jr., Sr., III" />
</div>
<div>
<label class="label">Date of Birth</label>
<input v-model="form.date_of_birth" type="date" class="input" />
</div>
<div>
<label class="label">Gender</label>
<select v-model="form.gender" class="input">
<option value="">Select</option>
<option>MALE</option>
<option>FEMALE</option>
</select>
</div>
<div>
<label class="label">Civil Status</label>
<select v-model="form.civil_status" class="input">
<option value="">Select</option>
<option>SINGLE</option>
<option>MARRIED</option>
<option>WIDOWED</option>
<option>SEPARATED</option>
<option>ANNULLED</option>
</select>
</div>
<div>
<label class="label">Blood Type</label>
<select v-model="form.blood_type" class="input">
<option value="">Unknown</option>
<option v-for="bt in ['A+','A-','B+','B-','AB+','AB-','O+','O-']" :key="bt" :value="bt">{{ bt }}</option>
</select>
</div>
<div>
<label class="label">Mobile Number</label>
<input v-model="form.mobile_number" class="input" />
</div>
<div>
<label class="label">Email</label>
<input v-model="form.email" type="email" class="input" />
</div>
<div>
<label class="label">Purok</label>
<input v-model="form.purok" class="input" />
</div>
<div>
<label class="label">Occupation</label>
<input v-model="form.occupation" class="input" />
</div>
<div class="col-span-2">
<label class="label">Address</label>
<input v-model="form.address" class="input" />
</div>
<div>
<label class="label">PhilHealth ID</label>
<input v-model="form.philhealth_id" class="input" />
</div>
<div>
<label class="label">SSS No.</label>
<input v-model="form.sss_id" class="input" />
</div>
<div>
<label class="label">TIN</label>
<input v-model="form.tin" class="input" />
</div>
<div>
<label class="label">Voter ID</label>
<input v-model="form.registered_voter_id" class="input" />
</div>
<div>
<label class="label">Emergency Contact Name</label>
<input v-model="form.emergency_contact_name" class="input" />
</div>
<div>
<label class="label">Emergency Contact Mobile</label>
<input v-model="form.emergency_contact_mobile" class="input" />
</div>
<div class="col-span-2">
<label class="label">Emergency Contact Relationship</label>
<input v-model="form.emergency_contact_relationship" class="input" />
</div>
</div>
<div class="flex justify-end gap-2 pt-2">
<button @click="cancelEdit" class="btn-secondary">Cancel</button>
<button @click="saveEdit" class="btn-primary">Save Changes</button>
</div>
</div>
</div>
<p v-else class="text-center text-gray-400 py-8">Resident not found.</p>
</div>
</template>