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
181 lines
8.5 KiB
Vue
181 lines
8.5 KiB
Vue
<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>
|