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