diff --git a/app/(admin)/sites/page.tsx b/app/(admin)/sites/page.tsx index 2f6d3de..03752fa 100644 --- a/app/(admin)/sites/page.tsx +++ b/app/(admin)/sites/page.tsx @@ -1,46 +1,187 @@ 'use client'; -import React from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import DashboardLayout from '../adminDashboard/dashlayout'; -import SiteCard from '@/components/dashboards/SiteCard'; // Import the new SiteCard component -import { mockSiteData, SiteName } from '@/types/SiteData'; // Import your mock data and SiteName type +import SiteCard from '@/components/dashboards/SiteCard'; -const SitesPage = () => { - // Helper function to determine status (can be externalized if used elsewhere) - const getSiteStatus = (siteName: SiteName): string => { - const statusMap: Record = { - 'Site A': 'Active', - 'Site B': 'Inactive', - 'Site C': 'Faulty', - }; - return statusMap[siteName]; - }; - - return ( - -
-

All Sites Overview

- -
- {/* Iterate over the keys of mockSiteData (which are your SiteNames) */} - {Object.keys(mockSiteData).map((siteNameKey) => { - const siteName = siteNameKey as SiteName; // Cast to SiteName type - const siteDetails = mockSiteData[siteName]; - const siteStatus = getSiteStatus(siteName); - - return ( - - ); - })} -
-
-
- ); +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; }; -export default SitesPage; \ No newline at end of file +const API = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:8000'; + +const SitesPage = () => { + const [projects, setProjects] = useState([]); + const [loading, setLoading] = useState(true); + const [err, setErr] = useState(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 ( + +
+
+

All Sites Overview

+ +
+ 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" + /> + +
+
+ + {loading && ( +
Loading CRM projects…
+ )} + + {err && ( +
Error: {err}
+ )} + + {!loading && !err && total === 0 && ( +
No sites found.
+ )} + + {!loading && !err && total > 0 && ( + <> + {/* Pagination header */} +
+
+ Showing {startIdx + 1}{endIdx} of {total} +
+
+ + + Page {safePage} / {totalPages} + + +
+
+ + {/* Cards */} +
+ {pageItems.map(p => ( + + ))} +
+ + {/* Pagination footer mirrors header for convenience */} +
+
+ Showing {startIdx + 1}{endIdx} of {total} +
+
+ + + Page {safePage} / {totalPages} + + +
+
+ + )} +
+
+ ); +}; + +export default SitesPage; + diff --git a/components/dashboards/SiteCard.tsx b/components/dashboards/SiteCard.tsx index f38ec36..6bc894b 100644 --- a/components/dashboards/SiteCard.tsx +++ b/components/dashboards/SiteCard.tsx @@ -1,63 +1,146 @@ // components/dashboards/SiteCard.tsx -import React from 'react'; -import Link from 'next/link'; // Import Link from Next.js -import { SiteName, SiteDetails } from '@/types/SiteData'; // Adjust path as necessary +'use client'; -interface SiteCardProps { - siteName: SiteName; - details: SiteDetails; - status: string; -} +import React, { useEffect, useMemo, useState } from 'react'; +import Link from 'next/link'; +import { formatAddress } from '@/app/utils/formatAddress'; +import { formatCrmTimestamp } from '@/app/utils/datetime'; -const SiteCard: React.FC = ({ siteName, details, status }) => { - const statusColorClass = - status === 'Active' ? 'text-green-500' : - status === 'Inactive' ? 'text-orange-500' : - 'text-red-500'; - - return ( -
-

- {siteName} -

- -
-

Status:

-

{status}

-
- -
-

Location:

-

{details.location}

-
- -
-

Inverter Provider:

-

{details.inverterProvider}

-
- -
-

Emergency Contact:

-

{details.emergencyContact}

-
- -
-

Last Sync:

-

{details.lastSyncTimestamp}

-
- - {/* New: View Dashboard Button */} - - View Dashboard - -
- ); +type CrmProject = { + name: string; // e.g. PROJ-0008 (siteId) + project_name: string; + status?: string; + percent_complete?: number | null; + owner?: 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; }; -export default SiteCard; \ No newline at end of file +interface SiteCardProps { + siteId: string; // CRM Project "name" (canonical id) + className?: string; // optional styling hook + fallbackStatus?: string; // optional backup status if CRM is missing it +} + +const API = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:8000'; + +const SiteCard: React.FC = ({ siteId, className = '', fallbackStatus }) => { + const [project, setProject] = useState(null); + const [loading, setLoading] = useState(true); + const [err, setErr] = useState(null); + + useEffect(() => { + let cancelled = false; + + const fetchProject = async () => { + setLoading(true); + setErr(null); + try { + // ---- Try a single-project endpoint first (best) ---- + // e.g. GET /crm/projects/PROJ-0008 + const single = await fetch(`${API}/crm/projects/${encodeURIComponent(siteId)}`); + if (single.ok) { + const pj = await single.json(); + if (!cancelled) setProject(pj?.data ?? pj ?? null); + } else { + // ---- Fallback: fetch all and find by name (works with your existing API) ---- + const list = await fetch(`${API}/crm/projects?limit=0`); + if (!list.ok) throw new Error(await list.text()); + const json = await list.json(); + const found = (json?.data ?? []).find((p: CrmProject) => p.name === siteId) ?? null; + if (!cancelled) setProject(found); + } + } catch (e: any) { + if (!cancelled) setErr(e?.message ?? 'Failed to load CRM project'); + } finally { + if (!cancelled) setLoading(false); + } + }; + + fetchProject(); + return () => { cancelled = true; }; + }, [siteId]); + + const status = project?.status || fallbackStatus || 'Unknown'; + const statusColorClass = + status === 'Active' ? 'text-green-500' : + status === 'Inactive' ? 'text-orange-500' : + 'text-red-500'; + + const niceAddress = useMemo(() => { + if (!project?.custom_address) return 'N/A'; + return formatAddress(project.custom_address).multiLine; + }, [project?.custom_address]); + + const lastSync = useMemo(() => { + return formatCrmTimestamp(project?.modified, { includeSeconds: true }) || 'N/A'; + }, [project?.modified]); + + const inverterProvider = project?.project_type || 'N/A'; + const emergencyContact = + project?.custom_mobile_phone_no || + project?.custom_email || + project?.customer || + 'N/A'; + + return ( +
+

+ {project?.project_name || siteId} +

+ + {loading ? ( +
+
+
+
+
+
+ ) : err ? ( +
Failed to load CRM: {err}
+ ) : !project ? ( +
No CRM project found for {siteId}.
+ ) : ( + <> +
+

Status:

+

{status}

+
+ +
+

Location:

+

{niceAddress}

+
+ +
+

Inverter Provider:

+

{inverterProvider}

+
+ +
+

Emergency Contact:

+

{emergencyContact}

+
+ +
+

Last Sync:

+

{lastSync}

+
+ + )} + + + View Dashboard + +
+ ); +}; + +export default SiteCard;