Files
Verde-Web/resources/views/admin/users.blade.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: '&copy; 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