147 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			147 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| // components/dashboards/SiteCard.tsx
 | |
| 'use client';
 | |
| 
 | |
| import React, { useEffect, useMemo, useState } from 'react';
 | |
| import Link from 'next/link';
 | |
| import { formatAddress } from '@/app/utils/formatAddress';
 | |
| import { formatCrmTimestamp } from '@/app/utils/datetime';
 | |
| 
 | |
| 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;
 | |
| };
 | |
| 
 | |
| 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<SiteCardProps> = ({ siteId, className = '', fallbackStatus }) => {
 | |
|   const [project, setProject] = useState<CrmProject | null>(null);
 | |
|   const [loading, setLoading] = useState(true);
 | |
|   const [err, setErr] = useState<string | null>(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 (
 | |
|     <div className={`bg-white p-4 rounded-lg shadow-md dark:bg-rtgray-800 dark:text-white-light flex flex-col space-y-2 ${className}`}>
 | |
|       <h3 className="text-xl font-bold text-primary-600 dark:text-primary-400 border-b pb-2 mb-2">
 | |
|         {project?.project_name || siteId}
 | |
|       </h3>
 | |
| 
 | |
|       {loading ? (
 | |
|         <div className="animate-pulse space-y-2">
 | |
|           <div className="h-4 w-24 bg-rtgray-200 dark:bg-rtgray-700 rounded" />
 | |
|           <div className="h-4 w-48 bg-rtgray-200 dark:bg-rtgray-700 rounded" />
 | |
|           <div className="h-4 w-40 bg-rtgray-200 dark:bg-rtgray-700 rounded" />
 | |
|           <div className="h-4 w-36 bg-rtgray-200 dark:bg-rtgray-700 rounded" />
 | |
|         </div>
 | |
|       ) : err ? (
 | |
|         <div className="text-red-500 text-sm">Failed to load CRM: {err}</div>
 | |
|       ) : !project ? (
 | |
|         <div className="text-amber-500 text-sm">No CRM project found for <span className="font-semibold">{siteId}</span>.</div>
 | |
|       ) : (
 | |
|         <>
 | |
|           <div className="flex justify-between items-center">
 | |
|             <p className="text-gray-600 dark:text-gray-400 font-medium">Status:</p>
 | |
|             <p className={`font-semibold ${statusColorClass}`}>{status}</p>
 | |
|           </div>
 | |
| 
 | |
|           <div className="flex justify-between items-center">
 | |
|             <p className="text-gray-600 dark:text-gray-400 font-medium">Location:</p>
 | |
|             <p className="font-medium whitespace-pre-line text-right">{niceAddress}</p>
 | |
|           </div>
 | |
| 
 | |
|           <div className="flex justify-between items-center">
 | |
|             <p className="text-gray-600 dark:text-gray-400 font-medium">Inverter Provider:</p>
 | |
|             <p className="font-medium">{inverterProvider}</p>
 | |
|           </div>
 | |
| 
 | |
|           <div className="flex justify-between items-center">
 | |
|             <p className="text-gray-600 dark:text-gray-400 font-medium">Emergency Contact:</p>
 | |
|             <p className="font-medium text-right">{emergencyContact}</p>
 | |
|           </div>
 | |
| 
 | |
|           <div className="flex justify-between items-center">
 | |
|             <p className="text-gray-600 dark:text-gray-400 font-medium">Last Sync:</p>
 | |
|             <p className="font-medium">{lastSync}</p>
 | |
|           </div>
 | |
|         </>
 | |
|       )}
 | |
| 
 | |
|       <Link
 | |
|         href={{ pathname: '/adminDashboard', query: { site: siteId } }}
 | |
|         className="mt-4 w-full text-center text-sm btn-primary"
 | |
|       >
 | |
|         View Dashboard
 | |
|       </Link>
 | |
|     </div>
 | |
|   );
 | |
| };
 | |
| 
 | |
| export default SiteCard;
 |