326 lines
15 KiB
PHP
326 lines
15 KiB
PHP
@extends('admin.layouts.app', ['pageTitle' => 'All Users'])
|
|
|
|
@section('page')
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin="">
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin=""></script>
|
|
|
|
<div class="mx-auto max-w-7xl">
|
|
<header class="mb-6 flex items-end justify-between">
|
|
<div>
|
|
<h2 class="text-2xl font-semibold tracking-tight text-neutral-900">All Users</h2>
|
|
<p class="mt-1 text-sm text-neutral-500">Residents, drivers, helpers, scanners, partner stores, and admins.</p>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="card mb-4 flex flex-wrap items-center gap-3 p-4">
|
|
<select id="filter-role" class="form-select w-44">
|
|
<option value="">All roles</option>
|
|
<option value="super_admin">Super admin</option>
|
|
<option value="admin">Admin</option>
|
|
<option value="resident">Resident</option>
|
|
<option value="driver">Driver</option>
|
|
<option value="helper">Helper</option>
|
|
<option value="scanner">Scanner</option>
|
|
<option value="store_partner">Store partner</option>
|
|
</select>
|
|
<select id="filter-status" class="form-select w-40">
|
|
<option value="">All statuses</option>
|
|
<option value="active">Active</option>
|
|
<option value="pending">Pending</option>
|
|
<option value="suspended">Suspended</option>
|
|
</select>
|
|
<input id="filter-q" type="search" placeholder="Search email, phone, name…" class="form-input flex-1 min-w-[200px]">
|
|
<button id="filter-apply" class="btn-primary">Apply</button>
|
|
</div>
|
|
|
|
<div class="table-wrap">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Email</th>
|
|
<th>Phone</th>
|
|
<th>Role</th>
|
|
<th>Status</th>
|
|
<th class="text-right">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="rows">
|
|
<tr><td colspan="6" class="py-10 text-center text-sm text-neutral-400">Loading…</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Group into Household modal --}}
|
|
<div id="group-modal" class="hidden">
|
|
<div class="slide-over-mask" data-close></div>
|
|
<div class="slide-over-panel translate-x-0 p-6 overflow-y-auto" style="max-width: 600px; width: 90%;">
|
|
<div class="mb-4 flex items-center justify-between">
|
|
<h3 class="text-base font-semibold text-neutral-900">Group Resident into Household</h3>
|
|
<button data-close class="text-neutral-400 hover:text-neutral-600">
|
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
|
</button>
|
|
</div>
|
|
<p class="text-sm text-neutral-500 mb-4 font-medium">Resident: <span id="resident-name-display" class="font-bold text-neutral-900"></span></p>
|
|
<form id="group-form" class="space-y-4">
|
|
<div>
|
|
<label class="form-label">Grouping Mode</label>
|
|
<div class="flex gap-4 mt-2">
|
|
<label class="inline-flex items-center cursor-pointer">
|
|
<input type="radio" name="group_mode" value="existing" checked class="form-radio text-green-600 focus:ring-green-500">
|
|
<span class="ml-2 text-sm text-neutral-700">Existing Household</span>
|
|
</label>
|
|
<label class="inline-flex items-center cursor-pointer">
|
|
<input type="radio" name="group_mode" value="new" class="form-radio text-green-600 focus:ring-green-500">
|
|
<span class="ml-2 text-sm text-neutral-700">New Household</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Existing Household field -->
|
|
<div id="existing-household-field">
|
|
<label class="form-label">Select Household</label>
|
|
<select id="select-household-id" name="household_id" class="form-select w-full">
|
|
<option value="">— Select Household —</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- New Household fields (hidden by default) -->
|
|
<div id="new-household-fields" class="hidden space-y-4">
|
|
<div>
|
|
<label class="form-label">Address Line</label>
|
|
<input name="address_line" class="form-input" placeholder="e.g. Block 1 Lot 2, Street Name">
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Barangay</label>
|
|
<select name="barangay_id" id="barangay-select" class="form-select w-full">
|
|
<option value="">— Select Barangay —</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Household Size</label>
|
|
<input type="number" name="household_size" value="1" min="1" max="50" class="form-input">
|
|
</div>
|
|
<div>
|
|
<label class="form-label mb-1">Pin Location on Map</label>
|
|
<div id="picker-map" class="w-full rounded-md border border-neutral-200 bg-neutral-50 h-[220px]" style="z-index: 1;"></div>
|
|
<div class="mt-1 text-xs text-neutral-400">Click on the map to set the household pin coordinate.</div>
|
|
</div>
|
|
<input type="hidden" name="lat" id="picker-lat">
|
|
<input type="hidden" name="lng" id="picker-lng">
|
|
</div>
|
|
|
|
<div class="flex justify-end gap-2 pt-4 border-t border-neutral-100">
|
|
<button type="button" data-close class="btn-ghost">Cancel</button>
|
|
<button type="submit" class="btn-primary">Save Grouping</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script type="module">
|
|
const rows = document.getElementById('rows');
|
|
const groupModal = document.getElementById('group-modal');
|
|
const groupForm = document.getElementById('group-form');
|
|
const householdSelect = document.getElementById('select-household-id');
|
|
const barangaySelect = document.getElementById('barangay-select');
|
|
|
|
let groupMap = null;
|
|
let groupMarker = null;
|
|
let groupHouseholdsList = [];
|
|
let currentGroupingUserId = null;
|
|
|
|
function statusBadge(s) {
|
|
const m = { active: 'badge-active', pending: 'badge-pending', suspended: 'badge-rejected' };
|
|
return `<span class="badge ${m[s] || 'badge-inactive'}">${s}</span>`;
|
|
}
|
|
|
|
function initGroupMap() {
|
|
if (groupMap) return;
|
|
groupMap = L.map('picker-map').setView([12.8797, 121.7740], 6);
|
|
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© OpenStreetMap'
|
|
}).addTo(groupMap);
|
|
|
|
groupMap.on('click', (e) => {
|
|
const lat = e.latlng.lat;
|
|
const lng = e.latlng.lng;
|
|
document.getElementById('picker-lat').value = lat;
|
|
document.getElementById('picker-lng').value = lng;
|
|
|
|
if (groupMarker) {
|
|
groupMarker.setLatLng(e.latlng);
|
|
} else {
|
|
groupMarker = L.marker(e.latlng).addTo(groupMap);
|
|
}
|
|
});
|
|
}
|
|
|
|
document.querySelectorAll('input[name="group_mode"]').forEach(radio => {
|
|
radio.addEventListener('change', (e) => {
|
|
const isNew = e.target.value === 'new';
|
|
document.getElementById('existing-household-field').classList.toggle('hidden', isNew);
|
|
document.getElementById('new-household-fields').classList.toggle('hidden', !isNew);
|
|
if (isNew) {
|
|
initGroupMap();
|
|
setTimeout(() => {
|
|
groupMap.invalidateSize();
|
|
}, 100);
|
|
}
|
|
});
|
|
});
|
|
|
|
async function loadHouseholds() {
|
|
const res = await window.Verde.apiFetch('/api/v1/admin/households?per_page=100');
|
|
if (!res.ok) return;
|
|
groupHouseholdsList = res.body.data ?? [];
|
|
householdSelect.innerHTML = '<option value="">— Select Household —</option>';
|
|
groupHouseholdsList.forEach(h => {
|
|
const opt = document.createElement('option');
|
|
opt.value = h.id;
|
|
opt.textContent = `${h.head ? h.head.full_name : 'Household'} (${h.address_line})`;
|
|
householdSelect.appendChild(opt);
|
|
});
|
|
}
|
|
|
|
async function loadBarangays() {
|
|
const res = await window.Verde.apiFetch('/api/v1/geo/barangays?per_page=100');
|
|
if (!res.ok) return;
|
|
const barangays = res.body.data ?? [];
|
|
barangaySelect.innerHTML = '<option value="">— Select Barangay —</option>';
|
|
barangays.forEach(b => {
|
|
const opt = document.createElement('option');
|
|
opt.value = b.id;
|
|
opt.textContent = b.name;
|
|
barangaySelect.appendChild(opt);
|
|
});
|
|
}
|
|
|
|
async function load() {
|
|
rows.innerHTML = `<tr><td colspan="6" class="py-10 text-center text-sm text-neutral-400">Loading…</td></tr>`;
|
|
const params = new URLSearchParams();
|
|
const role = document.getElementById('filter-role').value;
|
|
const status = document.getElementById('filter-status').value;
|
|
const q = document.getElementById('filter-q').value.trim();
|
|
if (role) params.set('role', role);
|
|
if (status) params.set('status', status);
|
|
if (q) params.set('q', q);
|
|
params.set('per_page', '50');
|
|
|
|
const res = await window.Verde.apiFetch(`/api/v1/admin/users?${params}`);
|
|
if (!res.ok) {
|
|
rows.innerHTML = `<tr><td colspan="6" class="py-10 text-center text-sm text-red-500">Failed to load.</td></tr>`;
|
|
return;
|
|
}
|
|
const items = res.body.data ?? [];
|
|
if (items.length === 0) {
|
|
rows.innerHTML = `<tr><td colspan="6" class="py-10 text-center text-sm text-neutral-400">No users match.</td></tr>`;
|
|
return;
|
|
}
|
|
rows.innerHTML = items.map(u => {
|
|
let actions = '';
|
|
if (u.role !== 'admin' && u.role !== 'super_admin') {
|
|
if (u.status === 'active') {
|
|
actions += `<button data-action="suspend" data-id="${u.id}" class="btn-ghost px-3 py-1 text-xs">Suspend</button>`;
|
|
} else {
|
|
actions += `<button data-action="activate" data-id="${u.id}" class="btn-primary px-3 py-1 text-xs">Activate</button>`;
|
|
}
|
|
|
|
if (u.role === 'resident' && !u.has_household) {
|
|
actions += `<button data-action="group-household" data-id="${u.id}" data-name="${window.Verde.escapeHtml(u.full_name)}" class="btn-ghost text-green-600 hover:text-green-700 px-3 py-1 text-xs font-semibold">Group Household</button>`;
|
|
}
|
|
} else {
|
|
actions = '<span class="text-xs text-neutral-400">—</span>';
|
|
}
|
|
|
|
return `<tr>
|
|
<td class="font-medium text-neutral-900">${window.Verde.escapeHtml(u.full_name || '—')}</td>
|
|
<td class="text-neutral-700">${window.Verde.escapeHtml(u.email)}</td>
|
|
<td class="font-mono text-xs text-neutral-600">${window.Verde.escapeHtml(u.phone ?? '')}</td>
|
|
<td><span class="badge badge-info">${u.role}</span></td>
|
|
<td>${statusBadge(u.status)}</td>
|
|
<td class="text-right space-x-1">${actions}</td>
|
|
</tr>`;
|
|
}).join('');
|
|
|
|
rows.querySelectorAll('button[data-action]').forEach(btn => {
|
|
btn.addEventListener('click', async () => {
|
|
const action = btn.dataset.action;
|
|
const id = btn.dataset.id;
|
|
|
|
if (action === 'group-household') {
|
|
currentGroupingUserId = id;
|
|
document.getElementById('resident-name-display').textContent = btn.dataset.name;
|
|
groupModal.classList.remove('hidden');
|
|
loadHouseholds();
|
|
loadBarangays();
|
|
return;
|
|
}
|
|
|
|
const r = await window.Verde.apiFetch(`/api/v1/admin/users/${id}/${action}`, { method: 'POST' });
|
|
if (r.ok) { window.Verde.toast(`User ${action}d`, 'success'); load(); }
|
|
else window.Verde.toast(r.body?.message ?? 'Failed', 'error');
|
|
});
|
|
});
|
|
}
|
|
|
|
groupModal.querySelectorAll('[data-close]').forEach(el => el.addEventListener('click', () => {
|
|
groupModal.classList.add('hidden');
|
|
groupForm.reset();
|
|
document.getElementById('existing-household-field').classList.remove('hidden');
|
|
document.getElementById('new-household-fields').classList.add('hidden');
|
|
if (groupMarker) {
|
|
groupMarker.remove();
|
|
groupMarker = null;
|
|
}
|
|
}));
|
|
|
|
groupForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const fd = new FormData(e.target);
|
|
const mode = fd.get('group_mode');
|
|
const payload = {};
|
|
|
|
if (mode === 'existing') {
|
|
payload.household_id = parseInt(fd.get('household_id'), 10);
|
|
if (isNaN(payload.household_id)) {
|
|
window.Verde.toast('Please select a household', 'error');
|
|
return;
|
|
}
|
|
} else {
|
|
payload.address_line = fd.get('address_line');
|
|
payload.barangay_id = parseInt(fd.get('barangay_id'), 10);
|
|
payload.household_size = parseInt(fd.get('household_size'), 10);
|
|
payload.lat = parseFloat(fd.get('lat'));
|
|
payload.lng = parseFloat(fd.get('lng'));
|
|
|
|
if (!payload.address_line || isNaN(payload.barangay_id) || isNaN(payload.lat) || isNaN(payload.lng)) {
|
|
window.Verde.toast('Please fill in all household fields and pin the map location', 'error');
|
|
return;
|
|
}
|
|
}
|
|
|
|
const res = await window.Verde.apiFetch(`/api/v1/admin/users/${currentGroupingUserId}/group-into-household`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (res.ok) {
|
|
window.Verde.toast('Resident grouped into household successfully', 'success');
|
|
groupModal.classList.add('hidden');
|
|
groupForm.reset();
|
|
load();
|
|
} else {
|
|
window.Verde.toast(res.body?.message ?? 'Failed to group resident', 'error');
|
|
}
|
|
});
|
|
|
|
document.getElementById('filter-apply').addEventListener('click', load);
|
|
document.getElementById('filter-role').addEventListener('change', load);
|
|
document.getElementById('filter-status').addEventListener('change', load);
|
|
|
|
load();
|
|
</script>
|
|
@endsection
|