diff --git a/components/admin-trade-workspace.tsx b/components/admin-trade-workspace.tsx index d704019..e80c1d4 100644 --- a/components/admin-trade-workspace.tsx +++ b/components/admin-trade-workspace.tsx @@ -2,6 +2,18 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import Link from "next/link"; +import { + ArrowLeft, + ArrowRightLeft, + CalendarClock, + Flag, + Mail, + MessageSquare, + Phone, + Sparkles, + UserRound, + type LucideIcon, +} from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { @@ -16,6 +28,7 @@ import { TRADE_VENUE_TYPES } from "@/lib/trade-leads-common"; import { classifyFollowUp, describeTradeLeadActivity, + followUpDayDelta, MANUAL_LEAD_SOURCES, TRADE_LEAD_STATUSES, tradeLeadSourceLabel, @@ -31,6 +44,12 @@ import { // Input/select borders use ink at 50% so every form control boundary is // visible against the paper background (WCAG 1.4.11 non-text contrast). const fieldClass = "w-full rounded-md border border-ink/50 px-3 py-2"; +// Denser variant for the left-rail filters. +const filterFieldClass = + "w-full rounded-md border border-ink/50 px-2.5 py-1.5 text-sm"; +// Small uppercase labels group read-only context vs. editable controls. +const sectionLabelClass = + "text-xs font-medium uppercase tracking-wide text-muted-foreground"; type StatusFilter = "all" | TradeLeadStatus; type FollowUpFilter = "all" | FollowUpState; @@ -104,17 +123,27 @@ function StatusBadge({ status }: { status: TradeLeadStatus }) { ); } -function FollowUpBadge({ lead }: { lead: TradeLeadView }) { - const state = classifyFollowUp(lead.nextFollowUpAt, lead.status); - if (state === "none") return null; - if (state === "overdue") { +function FollowUpBadge({ + lead, + showEmpty = false, +}: { + lead: TradeLeadView; + showEmpty?: boolean; +}) { + const delta = followUpDayDelta(lead.nextFollowUpAt, lead.status); + if (delta === null) { + return showEmpty ? ( + No follow-up + ) : null; + } + if (delta < 0) { return ( - Overdue · {formatAdminDate(lead.nextFollowUpAt)} + {delta === -1 ? "Overdue" : `Overdue by ${-delta} days`} ); } - if (state === "due_today") { + if (delta === 0) { return ( {formatAdminDate(lead.nextFollowUpAt)} + Due {formatAdminDate(lead.nextFollowUpAt)} ); } +// Timeline type → icon + tint. Restrained accents: notes read as the +// human-authored entries, system entries stay muted. +const ACTIVITY_ICON: Record< + string, + { icon: LucideIcon; className: string } +> = { + note: { icon: MessageSquare, className: "text-ocean" }, + lead_created: { icon: Sparkles, className: "text-moss" }, + status_changed: { icon: ArrowRightLeft, className: "text-ink" }, + owner_changed: { icon: UserRound, className: "text-ocean" }, + follow_up_set: { icon: CalendarClock, className: "text-amber-700" }, + follow_up_changed: { icon: CalendarClock, className: "text-amber-700" }, + follow_up_cleared: { icon: CalendarClock, className: "text-amber-700" }, + outcome_changed: { icon: Flag, className: "text-moss" }, + communication: { icon: Mail, className: "text-muted-foreground" }, +}; +const DEFAULT_ACTIVITY_ICON = { + icon: MessageSquare, + className: "text-muted-foreground", +}; + // wa.me accepts digits only; strip formatting from whatever was submitted. function whatsappDigits(phone: string): string { return phone.replace(/[^\d]/g, ""); @@ -247,6 +297,14 @@ export function AdminTradeWorkspace({ user }: { user: AdminPanelUser }) { [apiFetch] ); + // Clears the selection (mobile "back to list" and any future deselect). + function closeLead() { + selectedIdRef.current = null; + setSelectedId(null); + setDetail(null); + setDetailLoading(false); + } + // Applies the fresh lead + activities a mutation response returns. The // list always updates; the detail panel only updates when the mutated // lead is still the selected one (a response may land after switching). @@ -430,54 +488,58 @@ export function AdminTradeWorkspace({ user }: { user: AdminPanelUser }) { const waDigits = detail ? whatsappDigits(detail.lead.phoneOrWhatsapp) : ""; return ( -
-
+
+
-

Trade Leads

-

+

Trade Leads

+

Admin dashboard {" "} → Trade leads

-
+
{!loading && ( -

+

{counts.fresh} new · {counts.dueToday} due today ·{" "} {counts.overdue} overdue

)} - +
{statusMessage && (

{statusMessage}

)}
-
-
+
+
-
- - {selectedTerminal && ( -
-
+ ) : ( +
+ + Next follow-up + +
+ setFollowUpDraft(e.target.value)} + // Save on blur, not per keystroke: date inputs emit a + // change event per segment while typing (year "2026" + // passes through "0202", a valid-but-wrong date). An + // emptied field does NOT clear; clearing stays + // explicit via the Clear button. + onBlur={(e) => { + const iso = dateInputToIso(e.target.value); + if ( + iso && + e.target.value !== + dateToLocalDateInput(detail.lead.nextFollowUpAt) + ) { + void patchLead({ nextFollowUpAt: iso }, "followup"); + } + }} + /> + + {detail.lead.nextFollowUpAt && ( + + )} +
+
+ )} +