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>
64 lines
1.8 KiB
TypeScript
64 lines
1.8 KiB
TypeScript
'use client';
|
|
|
|
import { useDroppable } from '@dnd-kit/core';
|
|
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
|
|
|
import { PipelineCard } from '@/components/interests/pipeline-card';
|
|
import { Badge } from '@/components/ui/badge';
|
|
|
|
interface ColumnItem {
|
|
id: string;
|
|
clientName: string | null;
|
|
berthMooringNumber: string | null;
|
|
leadCategory: string | null;
|
|
updatedAt: string | Date;
|
|
}
|
|
|
|
interface PipelineColumnProps {
|
|
stage: string;
|
|
label: string;
|
|
items: ColumnItem[];
|
|
}
|
|
|
|
export function PipelineColumn({ stage, label, items }: PipelineColumnProps) {
|
|
const { setNodeRef, isOver } = useDroppable({ id: stage });
|
|
|
|
return (
|
|
<div
|
|
ref={setNodeRef}
|
|
className={`flex flex-col gap-2 min-w-[220px] w-[220px] flex-shrink-0 bg-muted/40 rounded-lg p-3 transition-colors ${
|
|
isOver ? 'bg-muted/70 ring-2 ring-primary/30' : ''
|
|
}`}
|
|
>
|
|
<div className="flex items-center justify-between mb-1">
|
|
<span className="text-sm font-semibold capitalize">{label}</span>
|
|
<Badge variant="outline" className="text-xs">
|
|
{items.length}
|
|
</Badge>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-2 overflow-y-auto max-h-[calc(100vh-220px)]">
|
|
<SortableContext
|
|
items={items.map((i) => i.id)}
|
|
strategy={verticalListSortingStrategy}
|
|
>
|
|
{items.map((item) => (
|
|
<PipelineCard
|
|
key={item.id}
|
|
id={item.id}
|
|
clientName={item.clientName}
|
|
berthMooringNumber={item.berthMooringNumber}
|
|
leadCategory={item.leadCategory}
|
|
updatedAt={item.updatedAt}
|
|
/>
|
|
))}
|
|
</SortableContext>
|
|
|
|
{items.length === 0 && (
|
|
<div className="text-center py-6 text-xs text-muted-foreground">Empty</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|