188 lines
7.1 KiB
TypeScript
188 lines
7.1 KiB
TypeScript
'use client';
|
||
|
||
import React, { useEffect, useMemo, useState } from 'react';
|
||
import DashboardLayout from '../adminDashboard/dashlayout';
|
||
import SiteCard from '@/components/dashboards/SiteCard';
|
||
|
||
type CrmProject = {
|
||
name: string; // e.g. PROJ-0008 (siteId)
|
||
project_name: string;
|
||
status?: string | null;
|
||
modified?: string | null;
|
||
customer?: string | null;
|
||
project_type?: string | null;
|
||
custom_address?: string | null;
|
||
custom_email?: string | null;
|
||
custom_mobile_phone_no?: string | null;
|
||
};
|
||
|
||
const API = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:8000';
|
||
|
||
const SitesPage = () => {
|
||
const [projects, setProjects] = useState<CrmProject[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [err, setErr] = useState<string | null>(null);
|
||
const [q, setQ] = useState(''); // search filter
|
||
|
||
// pagination
|
||
const [page, setPage] = useState(1);
|
||
const [pageSize, setPageSize] = useState(6); // tweak as you like
|
||
|
||
useEffect(() => {
|
||
let cancelled = false;
|
||
const run = async () => {
|
||
setLoading(true);
|
||
setErr(null);
|
||
try {
|
||
const res = await fetch(`${API}/crm/projects?limit=0`);
|
||
if (!res.ok) throw new Error(await res.text());
|
||
const json = await res.json();
|
||
const data: CrmProject[] = json?.data ?? [];
|
||
if (!cancelled) setProjects(data);
|
||
} catch (e: any) {
|
||
if (!cancelled) setErr(e?.message ?? 'Failed to load CRM projects');
|
||
} finally {
|
||
if (!cancelled) setLoading(false);
|
||
}
|
||
};
|
||
run();
|
||
return () => { cancelled = true; };
|
||
}, []);
|
||
|
||
// Reset to first page whenever search or pageSize changes
|
||
useEffect(() => {
|
||
setPage(1);
|
||
}, [q, pageSize]);
|
||
|
||
const filtered = useMemo(() => {
|
||
if (!q.trim()) return projects;
|
||
const needle = q.toLowerCase();
|
||
return projects.filter(p =>
|
||
(p.project_name || '').toLowerCase().includes(needle) ||
|
||
(p.name || '').toLowerCase().includes(needle) ||
|
||
(p.customer || '').toLowerCase().includes(needle)
|
||
);
|
||
}, [projects, q]);
|
||
|
||
const total = filtered.length;
|
||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||
const safePage = Math.min(page, totalPages);
|
||
const startIdx = (safePage - 1) * pageSize;
|
||
const endIdx = Math.min(startIdx + pageSize, total);
|
||
const pageItems = filtered.slice(startIdx, endIdx);
|
||
|
||
const goPrev = () => setPage(p => Math.max(1, p - 1));
|
||
const goNext = () => setPage(p => Math.min(totalPages, p + 1));
|
||
|
||
return (
|
||
<DashboardLayout>
|
||
<div className="p-6 space-y-6">
|
||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-3">
|
||
<h1 className="text-2xl font-bold dark:text-white-light">All Sites Overview</h1>
|
||
|
||
<div className="flex items-center gap-3">
|
||
<input
|
||
value={q}
|
||
onChange={e => setQ(e.target.value)}
|
||
placeholder="Search by name / ID / customer"
|
||
className="w-64 max-w-full px-3 py-2 rounded-md border dark:border-gray-700 bg-white dark:bg-gray-900 dark:text-white"
|
||
/>
|
||
<select
|
||
value={pageSize}
|
||
onChange={e => setPageSize(Number(e.target.value))}
|
||
className="px-3 py-2 rounded-md border dark:border-gray-700 bg-white dark:bg-gray-900 dark:text-white"
|
||
aria-label="Items per page"
|
||
>
|
||
<option value={6}>6 / page</option>
|
||
<option value={9}>9 / page</option>
|
||
<option value={12}>12 / page</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
{loading && (
|
||
<div className="text-gray-600 dark:text-gray-400">Loading CRM projects…</div>
|
||
)}
|
||
|
||
{err && (
|
||
<div className="text-red-600">Error: {err}</div>
|
||
)}
|
||
|
||
{!loading && !err && total === 0 && (
|
||
<div className="text-amber-600">No sites found.</div>
|
||
)}
|
||
|
||
{!loading && !err && total > 0 && (
|
||
<>
|
||
{/* Pagination header */}
|
||
<div className="flex items-center justify-between">
|
||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||
Showing <span className="font-semibold">{startIdx + 1}</span>–<span className="font-semibold">{endIdx}</span> of <span className="font-semibold">{total}</span>
|
||
</div>
|
||
<div className="flex items-center gap-2 dark:text-white">
|
||
<button
|
||
onClick={goPrev}
|
||
disabled={safePage <= 1}
|
||
className={`px-3 py-1.5 rounded-md border dark:border-gray-700 ${safePage <= 1 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-50 dark:hover:bg-gray-800'}`}
|
||
>
|
||
Previous
|
||
</button>
|
||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||
Page <span className="font-semibold">{safePage}</span> / {totalPages}
|
||
</span>
|
||
<button
|
||
onClick={goNext}
|
||
disabled={safePage >= totalPages}
|
||
className={`px-3 py-1.5 rounded-md border dark:border-gray-700 ${safePage >= totalPages ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-50 dark:hover:bg-gray-800 '}`}
|
||
>
|
||
Next
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Cards */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||
{pageItems.map(p => (
|
||
<SiteCard
|
||
key={p.name}
|
||
siteId={p.name} // SiteCard self-fetches details
|
||
fallbackStatus={p.status ?? undefined}
|
||
/>
|
||
))}
|
||
</div>
|
||
|
||
{/* Pagination footer mirrors header for convenience */}
|
||
<div className="flex items-center justify-between">
|
||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||
Showing <span className="font-semibold">{startIdx + 1}</span>–<span className="font-semibold">{endIdx}</span> of <span className="font-semibold">{total}</span>
|
||
</div>
|
||
<div className="flex items-center gap-2 dark:text-white">
|
||
<button
|
||
onClick={goPrev}
|
||
disabled={safePage <= 1}
|
||
className={`px-3 py-1.5 rounded-md border dark:border-gray-700 ${safePage <= 1 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-50 dark:hover:bg-gray-800'}`}
|
||
>
|
||
Previous
|
||
</button>
|
||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||
Page <span className="font-semibold">{safePage}</span> / {totalPages}
|
||
</span>
|
||
<button
|
||
onClick={goNext}
|
||
disabled={safePage >= totalPages}
|
||
className={`px-3 py-1.5 rounded-md border dark:border-gray-700 ${safePage >= totalPages ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-50 dark:hover:bg-gray-800'}`}
|
||
>
|
||
Next
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
</DashboardLayout>
|
||
);
|
||
};
|
||
|
||
export default SitesPage;
|
||
|