File size: 4,038 Bytes
41c19e3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | import React, { useEffect, useState } from 'react';
import { Phone, MapPin, Loader2 } from 'lucide-react';
import { apiFetch } from '@/lib/api';
function formatWhen(iso) {
if (!iso) return '';
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return '';
return d.toLocaleString(undefined, {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
});
}
/**
* Chronological Call/Visit activity feed. Self-fetching — pass `endpoint` (the GET URL) and bump
* `refreshKey` after logging a new activity elsewhere to re-fetch.
*/
export default function ActivityTimeline({ endpoint, refreshKey, heading = 'Activity timeline', className }) {
const [activities, setActivities] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!endpoint) return;
let cancelled = false;
setLoading(true);
(async () => {
try {
const res = await apiFetch(endpoint);
if (!res.ok) return;
const data = await res.json();
if (!cancelled) setActivities(data.activities || []);
} catch (e) {
console.error(e);
} finally {
if (!cancelled) setLoading(false);
}
})();
return () => {
cancelled = true;
};
}, [endpoint, refreshKey]);
if (!endpoint) return null;
return (
<div className={className}>
<h4 className="mb-3 font-semibold text-slate-800">{heading}</h4>
{loading ? (
<div className="flex items-center gap-2 text-sm text-slate-500">
<Loader2 className="h-4 w-4 animate-spin" aria-hidden />
Loading…
</div>
) : activities.length === 0 ? (
<p className="text-sm text-slate-500">No calls or visits logged yet.</p>
) : (
<ul className="space-y-2">
{activities.map((a) => (
<li
key={a.id}
className="flex gap-2.5 rounded-lg border border-slate-200 bg-white px-3 py-2"
>
<div
className={
'mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full ' +
(a.activity_type === 'call' ? 'bg-emerald-100 text-emerald-700' : 'bg-violet-100 text-violet-700')
}
>
{a.activity_type === 'call' ? (
<Phone className="h-3.5 w-3.5" aria-hidden />
) : (
<MapPin className="h-3.5 w-3.5" aria-hidden />
)}
</div>
<div className="min-w-0 flex-1">
<p className="text-xs text-slate-500">
<span className="font-medium text-slate-700">
{a.activity_type === 'call' ? 'Call' : 'Visit'}
</span>
{a.contact_name ? <span> with {a.contact_name}</span> : null}
{a.user_display_name ? <span> · {a.user_display_name}</span> : null}
{a.created_at ? <span> · {formatWhen(a.created_at)}</span> : null}
</p>
{a.notes ? (
<p className="mt-0.5 whitespace-pre-wrap text-sm text-slate-800">{a.notes}</p>
) : null}
</div>
</li>
))}
</ul>
)}
</div>
);
}
|