From 71f87a11be92f6f13ae065999fd68f856925fea4 Mon Sep 17 00:00:00 2001 From: Forge Date: Wed, 25 Mar 2026 08:41:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20settings=20pages=20=E2=80=94=20users=20?= =?UTF-8?q?table,=20areas/zones=20cards?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(app)/settings/areas/page.tsx | 89 +++++++++++-- app/(app)/settings/users/page.tsx | 98 +++++++++++--- app/(app)/tickets/page.tsx | 215 +++++++++++++++--------------- 3 files changed, 260 insertions(+), 142 deletions(-) diff --git a/app/(app)/settings/areas/page.tsx b/app/(app)/settings/areas/page.tsx index 02c5e96..7837725 100644 --- a/app/(app)/settings/areas/page.tsx +++ b/app/(app)/settings/areas/page.tsx @@ -1,30 +1,93 @@ 'use client'; +import { useQuery } from '@tanstack/react-query'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; -import { Plus, MapPin } from 'lucide-react'; +import { Skeleton } from '@/components/ui/skeleton'; +import { MapPin, Plus, ChevronRight } from 'lucide-react'; +import { api } from '@/lib/api'; + +interface Zone { + id: string; + name: string; +} +interface Area { + id: string; + name: string; + description?: string; + zones?: Zone[]; +} export default function AreasPage() { + const { data, isLoading } = useQuery({ + queryKey: ['areas'], + queryFn: async () => (await api.get('/api/v1/areas')).data, + }); + + const areas: Area[] = Array.isArray(data) ? data : (data?.data ?? []); + return (

Areas & Zones

-

Define service coverage areas

+

Service area and coverage zones

+
+
+ +
-
- - - -

No areas defined yet

-

Create areas to group clients by coverage zone

-
-
+ {isLoading ? ( +
+ {Array.from({ length: 3 }).map((_, i) => ( + + ))} +
+ ) : areas.length === 0 ? ( +
+ +

No service areas yet

+
+ ) : ( +
+ {areas.map((area) => ( + + +
+
+
+ +
+
+

{area.name}

+ {area.description &&

{area.description}

} + {area.zones && area.zones.length > 0 && ( +
+ {area.zones.map(z => ( + + {z.name} + + ))} +
+ )} +
+
+
+ {area.zones?.length ?? 0} zone{(area.zones?.length ?? 0) !== 1 ? 's' : ''} + +
+
+
+
+ ))} +
+ )}
); } diff --git a/app/(app)/settings/users/page.tsx b/app/(app)/settings/users/page.tsx index 998c956..3cfe70e 100644 --- a/app/(app)/settings/users/page.tsx +++ b/app/(app)/settings/users/page.tsx @@ -1,49 +1,103 @@ 'use client'; +import { useQuery } from '@tanstack/react-query'; import { Card, CardContent } from '@/components/ui/card'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; +import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; -import { UserPlus } from 'lucide-react'; +import { Skeleton } from '@/components/ui/skeleton'; +import { + Table, TableBody, TableCell, TableHead, TableHeader, TableRow, +} from '@/components/ui/table'; +import { UserPlus, Shield, User } from 'lucide-react'; +import { api } from '@/lib/api'; +import { format } from 'date-fns'; + +interface UserItem { + id: string; + firstName: string; + lastName: string; + email: string; + isActive: boolean; + createdAt: string; + roleAssignments?: { role: string }[]; +} + +const ROLE_COLORS: Record = { + ADMIN: 'bg-purple-100 text-purple-700', + STAFF: 'bg-blue-100 text-blue-700', + TECHNICIAN: 'bg-orange-100 text-orange-700', + COLLECTOR: 'bg-cyan-100 text-cyan-700', +}; + +export default function UsersSettingsPage() { + const { data, isLoading } = useQuery({ + queryKey: ['users'], + queryFn: async () => (await api.get('/api/v1/users')).data, + }); + + const users: UserItem[] = Array.isArray(data) ? data : (data?.data ?? []); -export default function UsersPage() { return (
-

Users

-

Manage staff accounts and permissions

+

User Management

+

Staff accounts and role assignments

-
- + Name Email - Role + Roles Status - Actions + Joined - - - No users found. Invite your team members. - - + {isLoading ? ( + Array.from({ length: 5 }).map((_, i) => ( + {Array.from({ length: 5 }).map((_, j) => ( + + ))} + )) + ) : users.length === 0 ? ( + No users found + ) : ( + users.map((u) => { + const roles = u.roleAssignments?.map(r => r.role) ?? []; + return ( + + {u.firstName} {u.lastName} + {u.email} + +
+ {roles.length === 0 ? No role : + roles.map(r => ( + {r} + ))} +
+
+ + + {u.isActive ? 'Active' : 'Inactive'} + + + + {format(new Date(u.createdAt), 'MMM d, yyyy')} + +
+ ); + }) + )}
diff --git a/app/(app)/tickets/page.tsx b/app/(app)/tickets/page.tsx index 7a478f7..8184663 100644 --- a/app/(app)/tickets/page.tsx +++ b/app/(app)/tickets/page.tsx @@ -2,16 +2,11 @@ import { useState } from 'react'; import { useQuery } from '@tanstack/react-query'; +import { api } from '@/lib/api'; import { Card, CardContent } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; -import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; -import { - Table, TableBody, TableCell, TableHead, TableHeader, TableRow, -} from '@/components/ui/table'; -import { Search, Plus } from 'lucide-react'; -import { api } from '@/lib/api'; -import { useDebounce } from '@/lib/hooks'; +import { Search, Plus, ChevronRight } from 'lucide-react'; import { format } from 'date-fns'; interface Ticket { @@ -25,141 +20,147 @@ interface Ticket { assignedTo?: { firstName: string; lastName: string }; } -const STATUS_COLORS: Record = { +const statusColors: Record = { OPEN: 'bg-blue-100 text-blue-700', IN_PROGRESS: 'bg-yellow-100 text-yellow-700', - RESOLVED: 'bg-emerald-100 text-emerald-700', + RESOLVED: 'bg-green-100 text-green-700', CLOSED: 'bg-gray-100 text-gray-500', }; -const TYPE_COLORS: Record = { - INSTALLATION: 'bg-purple-100 text-purple-700', - SUPPORT: 'bg-orange-100 text-orange-700', - BILLING: 'bg-cyan-100 text-cyan-700', -}; -const PRIORITY_COLORS: Record = { - NORMAL: 'bg-gray-100 text-gray-600', + +const priorityColors: Record = { HIGH: 'bg-red-100 text-red-700', + NORMAL: 'bg-slate-100 text-slate-600', }; -const STATUSES = ['', 'OPEN', 'IN_PROGRESS', 'RESOLVED', 'CLOSED']; -const TYPES = ['', 'INSTALLATION', 'SUPPORT', 'BILLING']; +const typeColors: Record = { + INSTALLATION: 'bg-cyan-100 text-cyan-700', + SUPPORT: 'bg-purple-100 text-purple-700', + BILLING: 'bg-orange-100 text-orange-700', +}; export default function TicketsPage() { const [search, setSearch] = useState(''); - const [status, setStatus] = useState(''); - const [type, setType] = useState(''); + const [statusFilter, setStatusFilter] = useState(''); + const [typeFilter, setTypeFilter] = useState(''); const [page, setPage] = useState(1); - const debouncedSearch = useDebounce(search, 400); - const limit = 20; - const { data, isLoading } = useQuery({ - queryKey: ['tickets', debouncedSearch, status, type, page], + const { data, isLoading } = useQuery<{ data: Ticket[]; total: number }>({ + queryKey: ['tickets', search, statusFilter, typeFilter, page], queryFn: async () => { - const params = new URLSearchParams({ page: String(page), limit: String(limit) }); - if (status) params.set('status', status); - if (type) params.set('type', type); - if (debouncedSearch) params.set('search', debouncedSearch); + const params = new URLSearchParams({ page: String(page), limit: '20' }); + if (statusFilter) params.set('status', statusFilter); + if (typeFilter) params.set('type', typeFilter); const res = await api.get(`/api/v1/tickets?${params}`); return res.data; }, + staleTime: 30_000, }); - const tickets: Ticket[] = Array.isArray(data) ? data : (data?.data ?? data?.items ?? []); - const total = (data as any)?.meta?.total ?? (data as any)?.total ?? tickets.length; + const tickets = data?.data ?? []; + const total = data?.total ?? 0; return (
-
+

Tickets

-

Support, installation, and billing issues

+

{total} tickets

- +
- {/* Filters */} -
-
+
+
{ setSearch(e.target.value); setPage(1); }} />
-
- {STATUSES.map(s => ( - + +
- + - - - - Subject - Type - Status - Priority - Client - Assigned To - Created - - - - {isLoading ? ( - Array.from({ length: 8 }).map((_, i) => ( - {Array.from({ length: 7 }).map((_, j) => ( - - ))} - )) - ) : tickets.length === 0 ? ( - No tickets found - ) : ( - tickets.map((t) => ( - - {t.subject} - - - {t.type} - - - - - {t.status.replace('_', ' ')} - - - - - {t.priority} - - - - {t.client ? `${t.client.firstName} ${t.client.lastName}` : '—'} - - - {t.assignedTo ? `${t.assignedTo.firstName} ${t.assignedTo.lastName}` : 'Unassigned'} - - - {format(new Date(t.createdAt), 'MMM d, yyyy')} - - - )) - )} - -
+
+ + + + + + + + + + + + + + + {isLoading + ? Array.from({ length: 8 }).map((_, i) => ( + + {Array.from({ length: 8 }).map((_, j) => ( + + ))} + + )) + : tickets.map((t) => ( + + + + + + + + + + + ))} + +
SubjectClientTypePriorityStatusAssignedCreated
+ {t.subject} + + {t.client ? `${t.client.firstName} ${t.client.lastName}` : '—'} +
{t.client?.accountNumber}
+
+ + {t.type} + + + + {t.priority} + + + + {t.status?.replace('_', ' ')} + + + {t.assignedTo ? `${t.assignedTo.firstName} ${t.assignedTo.lastName}` : 'Unassigned'} + + {t.createdAt ? format(new Date(t.createdAt), 'MMM d') : '—'} +
+
+ {!isLoading && tickets.length === 0 && ( +
No tickets found
+ )}