feat: implement barangay system phases 2-14
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:
152
resources/js/Pages/Barangay/BlotterDetail.vue
Normal file
152
resources/js/Pages/Barangay/BlotterDetail.vue
Normal 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>
|
||||
178
resources/js/Pages/Barangay/BudgetLedger.vue
Normal file
178
resources/js/Pages/Barangay/BudgetLedger.vue
Normal 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>
|
||||
188
resources/js/Pages/Barangay/DocumentRequestDetail.vue
Normal file
188
resources/js/Pages/Barangay/DocumentRequestDetail.vue
Normal 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>
|
||||
180
resources/js/Pages/Barangay/ManageBlotters.vue
Normal file
180
resources/js/Pages/Barangay/ManageBlotters.vue
Normal 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>
|
||||
113
resources/js/Pages/Barangay/ManageDocumentRequests.vue
Normal file
113
resources/js/Pages/Barangay/ManageDocumentRequests.vue
Normal 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>
|
||||
212
resources/js/Pages/Barangay/ManageHouseholds.vue
Normal file
212
resources/js/Pages/Barangay/ManageHouseholds.vue
Normal 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">×</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>
|
||||
193
resources/js/Pages/Barangay/ManageProjects.vue
Normal file
193
resources/js/Pages/Barangay/ManageProjects.vue
Normal 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>
|
||||
116
resources/js/Pages/Barangay/ManageRequestTypes.vue
Normal file
116
resources/js/Pages/Barangay/ManageRequestTypes.vue
Normal 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>
|
||||
213
resources/js/Pages/Barangay/ManageResidents.vue
Normal file
213
resources/js/Pages/Barangay/ManageResidents.vue
Normal 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>
|
||||
137
resources/js/Pages/Barangay/RequestDocument.vue
Normal file
137
resources/js/Pages/Barangay/RequestDocument.vue
Normal 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>
|
||||
238
resources/js/Pages/Barangay/ResidentProfile.vue
Normal file
238
resources/js/Pages/Barangay/ResidentProfile.vue
Normal 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>
|
||||
Reference in New Issue
Block a user