2025-08-15 09:20:13 +08:00

188 lines
7.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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;