chore: apply code review checklist and web design guidelines fixes
This commit is contained in:
@@ -9,14 +9,32 @@ use App\Http\Resources\UserDetailResource;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class AdminUserController extends ApiController
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'role' => ['nullable', 'in:admin,resident,driver,helper,scanner,store_partner'],
|
||||
'status' => ['nullable', 'in:active,suspended,pending'],
|
||||
'role' => [
|
||||
'nullable',
|
||||
Rule::in([
|
||||
User::ROLE_ADMIN,
|
||||
User::ROLE_RESIDENT,
|
||||
User::ROLE_DRIVER,
|
||||
User::ROLE_HELPER,
|
||||
User::ROLE_SCANNER,
|
||||
User::ROLE_STORE_PARTNER,
|
||||
])
|
||||
],
|
||||
'status' => [
|
||||
'nullable',
|
||||
Rule::in([
|
||||
User::STATUS_ACTIVE,
|
||||
User::STATUS_SUSPENDED,
|
||||
User::STATUS_PENDING,
|
||||
])
|
||||
],
|
||||
'verification_status' => ['nullable', 'in:pending,approved,rejected'],
|
||||
'q' => ['nullable', 'string', 'max:100'],
|
||||
'per_page' => ['nullable', 'integer', 'min:1', 'max:500'],
|
||||
|
||||
@@ -22,6 +22,7 @@ class DumpsiteResource extends JsonResource
|
||||
|
||||
return [
|
||||
'id' => $this->uuid,
|
||||
'db_id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'code' => $this->code,
|
||||
'address_line' => $this->address_line,
|
||||
|
||||
@@ -15,7 +15,7 @@ class SuperAdminSeeder extends Seeder
|
||||
['email' => 'super@verde.local'],
|
||||
[
|
||||
'phone' => '+639000000099',
|
||||
'password' => Hash::make('password'),
|
||||
'password' => Hash::make(env('SUPER_ADMIN_PASSWORD', 'password')),
|
||||
'first_name' => 'Verde',
|
||||
'last_name' => 'Super Admin',
|
||||
'role' => User::ROLE_SUPER_ADMIN,
|
||||
@@ -30,6 +30,6 @@ class SuperAdminSeeder extends Seeder
|
||||
$user->syncRoles([User::ROLE_SUPER_ADMIN]);
|
||||
NotificationPreference::firstOrCreate(['user_id' => $user->id]);
|
||||
|
||||
$this->command->info("Super admin: {$user->email} (password: password)");
|
||||
$this->command->info("Super admin: {$user->email} (password: ".env('SUPER_ADMIN_PASSWORD', 'password').")");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,11 +37,11 @@
|
||||
<div class="slide-over-panel translate-x-0 p-6 overflow-y-auto">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h3 class="text-base font-semibold text-neutral-900">New dumpsite</h3>
|
||||
<button data-close class="text-neutral-400 hover:text-neutral-600">
|
||||
<button data-close aria-label="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>
|
||||
<form id="create-form" class="space-y-4">
|
||||
<form id="create-form" class="space-y-4" autocomplete="off">
|
||||
<div><label class="form-label">Name</label><input name="name" required class="form-input"></div>
|
||||
<div><label class="form-label">Code</label><input name="code" required class="form-input" placeholder="DS-..."></div>
|
||||
<div><label class="form-label">Address</label><input name="address_line" required class="form-input"></div>
|
||||
@@ -83,7 +83,13 @@
|
||||
</div>
|
||||
<div class="flex justify-end gap-2 pt-2">
|
||||
<button type="button" data-close class="btn-ghost">Cancel</button>
|
||||
<button type="submit" class="btn-primary">Create</button>
|
||||
<button type="submit" id="submit-btn" class="btn-primary">
|
||||
<span class="btn-label">Create</span>
|
||||
<svg class="btn-spinner ml-2 hidden h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 0 1 8-8v4a4 4 0 0 0-4 4H4z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -125,7 +131,7 @@
|
||||
div.innerHTML = `
|
||||
<input type="number" step="0.000001" placeholder="lat" value="${lat}" class="form-input flex-1 boundary-lat" required>
|
||||
<input type="number" step="0.000001" placeholder="lng" value="${lng}" class="form-input flex-1 boundary-lng" required>
|
||||
<button type="button" class="btn-ghost px-2 remove-point">×</button>
|
||||
<button type="button" class="btn-ghost px-2 remove-point" aria-label="Remove point">×</button>
|
||||
`;
|
||||
div.querySelector('.remove-point').addEventListener('click', () => div.remove());
|
||||
pointsEl.appendChild(div);
|
||||
@@ -285,6 +291,15 @@
|
||||
|
||||
document.getElementById('create-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const submitBtn = document.getElementById('submit-btn');
|
||||
const btnLabel = submitBtn.querySelector('.btn-label');
|
||||
const btnSpinner = submitBtn.querySelector('.btn-spinner');
|
||||
|
||||
submitBtn.disabled = true;
|
||||
btnLabel.textContent = 'Creating…';
|
||||
btnSpinner.classList.remove('hidden');
|
||||
|
||||
const fd = new FormData(e.target);
|
||||
const payload = Object.fromEntries(fd.entries());
|
||||
payload.lat = parseFloat(payload.lat);
|
||||
@@ -299,6 +314,11 @@
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
submitBtn.disabled = false;
|
||||
btnLabel.textContent = 'Create';
|
||||
btnSpinner.classList.add('hidden');
|
||||
|
||||
if (res.ok) { window.Verde.toast('Dumpsite created', 'success'); modal.classList.add('hidden'); e.target.reset(); load(); }
|
||||
else window.Verde.toast(res.body?.message ?? 'Create failed', 'error');
|
||||
});
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
if (!res.ok) return;
|
||||
(res.body.data ?? []).forEach(d => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = d.id;
|
||||
opt.value = d.db_id;
|
||||
opt.textContent = d.name;
|
||||
dumpsiteSelect.appendChild(opt);
|
||||
});
|
||||
@@ -115,7 +115,7 @@
|
||||
function renderPicker() {
|
||||
if (allDops.length === 0) { stopsPicker.innerHTML = '<div class="text-neutral-400">No active DOPs found.</div>'; return; }
|
||||
stopsPicker.innerHTML = allDops.map(d => `
|
||||
<button type="button" data-id="${d.id}" data-name="${window.Verde.escapeHtml(d.name)}"
|
||||
<button type="button" data-id="${d.db_id}" data-name="${window.Verde.escapeHtml(d.name)}"
|
||||
class="dop-pick block w-full rounded px-2 py-1 text-left transition hover:bg-white">
|
||||
<span class="font-mono text-[10px] text-neutral-500">${window.Verde.escapeHtml(d.code)}</span>
|
||||
<span class="ml-2">${window.Verde.escapeHtml(d.name)}</span>
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
type="email"
|
||||
required
|
||||
autocomplete="email"
|
||||
spellcheck="false"
|
||||
autofocus
|
||||
class="form-input"
|
||||
placeholder="you@verde.local">
|
||||
@@ -78,7 +79,7 @@
|
||||
placeholder="••••••••">
|
||||
</div>
|
||||
|
||||
<div id="error-banner" class="hidden rounded-md border border-red-200 bg-red-50 px-3 py-2.5 text-sm text-red-700"></div>
|
||||
<div id="error-banner" aria-live="polite" class="hidden rounded-md border border-red-200 bg-red-50 px-3 py-2.5 text-sm text-red-700"></div>
|
||||
|
||||
<button type="submit" id="submit-btn" class="btn-primary w-full">
|
||||
<span class="btn-label">Sign in</span>
|
||||
|
||||
Reference in New Issue
Block a user