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
195 lines
8.2 KiB
Vue
195 lines
8.2 KiB
Vue
<script setup>
|
|
import { ref, onMounted } from 'vue';
|
|
import { usePageTitle } from '../composables/Core/usePageTitle';
|
|
import { executeRequest } from '../utils/executeRequest.js';
|
|
|
|
usePageTitle('Admin Console');
|
|
|
|
const stats = ref({});
|
|
const logs = ref([]);
|
|
const queryResult = ref(null);
|
|
const queryError = ref('');
|
|
const activeTab = ref('stats');
|
|
const loading = ref(false);
|
|
|
|
const sqlQuery = ref('');
|
|
const maintenanceMode = ref(false);
|
|
const globalMessage = ref('');
|
|
|
|
const tabs = [
|
|
{ key: 'stats', label: 'Statistics' },
|
|
{ key: 'logs', label: 'Activity Logs' },
|
|
{ key: 'query', label: 'SQL Query' },
|
|
{ key: 'maintenance', label: 'Maintenance' },
|
|
];
|
|
|
|
const loadStats = async () => {
|
|
const res = await executeRequest('/admin/console/stats');
|
|
if (res.success) stats.value = res.data;
|
|
};
|
|
|
|
const loadLogs = async () => {
|
|
loading.value = true;
|
|
const res = await executeRequest('/admin/console/logs');
|
|
if (res.success) logs.value = res.data;
|
|
loading.value = false;
|
|
};
|
|
|
|
const runQuery = async () => {
|
|
if (!sqlQuery.value.trim()) return;
|
|
queryError.value = '';
|
|
queryResult.value = null;
|
|
const res = await executeRequest('/admin/console/query', 'POST', { query: sqlQuery.value });
|
|
if (res.success) {
|
|
queryResult.value = res.data;
|
|
} else {
|
|
queryError.value = res.message ?? 'Query failed.';
|
|
}
|
|
};
|
|
|
|
const clearCache = async () => {
|
|
if (!confirm('Clear all application cache?')) return;
|
|
const res = await executeRequest('/admin/console/cache/clear', 'POST');
|
|
alert(res.message ?? 'Done.');
|
|
};
|
|
|
|
const setMaintenance = async (val) => {
|
|
await executeRequest('/admin/console/maintenance', 'POST', { enabled: val });
|
|
maintenanceMode.value = val;
|
|
};
|
|
|
|
const setGlobalMessage = async () => {
|
|
const res = await executeRequest('/admin/console/message', 'POST', { message: globalMessage.value });
|
|
if (res.success) alert('Global message updated.');
|
|
};
|
|
|
|
const queryColumns = () => {
|
|
if (!queryResult.value || !queryResult.value.length) return [];
|
|
return Object.keys(queryResult.value[0]);
|
|
};
|
|
|
|
onMounted(() => {
|
|
loadStats();
|
|
});
|
|
|
|
const onTabChange = (tab) => {
|
|
activeTab.value = tab;
|
|
if (tab === 'logs') loadLogs();
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div class="p-4 max-w-6xl mx-auto">
|
|
<h1 class="text-2xl font-bold mb-1">Admin Console</h1>
|
|
<p class="text-gray-500 text-sm mb-4">System administration and diagnostics</p>
|
|
|
|
<!-- Tabs -->
|
|
<div class="flex border-b mb-5">
|
|
<button
|
|
v-for="tab in tabs" :key="tab.key"
|
|
@click="onTabChange(tab.key)"
|
|
:class="`px-4 py-2 text-sm font-medium border-b-2 -mb-px transition ${activeTab === tab.key ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700'}`">
|
|
{{ tab.label }}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Statistics -->
|
|
<div v-if="activeTab === 'stats'">
|
|
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3 mb-6">
|
|
<div v-for="(val, key) in stats" :key="key" class="bg-white rounded-lg shadow p-4 text-center">
|
|
<div class="text-3xl font-bold text-blue-600">{{ val }}</div>
|
|
<div class="text-xs text-gray-500 mt-1 capitalize">{{ key.replace(/_/g, ' ') }}</div>
|
|
</div>
|
|
</div>
|
|
<button @click="loadStats" class="btn-secondary text-sm">Refresh</button>
|
|
</div>
|
|
|
|
<!-- Logs -->
|
|
<div v-else-if="activeTab === 'logs'">
|
|
<div v-if="loading" class="text-center py-8 text-gray-400">Loading...</div>
|
|
<div v-else class="bg-white rounded-lg shadow overflow-hidden">
|
|
<table class="w-full text-sm">
|
|
<thead class="bg-gray-50 text-gray-600 text-left">
|
|
<tr>
|
|
<th class="px-4 py-2">Time</th>
|
|
<th class="px-4 py-2">User</th>
|
|
<th class="px-4 py-2">Action</th>
|
|
<th class="px-4 py-2">Details</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="log in logs" :key="log.id" class="border-t hover:bg-gray-50">
|
|
<td class="px-4 py-2 text-gray-400 whitespace-nowrap">{{ log.created_at }}</td>
|
|
<td class="px-4 py-2">{{ log.user?.username ?? log.user_id }}</td>
|
|
<td class="px-4 py-2 font-mono text-xs">{{ log.action }}</td>
|
|
<td class="px-4 py-2 text-gray-500 max-w-xs truncate">{{ log.details }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<p v-if="!logs.length" class="text-center text-gray-400 py-6">No logs found.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- SQL Query -->
|
|
<div v-else-if="activeTab === 'query'">
|
|
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-3 mb-4 text-sm text-yellow-800">
|
|
Only SELECT queries are permitted. This console is for diagnostics only.
|
|
</div>
|
|
<textarea
|
|
v-model="sqlQuery"
|
|
class="input font-mono text-sm h-32 mb-3"
|
|
placeholder="SELECT * FROM barangay_residents LIMIT 10;"
|
|
></textarea>
|
|
<button @click="runQuery" class="btn-primary mb-4">Run Query</button>
|
|
|
|
<div v-if="queryError" class="bg-red-50 border border-red-200 rounded p-3 text-red-700 text-sm mb-3">
|
|
{{ queryError }}
|
|
</div>
|
|
|
|
<div v-if="queryResult" class="bg-white rounded-lg shadow overflow-x-auto">
|
|
<div class="text-xs text-gray-400 px-4 pt-3">{{ queryResult.length }} row(s)</div>
|
|
<table class="w-full text-xs">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th v-for="col in queryColumns()" :key="col" class="px-3 py-2 text-left text-gray-600 font-medium">{{ col }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="(row, i) in queryResult" :key="i" class="border-t">
|
|
<td v-for="col in queryColumns()" :key="col" class="px-3 py-1.5 text-gray-700 max-w-xs truncate">
|
|
{{ row[col] ?? '—' }}
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<p v-if="!queryResult.length" class="text-center text-gray-400 py-4 text-sm">No rows returned.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Maintenance -->
|
|
<div v-else-if="activeTab === 'maintenance'" class="space-y-5">
|
|
<div class="bg-white rounded-lg shadow p-5">
|
|
<h3 class="font-semibold text-gray-700 mb-3">Maintenance Mode</h3>
|
|
<p class="text-sm text-gray-500 mb-3">Enabling maintenance mode will show a maintenance page to non-admin users.</p>
|
|
<div class="flex gap-2">
|
|
<button @click="setMaintenance(true)" class="btn-sm bg-red-500 text-white">Enable Maintenance</button>
|
|
<button @click="setMaintenance(false)" class="btn-sm bg-green-500 text-white">Disable Maintenance</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white rounded-lg shadow p-5">
|
|
<h3 class="font-semibold text-gray-700 mb-3">Global Message</h3>
|
|
<p class="text-sm text-gray-500 mb-3">Show a banner message to all users across the system.</p>
|
|
<textarea v-model="globalMessage" class="input h-20 mb-3" placeholder="System maintenance scheduled for..."></textarea>
|
|
<button @click="setGlobalMessage" class="btn-secondary">Set Global Message</button>
|
|
</div>
|
|
|
|
<div class="bg-white rounded-lg shadow p-5">
|
|
<h3 class="font-semibold text-gray-700 mb-3">Cache Management</h3>
|
|
<p class="text-sm text-gray-500 mb-3">Clear Redis cache and application cached data.</p>
|
|
<button @click="clearCache" class="btn-sm bg-orange-500 text-white">Clear All Cache</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|