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