sortable-column-header.tsx11.1 KBView on GitHub
/**
 * Sortable Column Header Component
 * Renders column name with sorting functionality - entire header is clickable
 * For enum fields, shows a popover with unified filter/sort controls
 * For date fields, shows a popover with date filtering controls
 * For binary fields, shows a popover with binary choice filtering
 */

import { BinaryColumnPopover, type BinaryColumnField } from './binary-column-popover';
import { NumberColumnPopover, type NumberColumnField } from './number-column-popover';
import { DateColumnPopover, type DateColumnField } from './date-column-popover';
import { UnifiedColumnPopover } from './unified-column-popover';
import { ArrowUp, ArrowDown, Filter } from 'lucide-react';
import { getColumnIcon } from './editable-column-header';
import type { CRMColumn } from '../store/crmSlice';
import { useCedarStore } from '@/modules/store';

interface SortableColumnHeaderProps {
  colConfig: CRMColumn;
  onUpdate?: (updatedColumn: CRMColumn) => void;
  onDelete?: (columnId: string) => void;
}

const SORTABLE_FIELDS = new Set<string>([
  'type',
  'status',
  'priority',
  'lastContactedAt',
  'nextSteps',
  'currentTasks',
  'history',
  'dealValue',
  'name',
  'crmSynced',
  'hasFutureCalendar',
]);

const ENUM_FIELDS = new Set<string>([
  'type',
  'status',
  'priority',
  'crmSynced',
  'history',
  'hasFutureCalendar',
]);

const DATE_FIELDS = new Set<string>(['lastContactedAt', 'nextSteps']);

const NUMBER_FIELDS = new Set<string>(['dealValue']);

const BINARY_FIELDS = new Set<string>(['currentTasks']);

export function SortableColumnHeader({ colConfig }: SortableColumnHeaderProps) {
  const enumOptions = colConfig.enumOptions;
  const toggleColumnSort = useCedarStore((state) => state.toggleColumnSort);

  const isSortable = SORTABLE_FIELDS.has(colConfig.id);
  const isEnumField = ENUM_FIELDS.has(colConfig.id);
  const isDateField = DATE_FIELDS.has(colConfig.id);
  const isNumberField = NUMBER_FIELDS.has(colConfig.id);
  const isBinaryField = BINARY_FIELDS.has(colConfig.id);

  // Get sort state from column config
  const columnSort = colConfig.sort;
  const isActiveSortField = columnSort?.active ?? false;
  const sortPriority = isActiveSortField && columnSort ? columnSort.priority + 1 : null; // 1-indexed for display
  const sortDirection = columnSort?.direction ?? 'asc';

  // Check if this column has active filters
  const columnFilter = colConfig.filter;
  const hasActiveFilters =
    (columnFilter?.selected?.length || 0) + (columnFilter?.excluded?.length || 0) > 0;
  const hasActiveDateFilter = !!columnFilter?.dateOperator; // Show if any operator is set (including 'empty')
  const hasActiveNumberFilter = !!columnFilter?.numberOperator; // Show if any operator is set (including 'empty')
  const hasActiveBinaryFilter = !!columnFilter?.binaryChoice;

  const handleHeaderClick = () => {
    if (!isSortable) return;
    // For non-enum, non-date, non-binary fields, toggle sort (asc -> desc -> off)
    toggleColumnSort(colConfig.id);
  };

  // Binary field with binary choice popover (e.g., tasks)
  if (isBinaryField) {
    const binaryOptions: [
      { value: 'true'; label: string; color: string },
      { value: 'false'; label: string; color: string },
    ] =
      colConfig.id === 'currentTasks'
        ? [
            {
              value: 'true',
              label: 'Has Todo Tasks',
              color: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
            },
            {
              value: 'false',
              label: 'No Todo Tasks',
              color: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200',
            },
          ]
        : [
            { value: 'true', label: 'Yes', color: 'bg-green-100 text-green-800' },
            { value: 'false', label: 'No', color: 'bg-gray-100 text-gray-800' },
          ];

    return (
      <BinaryColumnPopover
        field={colConfig.id as BinaryColumnField}
        options={binaryOptions}
        title={colConfig.name}
      >
        <button
          type="button"
          className="flex min-w-0 cursor-pointer items-center gap-2 text-left hover:opacity-70"
        >
          <div className="text-muted-foreground shrink-0">{getColumnIcon(colConfig.type)}</div>
          <span className="truncate font-medium">{colConfig.name}</span>
          <div className="ml-auto flex items-center gap-1">
            {hasActiveBinaryFilter && (
              <div className="text-primary flex h-5 w-5 shrink-0 items-center justify-center">
                <Filter className="h-3 w-3" />
              </div>
            )}
            {isActiveSortField && sortPriority && sortPriority > 1 && (
              <div className="bg-primary/10 text-primary rounded-full px-1.5 py-0.5 text-[10px] font-bold">
                {sortPriority}
              </div>
            )}
            {isActiveSortField && (
              <div className="text-primary flex h-5 w-5 shrink-0 items-center justify-center">
                {sortDirection === 'asc' ? (
                  <ArrowUp className="h-3.5 w-3.5" />
                ) : (
                  <ArrowDown className="h-3.5 w-3.5" />
                )}
              </div>
            )}
          </div>
        </button>
      </BinaryColumnPopover>
    );
  }

  // Date field with date filter popover
  if (isDateField) {
    return (
      <DateColumnPopover field={colConfig.id as DateColumnField}>
        <button
          type="button"
          className="flex min-w-0 cursor-pointer items-center gap-2 text-left hover:opacity-70"
        >
          <div className="text-muted-foreground shrink-0">{getColumnIcon(colConfig.type)}</div>
          <span className="truncate font-medium">{colConfig.name}</span>
          <div className="ml-auto flex items-center gap-1">
            {hasActiveDateFilter && (
              <div className="text-primary flex h-5 w-5 shrink-0 items-center justify-center">
                <Filter className="h-3 w-3" />
              </div>
            )}
            {isActiveSortField && sortPriority && sortPriority > 1 && (
              <div className="bg-primary/10 text-primary rounded-full px-1.5 py-0.5 text-[10px] font-bold">
                {sortPriority}
              </div>
            )}
            {isActiveSortField && (
              <div className="text-primary flex h-5 w-5 shrink-0 items-center justify-center">
                {sortDirection === 'asc' ? (
                  <ArrowUp className="h-3.5 w-3.5" />
                ) : (
                  <ArrowDown className="h-3.5 w-3.5" />
                )}
              </div>
            )}
          </div>
        </button>
      </DateColumnPopover>
    );
  }

  // Number field with number filter popover
  if (isNumberField) {
    return (
      <NumberColumnPopover
        field={colConfig.id as NumberColumnField}
        title={colConfig.name}
        prefix={colConfig.id === 'dealValue' ? '$' : undefined}
      >
        <button
          type="button"
          className="flex min-w-0 cursor-pointer items-center gap-2 text-left hover:opacity-70"
        >
          <div className="text-muted-foreground shrink-0">{getColumnIcon(colConfig.type)}</div>
          <span className="truncate font-medium">{colConfig.name}</span>
          <div className="ml-auto flex items-center gap-1">
            {hasActiveNumberFilter && (
              <div className="text-primary flex h-5 w-5 shrink-0 items-center justify-center">
                <Filter className="h-3 w-3" />
              </div>
            )}
            {isActiveSortField && sortPriority && sortPriority > 1 && (
              <div className="bg-primary/10 text-primary rounded-full px-1.5 py-0.5 text-[10px] font-bold">
                {sortPriority}
              </div>
            )}
            {isActiveSortField && (
              <div className="text-primary flex h-5 w-5 shrink-0 items-center justify-center">
                {sortDirection === 'asc' ? (
                  <ArrowUp className="h-3.5 w-3.5" />
                ) : (
                  <ArrowDown className="h-3.5 w-3.5" />
                )}
              </div>
            )}
          </div>
        </button>
      </NumberColumnPopover>
    );
  }

  // Non-enum, non-date, non-number field - simple sortable header
  if (isSortable && !isEnumField && !isNumberField) {
    return (
      <button
        onClick={handleHeaderClick}
        className="flex min-w-0 cursor-pointer items-center gap-2 text-left hover:opacity-70"
      >
        <div className="text-muted-foreground shrink-0">{getColumnIcon(colConfig.type)}</div>
        <span className="truncate font-medium">{colConfig.name}</span>
        <div className="ml-auto flex items-center gap-1">
          {isActiveSortField && sortPriority && sortPriority > 1 && (
            <div className="bg-primary/10 text-primary rounded-full px-1.5 py-0.5 text-[10px] font-bold">
              {sortPriority}
            </div>
          )}
          {isActiveSortField && (
            <div className="text-primary flex h-5 w-5 shrink-0 items-center justify-center">
              {sortDirection === 'asc' ? (
                <ArrowUp className="h-3.5 w-3.5" />
              ) : (
                <ArrowDown className="h-3.5 w-3.5" />
              )}
            </div>
          )}
        </div>
      </button>
    );
  }

  // Enum field with unified filter/sort popover
  // Let Radix handle open state - don't use controlled mode
  if (isEnumField) {
    return (
      <UnifiedColumnPopover
        field={
          colConfig.id as
            | 'status'
            | 'priority'
            | 'type'
            | 'crmSynced'
            | 'history'
            | 'hasFutureCalendar'
        }
        enumOptions={enumOptions}
      >
        <button
          type="button"
          className="flex min-w-0 cursor-pointer items-center gap-2 text-left hover:opacity-70"
        >
          <div className="text-muted-foreground shrink-0">{getColumnIcon(colConfig.type)}</div>
          <span className="truncate font-medium">{colConfig.name}</span>
          <div className="ml-auto flex items-center gap-1">
            {hasActiveFilters && (
              <div className="bg-primary/10 text-primary rounded-full px-1.5 py-0.5 text-[10px] font-bold">
                {(columnFilter?.selected?.length || 0) + (columnFilter?.excluded?.length || 0)}
              </div>
            )}
            {isActiveSortField && sortPriority && sortPriority > 1 && (
              <div className="bg-primary/10 text-primary rounded-full px-1.5 py-0.5 text-[10px] font-bold">
                {sortPriority}
              </div>
            )}
            {isActiveSortField && (
              <div className="text-primary flex h-5 w-5 shrink-0 items-center justify-center">
                {sortDirection === 'asc' ? (
                  <ArrowUp className="h-3.5 w-3.5" />
                ) : (
                  <ArrowDown className="h-3.5 w-3.5" />
                )}
              </div>
            )}
          </div>
        </button>
      </UnifiedColumnPopover>
    );
  }

  // Non-sortable field
  return (
    <div className="flex min-w-0 items-center gap-2">
      <div className="text-muted-foreground shrink-0">{getColumnIcon(colConfig.type)}</div>
      <span className="truncate font-medium">{colConfig.name}</span>
    </div>
  );
}