mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 17:29:45 +00:00
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>
114 lines
3.8 KiB
TypeScript
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>
|
|
);
|
|
}
|