crm-kanban-card.tsx10.5 KBView on GitHub
/**
 * CRM Kanban Card Component
 *
 * Individual card component for displaying conversations in the CRM kanban board.
 * Shows company information, status, last contact info, next steps, and user tasks.
 */

import type { ConversationWithMetadata } from '@/modules/conversations/slice/conversationsSlice';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { formatDealValue, getDealSizeColor } from '@/modules/crm/utils';
import { Building2, ListTodo, Circle, Maximize2 } from 'lucide-react';
import type { CrmFieldEnumOption } from '@/modules/crm/types';
import { KanbanCard } from '@/components/ui/kanban-card';
import { getEnumTextColor } from '@/modules/crm/field-enums';
import { getEnumDisplayText } from '@/modules/crm/utils';
import { FieldBadge } from './ConversationCellComponents/FieldBadge';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { memo, useMemo } from 'react';
import { cn } from '@/lib/utils';

interface CRMKanbanCardProps {
  conversation: ConversationWithMetadata;
  isSelected: boolean;
  isDragging?: boolean;
  statusOptions?: CrmFieldEnumOption[];
  priorityOptions?: CrmFieldEnumOption[];
  onMouseEnter?: (conversationId: string) => void;
  onClick?: () => void;
  onExpandClick?: () => void;
}

export const CRMKanbanCard = memo(function CRMKanbanCard({
  conversation,
  isSelected,
  isDragging = false,
  statusOptions,
  priorityOptions,
  onMouseEnter,
  onClick,
  onExpandClick,
}: CRMKanbanCardProps) {
  const conv = conversation.data.conversation;
  const company = conversation.data.company;

  // Get company name
  const companyName = company?.name || 'No company';

  // Get formatted last contact date
  const lastContactInfo = useMemo(() => {
    if (!conv.lastContactedAt) {
      return {
        text: 'Never',
        color: 'bg-muted/50 text-muted-foreground',
      };
    }
    const lastContactedAt = new Date(conv.lastContactedAt);
    const daysSince = Math.floor((Date.now() - lastContactedAt.getTime()) / (1000 * 60 * 60 * 24));

    let text: string;
    let color: string;

    // Handle negative values (future dates or very recent contacts within milliseconds)
    if (daysSince < 0) {
      text = 'Today';
      color = 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-300';
    } else if (daysSince === 0) {
      text = 'Today';
      color = 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-300';
    } else if (daysSince === 1) {
      text = 'Yesterday';
      color = 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-300';
    } else if (daysSince <= 3) {
      text = `${daysSince}d ago`;
      color = 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-300';
    } else if (daysSince <= 7) {
      text = `${daysSince}d ago`;
      color = 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300';
    } else if (daysSince <= 14) {
      text = `${Math.floor(daysSince / 7)}w ago`;
      color = 'bg-lime-100 text-lime-800 dark:bg-lime-900/30 dark:text-lime-300';
    } else if (daysSince <= 30) {
      text = `${Math.floor(daysSince / 7)}w ago`;
      color = 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300';
    } else if (daysSince <= 60) {
      text = `${Math.floor(daysSince / 30)}mo ago`;
      color = 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300';
    } else if (daysSince <= 90) {
      text = `${Math.floor(daysSince / 30)}mo ago`;
      color = 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300';
    } else {
      text = `${Math.floor(daysSince / 30)}mo ago`;
      color = 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300';
    }

    return { text, color };
  }, [conv.lastContactedAt]);

  // Get user tasks for this conversation
  const userTasks = conversation.data.userTasks || [];

  return (
    <KanbanCard
      id={conv.id}
      isSelected={isSelected}
      isDragging={isDragging}
      onClick={onClick}
      onMouseEnter={() => {
        if (onMouseEnter) {
          onMouseEnter(conv.id);
        }
      }}
    >
      {/* Header Row - Company and Conversation Name */}
      <div className="group mb-3 flex items-center justify-between gap-2">
        <div className="line-clamp-2 min-w-0 flex-1 text-sm font-semibold">
          <Badge variant="outline" className="inline-flex gap-1.5 rounded-sm align-middle">
            <Building2 className="text-muted-foreground h-3.5 w-3.5 shrink-0" />
            {companyName}
          </Badge>{' '}
          <span>{conv.name}</span>
        </div>
        <div className="flex shrink-0 items-center gap-1">
          {conv.priority && (
            <FieldBadge
              colorClass={getEnumTextColor(conv.priority, 'priority', priorityOptions)}
              className="text-xs"
            >
              {getEnumDisplayText(conv.priority, priorityOptions)}
            </FieldBadge>
          )}
          {/* Expand Button - Shows on hover */}
          {onExpandClick && (
            <Tooltip>
              <TooltipTrigger asChild>
                <Button
                  variant="ghost"
                  size="icon"
                  onClick={(e) => {
                    e.stopPropagation(); // Prevent card selection
                    onExpandClick();
                  }}
                  className="h-6 w-6"
                >
                  <Maximize2 className="h-3 w-3" />
                </Button>
              </TooltipTrigger>
              <TooltipContent>Open Full Conversation</TooltipContent>
            </Tooltip>
          )}
        </div>
      </div>

      {/* Two-column layout */}
      <div className="flex gap-4">
        {/* LEFT COLUMN - Overview */}
        <div className="flex flex-1 flex-col gap-2">
          {/* Status Overview Section */}
          {conv.statusOverview && (
            <div className="space-y-0.5">
              <h4 className="text-sm font-semibold">Status Overview</h4>
              <div className="text-sm">{conv.statusOverview}</div>
            </div>
          )}

          {/* Status row */}
          <div className="flex items-center justify-between gap-2">
            <span className="text-sm font-medium">Status:</span>
            <FieldBadge
              colorClass={getEnumTextColor(conv.status, 'status', statusOptions)}
              className="text-xs"
            >
              {conv.status ? getEnumDisplayText(conv.status, statusOptions) : 'Unknown'}
            </FieldBadge>
          </div>

          {/* Deal size row */}
          <div className="flex items-center justify-between gap-2">
            <span className="text-sm font-medium">Deal size:</span>
            <Badge
              variant="secondary"
              className={cn('rounded-sm text-xs', getDealSizeColor(conv.dealValue))}
            >
              {formatDealValue(conv.dealValue)}
            </Badge>
          </div>

          {/* Last contacted row */}
          <div className="flex items-center justify-between gap-2">
            <span className="text-sm font-medium">Last contacted:</span>
            <Badge variant="secondary" className={cn('rounded-sm text-xs', lastContactInfo.color)}>
              {lastContactInfo.text}
            </Badge>
          </div>
        </div>

        {/* RIGHT COLUMN - Actions */}
        <div className="flex w-1/2 flex-col gap-3 border-l pl-4">
          {/* Next Steps Section */}
          <div className="space-y-0.5">
            {/* Next steps label (left) and date (right) */}
            <div className="flex items-center justify-between gap-2">
              <h4 className="text-sm font-semibold">Next Steps</h4>
              {conv.nextStepDate && (
                <Badge variant="secondary" className="shrink-0 rounded-sm text-xs font-medium">
                  {new Date(conv.nextStepDate).toLocaleDateString('en-US', {
                    month: 'short',
                    day: 'numeric',
                  })}
                </Badge>
              )}
            </div>

            {/* Next steps text */}
            {conv.nextSteps && <div className="whitespace-pre-wrap text-sm">{conv.nextSteps}</div>}
          </div>

          {/* Tasks */}
          {userTasks.length > 0 && (
            <div className="mt-1 space-y-2">
              <div className="flex items-center gap-2">
                <h4 className="text-sm font-semibold">Tasks</h4>
                <Badge variant="secondary" className="text-xs">
                  {userTasks.length}
                </Badge>
              </div>
              <div className="space-y-1">
                {userTasks.slice(0, 1).map((task) => {
                  // The output axis decides what this task produces; payload presence on it
                  // decides whether the artifact already exists.
                  const output = task.taskOutput;
                  const metaDescription =
                    output?.kind === 'email'
                      ? output.draftId
                        ? 'Email draft ready to send'
                        : 'Reply needed'
                      : output?.kind === 'slack'
                        ? `Reply in ${output.channelName || 'Slack'}`
                        : task.description || 'Task';

                  return (
                    <div
                      key=[redacted]
                      className={cn(
                        'flex items-center gap-2 rounded-md border px-2 py-1.5 transition-colors',
                        'border-orange-200 bg-orange-50/50 dark:border-orange-900/50 dark:bg-orange-950/20',
                      )}
                    >
                      <div className="flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-orange-100 text-orange-600 dark:bg-orange-900/50 dark:text-orange-400">
                        <ListTodo className="h-3 w-3" />
                      </div>
                      <div className="min-w-0 flex-1">
                        <span className="truncate text-xs font-medium">{metaDescription}</span>
                      </div>
                      <Circle className="h-2.5 w-2.5 flex-shrink-0 text-orange-300 dark:text-orange-700" />
                    </div>
                  );
                })}
                {userTasks.length > 1 && (
                  <div className="text-muted-foreground text-xs">
                    +{userTasks.length - 1} more task{userTasks.length - 1 !== 1 ? 's' : ''}
                  </div>
                )}
              </div>
            </div>
          )}
        </div>
      </div>
    </KanbanCard>
  );
});