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
213 lines
9.4 KiB
Vue
213 lines
9.4 KiB
Vue
<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>
|