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,115 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { api } from '@/lib/api';
import { PageHeader } from '@/components/ui/page-header';
import { DataTable } from '@/components/ui/data-table';
import { Badge, statusBadgeVariant } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
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';
const CATEGORIES = ['router', 'olt', 'cable', 'tool', 'vehicle', 'computer', 'other'];
export default function AssetsPage() {
const { toast } = useToast();
const [assets, setAssets] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState('');
const [showCreate, setShowCreate] = useState(false);
const [detailTarget, setDetailTarget] = useState<any>(null);
const [catFilter, setCatFilter] = useState('');
const load = useCallback(async () => {
try {
const params = new URLSearchParams();
if (catFilter) params.set('category', catFilter);
const r = await api.get(`/assets?${params}`);
setAssets(r.data.data);
} catch { toast('Failed to load assets', 'error'); }
finally { setLoading(false); }
}, [catFilter, toast]);
useEffect(() => { load(); }, [load]);
const filtered = search ? assets.filter((a: any) => a.name.toLowerCase().includes(search.toLowerCase()) || a.serialNumber?.toLowerCase().includes(search.toLowerCase())) : assets;
return (
<div>
<PageHeader title="Assets" description="Track company equipment and inventory"
action={<div className="flex gap-2">
<select value={catFilter} onChange={(e) => setCatFilter(e.target.value)} aria-label="Filter category"
className="rounded-lg border border-surface-200 dark:border-surface-700 px-3 py-2 text-sm bg-white dark:bg-surface-800 text-surface-700 dark:text-surface-300 cursor-pointer focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20">
<option value="">All categories</option>
{CATEGORIES.map((c) => <option key={c} value={c}>{c.charAt(0).toUpperCase() + c.slice(1)}</option>)}
</select>
<Button onClick={() => setShowCreate(true)}>Add Asset</Button>
</div>} />
<div className="mt-5">
<DataTable data={filtered} loading={loading} keyExtractor={(a: any) => a.id}
emptyTitle="No assets" emptyDescription="Add your first equipment or tool."
searchPlaceholder="Search by name or serial number..." searchValue={search} onSearchChange={setSearch}
onRowClick={(a: any) => setDetailTarget(a)}
columns={[
{ key: 'name', label: 'Name', sortable: true, render: (a: any) => <span className="font-medium text-surface-800 dark:text-surface-200">{a.name}</span> },
{ key: 'category', label: 'Category', sortable: true, render: (a: any) => <Badge label={a.category} /> },
{ key: 'serialNumber', label: 'Serial #', render: (a: any) => <span className="font-mono text-surface-500 dark:text-surface-400 text-xs">{a.serialNumber || '—'}</span> },
{ key: 'status', label: 'Status', sortable: true, render: (a: any) => <Badge label={a.status} variant={statusBadgeVariant(a.status)} /> },
{ key: 'assignedTo', label: 'Assigned To', render: (a: any) => a.assignedTo ? <span className="text-surface-600 dark:text-surface-300">{a.assignedTo.firstName} {a.assignedTo.lastName}</span> : <span className="text-surface-300">Unassigned</span> },
{ key: 'purchasePrice', label: 'Value', align: 'right' as const, render: (a: any) => a.purchasePrice ? <span className="text-surface-700 dark:text-surface-300">PHP {Number(a.purchasePrice).toLocaleString()}</span> : <span className="text-surface-300"></span> },
]}
/>
</div>
<CreateAssetModal open={showCreate} onClose={() => setShowCreate(false)} onSuccess={load} />
{/* Asset Detail Modal */}
<Modal open={!!detailTarget} onClose={() => setDetailTarget(null)}
title={detailTarget?.name || 'Asset Details'} description={detailTarget?.serialNumber ? `S/N: ${detailTarget.serialNumber}` : ''}>
{detailTarget && (
<div className="space-y-3 text-sm">
<div className="grid grid-cols-2 gap-3">
<div><span className="text-surface-500 dark:text-surface-400">Category:</span> <Badge label={detailTarget.category} /></div>
<div><span className="text-surface-500 dark:text-surface-400">Status:</span> <Badge label={detailTarget.status} variant={statusBadgeVariant(detailTarget.status)} /></div>
<div><span className="text-surface-500 dark:text-surface-400">Serial #:</span> <span className="font-mono text-surface-700 dark:text-surface-300">{detailTarget.serialNumber || '—'}</span></div>
<div><span className="text-surface-500 dark:text-surface-400">Value:</span> <span className="text-surface-900 dark:text-surface-200 font-medium">{detailTarget.purchasePrice ? `PHP ${Number(detailTarget.purchasePrice).toLocaleString()}` : '—'}</span></div>
<div className="col-span-2"><span className="text-surface-500 dark:text-surface-400">Assigned To:</span> <span className="text-surface-700 dark:text-surface-300">{detailTarget.assignedTo ? `${detailTarget.assignedTo.firstName} ${detailTarget.assignedTo.lastName}` : 'Unassigned'}</span></div>
{detailTarget.location && <div className="col-span-2"><span className="text-surface-500 dark:text-surface-400">Location:</span> <span className="text-surface-700 dark:text-surface-300">{detailTarget.location}</span></div>}
{detailTarget.notes && <div className="col-span-2"><span className="text-surface-500 dark:text-surface-400">Notes:</span> <span className="text-surface-600 dark:text-surface-300">{detailTarget.notes}</span></div>}
</div>
<div className="flex justify-end pt-2 border-t border-surface-200 dark:border-surface-700">
<Button size="sm" variant="ghost" onClick={() => setDetailTarget(null)}>Close</Button>
</div>
</div>
)}
</Modal>
</div>
);
}
function CreateAssetModal({ open, onClose, onSuccess }: { open: boolean; onClose: () => void; onSuccess: () => void }) {
const { toast } = useToast();
const [form, setForm] = useState({ name: '', category: 'router', serialNumber: '', purchasePrice: 0, location: '', notes: '' });
const [submitting, setSubmitting] = useState(false);
const ic = 'mt-1 block w-full rounded-lg border border-surface-200 dark:border-surface-700 px-3.5 py-2.5 text-sm bg-white dark:bg-surface-800 text-surface-900 dark:text-surface-200 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20 transition-all duration-200';
useEffect(() => { if (open) setForm({ name: '', category: 'router', serialNumber: '', purchasePrice: 0, location: '', notes: '' }); }, [open]);
async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setSubmitting(true);
try { await api.post('/assets', { ...form, serialNumber: form.serialNumber || undefined, purchasePrice: form.purchasePrice || undefined, location: form.location || undefined, notes: form.notes || undefined }); toast('Asset added', 'success'); onSuccess(); onClose(); }
catch (err: any) { toast(err.response?.data?.error || 'Failed', 'error'); } finally { setSubmitting(false); } }
return (
<FormModal open={open} onClose={onClose} title="Add Asset"><form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div><label className="block text-sm font-medium text-surface-700 dark:text-surface-300">Name</label><input type="text" required value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} className={ic} placeholder="e.g. Mikrotik hEX S" /></div>
<div><label className="block text-sm font-medium text-surface-700 dark:text-surface-300">Category</label><select value={form.category} onChange={(e) => setForm({ ...form, category: e.target.value })} className={ic}>{CATEGORIES.map((c) => <option key={c} value={c}>{c.charAt(0).toUpperCase() + c.slice(1)}</option>)}</select></div>
</div>
<div className="grid grid-cols-2 gap-4">
<div><label className="block text-sm font-medium text-surface-700 dark:text-surface-300">Serial Number</label><input type="text" value={form.serialNumber} onChange={(e) => setForm({ ...form, serialNumber: e.target.value })} className={ic} /></div>
<div><label className="block text-sm font-medium text-surface-700 dark:text-surface-300">Purchase Price (PHP)</label><input type="number" min={0} value={form.purchasePrice || ''} onChange={(e) => setForm({ ...form, purchasePrice: parseFloat(e.target.value) || 0 })} className={ic} /></div>
</div>
<div><label className="block text-sm font-medium text-surface-700 dark:text-surface-300">Location</label><input type="text" value={form.location} onChange={(e) => setForm({ ...form, location: e.target.value })} className={ic} placeholder="e.g. Warehouse, Field" /></div>
<div className="flex justify-end gap-3 pt-2"><Button type="button" variant="secondary" onClick={onClose}>Cancel</Button><Button type="submit" loading={submitting}>Add Asset</Button></div>
</form></FormModal>);
}