Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM, PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source files covering clients, berths, interests/pipeline, documents/EOI, expenses/invoices, email, notifications, dashboard, admin, and client portal. CI/CD via Gitea Actions with Docker builds. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
87
src/components/interests/interest-timeline.tsx
Normal file
87
src/components/interests/interest-timeline.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
'use client';
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { format } from 'date-fns';
|
||||
import { Pencil, FileText, Clock, PlusCircle, Archive, RotateCcw } from 'lucide-react';
|
||||
|
||||
import { apiFetch } from '@/lib/api/client';
|
||||
|
||||
interface TimelineEvent {
|
||||
id: string;
|
||||
type: 'audit' | 'document_event';
|
||||
action: string;
|
||||
description: string;
|
||||
userId: string | null;
|
||||
createdAt: string;
|
||||
metadata: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface InterestTimelineProps {
|
||||
interestId: string;
|
||||
}
|
||||
|
||||
function eventIcon(event: TimelineEvent) {
|
||||
if (event.type === 'document_event') return <FileText className="h-4 w-4" />;
|
||||
if (event.action === 'create') return <PlusCircle className="h-4 w-4 text-green-500" />;
|
||||
if (event.action === 'archive') return <Archive className="h-4 w-4 text-orange-500" />;
|
||||
if (event.action === 'restore') return <RotateCcw className="h-4 w-4 text-blue-500" />;
|
||||
if (event.metadata?.type === 'stage_change') return <Clock className="h-4 w-4 text-purple-500" />;
|
||||
return <Pencil className="h-4 w-4 text-muted-foreground" />;
|
||||
}
|
||||
|
||||
export function InterestTimeline({ interestId }: InterestTimelineProps) {
|
||||
const { data, isLoading } = useQuery<{ data: TimelineEvent[] }>({
|
||||
queryKey: ['interest-timeline', interestId],
|
||||
queryFn: () => apiFetch(`/api/v1/interests/${interestId}/timeline`),
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<div key={i} className="flex gap-3 animate-pulse">
|
||||
<div className="h-8 w-8 rounded-full bg-muted shrink-0" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="h-3 bg-muted rounded w-3/4" />
|
||||
<div className="h-2 bg-muted rounded w-1/2" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const events = data?.data ?? [];
|
||||
|
||||
if (events.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
<p>No activity yet.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative space-y-0">
|
||||
{/* Vertical line */}
|
||||
<div className="absolute left-4 top-2 bottom-2 w-px bg-border" />
|
||||
|
||||
{events.map((event, idx) => (
|
||||
<div key={event.id} className="relative flex gap-4 pb-6">
|
||||
{/* Icon */}
|
||||
<div className="relative z-10 flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-background border">
|
||||
{eventIcon(event)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 pt-1">
|
||||
<p className="text-sm">{event.description}</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{format(new Date(event.createdAt), 'MMM d, yyyy HH:mm')}
|
||||
{event.userId && ` · by ${event.userId}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user