wacrm/src/components/pipelines/deal-card.tsx
Arnas Donauskas 2cc03fcdfa
fix(pipelines): dark-theme the board + cards, put analytics above, add metric tooltips
Three follow-ups to the Pipelines page that shipped in #18:

1. Analytics strip now sits ABOVE the Kanban board rather than below, so
   top-line numbers read first and the board flows beneath.
2. Re-theme the board to match the rest of the dark app: slate-900 columns
   with slate-800 borders, slate-800 deal cards with stage-colored accent
   bar, emerald dollar values, and slate-500/slate-400 secondary text. The
   empty-column "Drop a deal here" and drop-zone highlight stay but in
   slate/emerald tones instead of white/light-gray.
3. Each analytics metric now has an info icon + tooltip explaining how it
   is calculated in plain language (e.g. Weighted Value explains the
   10% → 90% stage interpolation).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 22:50:52 +03:00

114 lines
3.8 KiB
TypeScript

"use client";
import type { Deal, PipelineStage } from "@/types";
import { Calendar, Check, X } from "lucide-react";
interface DealCardProps {
deal: Deal;
stage: PipelineStage | null;
onEdit: (deal: Deal) => void;
isOverlay?: boolean;
}
function formatCurrency(value: number, currency?: string) {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: currency || "USD",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(Number(value || 0));
}
function formatDate(dateStr: string) {
return new Date(dateStr).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
}
function initials(name?: string, fallback?: string) {
const source = (name || fallback || "?").trim();
if (!source) return "?";
return source.charAt(0).toUpperCase();
}
export function DealCard({ deal, stage, onEdit, isOverlay }: DealCardProps) {
const contactLabel = deal.contact?.name || deal.contact?.phone || "No contact";
const assigneeLabel = deal.assignee?.full_name || null;
return (
<button
type="button"
onClick={(e) => {
// `onClick` still fires after a non-drag tap because the PointerSensor
// requires 5px movement before it counts as a drag.
if (isOverlay) return;
e.stopPropagation();
onEdit(deal);
}}
className={`group relative w-full cursor-pointer rounded-xl border border-slate-700/50 bg-slate-800/70 pl-4 pr-3 py-3 text-left shadow-sm transition-all ${
isOverlay
? "shadow-xl"
: "hover:-translate-y-0.5 hover:border-slate-600 hover:bg-slate-800 hover:shadow-lg"
}`}
>
{/* 4px left accent bar using stage color */}
<span
aria-hidden
className="absolute left-0 top-0 h-full w-1 rounded-l-xl"
style={{ backgroundColor: stage?.color ?? "#94a3b8" }}
/>
<div className="flex items-start justify-between gap-2">
<h4 className="flex-1 text-sm font-semibold leading-snug text-white break-words">
{deal.title}
</h4>
{deal.status === "won" && (
<span className="inline-flex shrink-0 items-center gap-1 rounded-full bg-emerald-500/15 px-2 py-0.5 text-[10px] font-semibold text-emerald-400">
<Check className="h-3 w-3" />
Won
</span>
)}
{deal.status === "lost" && (
<span className="inline-flex shrink-0 items-center gap-1 rounded-full bg-red-500/15 px-2 py-0.5 text-[10px] font-semibold text-red-400">
<X className="h-3 w-3" />
Lost
</span>
)}
</div>
{/* Contact row */}
<div className="mt-2 flex items-center gap-2">
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-slate-700 text-[10px] font-semibold text-slate-200">
{initials(deal.contact?.name, deal.contact?.phone)}
</span>
<span className="truncate text-xs text-slate-400">{contactLabel}</span>
</div>
<div className="mt-2 flex items-center justify-between">
<span className="text-sm font-bold text-emerald-400">
{formatCurrency(deal.value, deal.currency)}
</span>
{deal.expected_close_date && (
<span className="flex items-center gap-1 text-[11px] text-slate-500">
<Calendar className="h-3 w-3" />
{formatDate(deal.expected_close_date)}
</span>
)}
</div>
{assigneeLabel && (
<div className="mt-2 flex items-center justify-end">
<span
title={assigneeLabel}
className="flex h-5 w-5 items-center justify-center rounded-full bg-emerald-500/15 text-[10px] font-semibold text-emerald-400"
>
{initials(assigneeLabel)}
</span>
</div>
)}
</button>
);
}