initial: standalone repo from monorepo split

This commit is contained in:
kevin-asprec
2026-04-13 09:36:37 +08:00
commit 3f5bf0e118
88 changed files with 10997 additions and 0 deletions

View File

@@ -0,0 +1,162 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { api } from '@/lib/api';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { DataTable } from '@/components/ui/data-table';
import { ActionIcon } from '@/components/ui/action-icon';
import { Modal } from '@/components/ui/modal';
import { FormModal } from '@/components/ui/form-modal';
import { useToast } from '@/components/ui/toast';
interface Area {
id: string;
name: string;
description: string | null;
isActive: boolean;
_count: { clients: number };
}
export default function AreasPage() {
const { toast } = useToast();
const [areas, setAreas] = useState<Area[]>([]);
const [loading, setLoading] = useState(true);
const [showCreate, setShowCreate] = useState(false);
const [deleteTarget, setDeleteTarget] = useState<Area | null>(null);
const [deleting, setDeleting] = useState(false);
const [search, setSearch] = useState('');
const loadAreas = useCallback(async () => {
try {
const res = await api.get<{ data: Area[] }>('/areas');
setAreas(res.data.data);
} catch {
toast('Failed to load areas', 'error');
} finally {
setLoading(false);
}
}, [toast]);
useEffect(() => { loadAreas(); }, [loadAreas]);
async function handleDelete() {
if (!deleteTarget) return;
setDeleting(true);
try {
await api.delete(`/areas/${deleteTarget.id}`);
toast(`Area "${deleteTarget.name}" deleted`, 'success');
setDeleteTarget(null);
loadAreas();
} catch (err: any) {
toast(err.response?.data?.error || 'Failed to delete area', 'error');
} finally {
setDeleting(false);
}
}
const filtered = search
? areas.filter((a) => a.name.toLowerCase().includes(search.toLowerCase()) || (a.description || '').toLowerCase().includes(search.toLowerCase()))
: areas;
return (
<div>
<div className="flex items-center justify-between mb-5">
<p className="text-sm text-surface-500">Define service areas and zones for client assignment</p>
<Button onClick={() => setShowCreate(!showCreate)} variant={showCreate ? 'secondary' : 'primary'}>
{showCreate ? 'Cancel' : 'Add Area'}
</Button>
</div>
<DataTable
data={filtered}
loading={loading}
keyExtractor={(a) => a.id}
emptyTitle="No areas yet"
emptyDescription="Create your first service area to start organizing clients by location."
searchPlaceholder="Search areas..."
searchValue={search}
onSearchChange={setSearch}
columns={[
{ key: 'name', label: 'Name', sortable: true, render: (a: Area) => <span className="font-medium text-surface-800 dark:text-surface-200">{a.name}</span> },
{ key: 'description', label: 'Description', render: (a: Area) => <span className="text-sm text-surface-500 dark:text-surface-400">{a.description || '—'}</span> },
{ key: 'isActive', label: 'Status', sortable: true, render: (a: Area) => <Badge label={a.isActive ? 'Active' : 'Inactive'} variant={a.isActive ? 'success' : 'error'} /> },
{ key: 'clients', label: 'Clients', align: 'right' as const, render: (a: Area) => <span className="text-sm text-surface-600 dark:text-surface-300">{a._count.clients}</span> },
{
key: 'actions', label: '', align: 'right',
render: (a: Area) => (
<div className="flex justify-end" onClick={(e) => e.stopPropagation()}>
{a._count.clients === 0 && (
<ActionIcon icon="trash" variant="danger" label="Delete" onClick={() => setDeleteTarget(a)} />
)}
</div>
),
},
]}
/>
<Modal
open={!!deleteTarget}
onClose={() => setDeleteTarget(null)}
title="Delete Area"
description={`Are you sure you want to delete "${deleteTarget?.name}"? This action cannot be undone.`}
variant="danger"
confirmLabel="Delete Area"
onConfirm={handleDelete}
loading={deleting}
/>
{showCreate && (
<CreateAreaModal onCreated={() => { setShowCreate(false); loadAreas(); toast('Area created', 'success'); }} onClose={() => setShowCreate(false)} />
)}
</div>
);
}
function CreateAreaModal({ onCreated, onClose }: { onCreated: () => void; onClose: () => void }) {
const { toast } = useToast();
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [error, setError] = useState('');
const [submitting, setSubmitting] = useState(false);
const ic = 'mt-1 block w-full rounded-lg border border-surface-200 dark:border-surface-600 bg-white dark:bg-surface-800 px-3.5 py-2.5 text-sm text-surface-900 dark:text-surface-100 placeholder:text-surface-400 dark:placeholder:text-surface-500 focus:border-primary-500 dark:focus:border-primary-400 focus:outline-none focus:ring-2 focus:ring-primary-500/20 transition-all duration-200';
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError('');
setSubmitting(true);
try {
await api.post('/areas', { name, description: description || undefined });
onCreated();
} catch (err: any) {
setError(err.response?.data?.error || 'Failed to create area');
toast(err.response?.data?.error || 'Failed to create area', 'error');
} finally {
setSubmitting(false);
}
}
return (
<FormModal open onClose={onClose} title="Create Area" description="Define a new service area for client assignment">
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-2 rounded-lg text-sm" role="alert">{error}</div>
)}
<div>
<label htmlFor="area-name" className="block text-sm font-medium text-surface-700 dark:text-surface-300">Area Name</label>
<input id="area-name" type="text" required minLength={2} value={name} onChange={(e) => setName(e.target.value)}
className={ic} placeholder="e.g. Barangay 1 - Centro" />
</div>
<div>
<label htmlFor="area-desc" className="block text-sm font-medium text-surface-700 dark:text-surface-300">Description <span className="text-surface-400 dark:text-surface-500 font-normal">(optional)</span></label>
<input id="area-desc" type="text" value={description} onChange={(e) => setDescription(e.target.value)}
className={ic} placeholder="Description of the service area" />
</div>
<div className="flex justify-end gap-3 pt-2">
<Button type="button" variant="secondary" onClick={onClose}>Cancel</Button>
<Button type="submit" loading={submitting}>Create Area</Button>
</div>
</form>
</FormModal>
);
}