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