unified-column-popover.tsx53.2 KBView on GitHub
/**
 * Unified Column Popover Component
 *
 * Combines filtering and sorting functionality for CRM table column headers.
 * Each option can be:
 * - Clicked to select/deselect (for filtering)
 * - Clicked on the exclude button to exclude
 * - Dragged to reorder (for sorting)
 *
 * NOTE: For the 'type' field, selection changes selectedAopId (single source of truth)
 * instead of using column filters. This syncs schema and display filtering.
 */

import {
  closestCenter,
  DndContext,
  DragOverlay,
  PointerSensor,
  useSensor,
  useSensors,
  type DragEndEvent,
  type DragStartEvent,
} from '@dnd-kit/core';
import { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { ArrowUp, Ban, Calendar as CalendarIcon, Check, ChevronDown, GripVertical, X } from 'lucide-react';
import { getEnumColor, sortEnumOptions, type EnumFieldKey } from '@/modules/crm/field-enums';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import type { CanvasFilterSortConfiguration } from '@/modules/cedar-os/src/store/messages/MessageTypes';
import type { ConversationViewConfig } from '@/modules/canvas/types/canvas-types';
import type { ColumnPopoverMode } from './column-popover-presentation';
import { useColumnFilterOverride } from './column-filter-override';
import { useActivateColumnSortAtTop } from '../hooks/use-activate-column-sort';
import type { ColumnFilter } from '@/modules/crm/store/crmSlice';
import { computeDaysOffset, resolveOffsetToDate, formatDaysOffset } from '@/modules/crm/utils/relative-dates';
import { DatePickerWithNaturalInput } from '@/components/ui/date-picker-with-natural-input';
import { Checkbox } from '@/components/ui/checkbox';
import { useCallback, useEffect, useMemo, useState } from 'react';
import type { CrmFieldEnumOption } from '@/modules/crm/types';
import { getEnumDisplayText, getAopColorStyles, getTypeColor } from '@/modules/crm/utils';
import { useAOPs } from '@/modules/aop/hooks/use-aops';
import { Switch } from '@/components/ui/switch';
import { Button } from '@/components/ui/button';
import { useCedarStore } from '@/modules/store';
import { Label } from '@/components/ui/label';
import { CSS } from '@dnd-kit/utilities';
import { createPortal } from 'react-dom';
import { motion } from 'motion/react';
import { format } from 'date-fns';
import { cn } from '@/lib/utils';

type SortableEnumField = EnumFieldKey;

interface OptionValue {
  value: string | null;
  label: string;
}

interface SortableFilterItemProps {
  id: string; // Already converted: null becomes '__null__'
  option: OptionValue;
  field: SortableEnumField;
  isSelected: boolean;
  isExcluded: boolean;
  onToggleSelect: () => void;
  onToggleExclude: () => void;
  enumOptions?: CrmFieldEnumOption[];
  aopColor?: string | null; // AOP color for 'type' field
  /** Select/exclude affordances — off when the popover is scoped to sorting. */
  showFilterControls?: boolean;
  /** Drag handle — position in this list is the custom sort order, so it is sort-only. */
  showDragHandle?: boolean;
}

function SortableFilterItem({
  id,
  option,
  field,
  isSelected,
  isExcluded,
  onToggleSelect,
  onToggleExclude,
  enumOptions,
  aopColor,
  showFilterControls = true,
  showDragHandle = true,
}: SortableFilterItemProps) {
  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
    id,
    animateLayoutChanges: () => true,
  });

  const style = {
    transform: CSS.Transform.toString(transform),
    transition: transition || 'transform 200ms ease',
  };

  // For 'type' field, use AOP color if available
  const colorStyles = field === 'type' ? getAopColorStyles(aopColor) : null;
  const colorClass = colorStyles
    ? ''
    : field === 'type'
      ? getTypeColor(option.value)
      : getEnumColor(option.value, field, enumOptions);

  return (
    <div
      ref={setNodeRef}
      style={style}
      className={cn('flex items-center gap-2 transition-opacity', isDragging && 'opacity-30')}
    >
      {/* Select/Deselect badge - clickable, with drag handle inside */}
      <button
        onClick={showFilterControls ? onToggleSelect : undefined}
        style={colorStyles || undefined}
        className={cn(
          'flex-1 flex items-center gap-1 rounded-sm px-1.5 py-1.5 text-xs font-semibold transition-all',
          colorClass,
          showFilterControls ? 'cursor-pointer hover:scale-[1.01]' : 'cursor-default',
        )}
      >
        {/* Drag handle inside badge — position is the sort order, so it is sort-only */}
        {showDragHandle && (
          <div
            {...attributes}
            {...listeners}
            className="cursor-grab active:cursor-grabbing shrink-0 opacity-40 hover:opacity-70"
            onClick={(e) => e.stopPropagation()}
          >
            <GripVertical className="h-3.5 w-3.5" />
          </div>
        )}
        <span className="flex-1 text-left">{option.label}</span>
        {showFilterControls && isSelected && <Check className="h-3 w-3 shrink-0" />}
      </button>

      {/* Exclude button - hidden for type and crmSynced (single-select only) */}
      {showFilterControls && field !== 'type' && field !== 'crmSynced' && (
        <button
          onClick={onToggleExclude}
          className={cn(
            'shrink-0 rounded-sm border px-2 py-1.5 transition-all hover:scale-105',
            isExcluded
              ? 'border-red-300 bg-red-100 text-red-700 hover:bg-red-200 dark:border-red-800 dark:bg-red-950 dark:text-red-400 dark:hover:bg-red-900'
              : 'border-transparent hover:border-red-200 hover:bg-red-50 hover:text-red-600 dark:hover:border-red-800 dark:hover:bg-red-950/50 dark:hover:text-red-400',
          )}
        >
          <Ban className="h-3.5 w-3.5" />
        </button>
      )}
    </div>
  );
}

// ============================================================
// InlineDateSection — reusable date filter UI for history popover
// Matches the DateColumnPopover style: operator select, DatePickerWithNaturalInput, relative checkbox
// ============================================================

type HistoryDateOp = 'before' | 'after' | 'on' | 'range';

const HISTORY_OPERATOR_LABELS: Record<HistoryDateOp, string> = {
  before: 'Before',
  after: 'After',
  on: 'On',
  range: 'Range',
};

interface InlineDateSectionState {
  operator?: HistoryDateOp;
  date?: Date | null;
  dateTo?: Date | null;
  relative?: boolean;
  daysOffset?: number;
  daysOffsetTo?: number;
}

function InlineDateSection({
  label,
  state,
  onApply,
  onClear,
}: {
  label: string;
  state: InlineDateSectionState;
  onApply: (next: InlineDateSectionState) => void;
  onClear: () => void;
}) {
  const [operatorOpen, setOperatorOpen] = useState(false);
  const [localOperator, setLocalOperator] = useState<HistoryDateOp>(state.operator ?? 'after');

  useEffect(() => {
    if (state.operator) setLocalOperator(state.operator);
  }, [state.operator]);

  const currentOperator = state.operator ?? localOperator;
  const hasActiveFilter = !!state.date;

  const handleOperatorChange = (op: HistoryDateOp) => {
    setLocalOperator(op);
    if (state.date) {
      onApply({ ...state, operator: op, dateTo: op === 'range' ? state.dateTo : undefined, daysOffsetTo: op === 'range' ? state.daysOffsetTo : undefined });
    }
    setOperatorOpen(false);
  };

  const handleDateChange = (date: Date | null) => {
    if (date) {
      const op = currentOperator;
      const offset = computeDaysOffset(date);
      onApply({
        ...state,
        operator: op,
        date,
        daysOffset: state.relative ? offset : undefined,
      });
    } else {
      onClear();
    }
  };

  const handleDateToChange = (date: Date | null) => {
    onApply({
      ...state,
      dateTo: date ?? undefined,
      daysOffsetTo: state.relative && date ? computeDaysOffset(date) : undefined,
    });
  };

  const handleToggleRelative = (checked: boolean) => {
    if (!state.date) return;
    onApply({
      ...state,
      relative: checked,
      daysOffset: checked ? computeDaysOffset(state.date) : undefined,
      daysOffsetTo: checked && state.dateTo ? computeDaysOffset(state.dateTo) : undefined,
    });
  };

  return (
    <div className="space-y-2 pt-2">
      {label && (
        <div className="flex items-center justify-between">
          <span className="text-xs font-medium">{label}</span>
          {hasActiveFilter && (
            <button
              type="button"
              onClick={onClear}
              className="flex h-5 w-5 items-center justify-center rounded-sm text-red-500 transition-colors hover:bg-red-50 hover:text-red-700 dark:hover:bg-red-950/50"
            >
              <X className="h-3 w-3" />
            </button>
          )}
        </div>
      )}

      <div className="flex items-center gap-2">
        {/* Operator select */}
        <Popover open={operatorOpen} onOpenChange={setOperatorOpen}>
          <PopoverTrigger asChild>
            <Button variant="outline" size="sm" className="h-8 min-w-[80px] justify-between text-xs">
              {HISTORY_OPERATOR_LABELS[currentOperator]}
              <ChevronDown className="ml-1 h-3 w-3 opacity-50" />
            </Button>
          </PopoverTrigger>
          <PopoverContent className="w-[100px] p-1" align="start">
            {(Object.keys(HISTORY_OPERATOR_LABELS) as HistoryDateOp[]).map((op) => (
              <button
                key=[redacted]
                type="button"
                onClick={() => handleOperatorChange(op)}
                className={cn(
                  'w-full rounded-sm px-2 py-1.5 text-left text-xs transition-colors',
                  currentOperator === op ? 'bg-primary/10 text-primary font-medium' : 'hover:bg-muted',
                )}
              >
                {HISTORY_OPERATOR_LABELS[op]}
              </button>
            ))}
          </PopoverContent>
        </Popover>

        {/* Primary date picker */}
        <DatePickerWithNaturalInput
          value={state.date ?? null}
          onChange={handleDateChange}
          showTimeInput={false}
          showClearButton={false}
          trigger={
            <Button
              variant="outline"
              size="sm"
              className={cn('h-8 flex-1 justify-start text-xs', !state.date && 'text-muted-foreground')}
            >
              <CalendarIcon className="mr-2 h-3.5 w-3.5" />
              {state.date
                ? state.relative && state.daysOffset !== undefined
                  ? `${formatDaysOffset(state.daysOffset)} (${format(state.date, 'MMM d')})`
                  : format(state.date, 'MMM d, yyyy')
                : currentOperator === 'range'
                  ? 'From...'
                  : 'Select date...'}
            </Button>
          }
        />
      </div>

      {/* Range: second date picker */}
      {currentOperator === 'range' && (
        <div className="flex items-center gap-2">
          <span className="w-[80px] shrink-0 pl-1 text-xs text-muted-foreground">to</span>
          <DatePickerWithNaturalInput
            value={state.dateTo ?? null}
            onChange={handleDateToChange}
            showTimeInput={false}
            showClearButton={false}
            trigger={
              <Button
                variant="outline"
                size="sm"
                className={cn('h-8 flex-1 justify-start text-xs', !state.dateTo && 'text-muted-foreground')}
              >
                <CalendarIcon className="mr-2 h-3.5 w-3.5" />
                {state.dateTo
                  ? state.relative && state.daysOffsetTo !== undefined
                    ? `${formatDaysOffset(state.daysOffsetTo)} (${format(state.dateTo, 'MMM d')})`
                    : format(state.dateTo, 'MMM d, yyyy')
                  : 'To...'}
              </Button>
            }
          />
        </div>
      )}

      {/* Relative checkbox */}
      <div className="flex items-center gap-2">
        <Checkbox
          id={`relative-${label}`}
          checked={state.relative ?? false}
          disabled={!state.date}
          onCheckedChange={(checked) => handleToggleRelative(checked === true)}
        />
        <Label
          htmlFor={`relative-${label}`}
          className={cn(
            'cursor-pointer select-none text-xs',
            !state.date && 'cursor-not-allowed opacity-50',
          )}
        >
          Relative
        </Label>
        {state.relative && (
          <span className="text-[10px] text-muted-foreground">Updates daily</span>
        )}
      </div>
    </div>
  );
}

interface UnifiedColumnPopoverContentProps {
  field: SortableEnumField;
  enumOptions?: CrmFieldEnumOption[];
  open?: boolean;
  onOpenChange?: (open: boolean) => void;
  /** When provided, reads/writes sort+filter state from canvas viewConfig instead of global CRMSlice */
  canvasId?: string;
  /**
   * Canvas-scoped AOP selection. When provided (canvas mode + type field),
   * overrides crmSlice.selectedAopIds so the type popover controls the canvas's
   * own selectedAopIds rather than the global CRM page state.
   */
  canvasSelectedAopIds?: (string | null)[] | null;
  /** Canvas-scoped setter for selectedAopIds. Called when user toggles AOPs in the type popover. */
  canvasSetSelectedAopIds?: (aopIds: (string | null)[] | null) => void;
  /** Which half of the popover to show. Defaults to 'full' (column header). */
  mode?: ColumnPopoverMode;
  /** Hand-off button ("Set filter") rendered below the controls. */
  crossLink?: React.ReactNode;
}

export function UnifiedColumnPopoverContent({
  field,
  enumOptions,
  canvasId,
  canvasSelectedAopIds,
  canvasSetSelectedAopIds,
  mode = 'full',
  crossLink,
}: UnifiedColumnPopoverContentProps) {
  const columns = useCedarStore((state) => state.columns);
  const setColumnFilter = useCedarStore((state) => state.setColumnFilter);
  const setColumnSortCustomOrder = useCedarStore((state) => state.setColumnSortCustomOrder);

  // For type field: use selectedAopIds as single source of truth (multi-select)
  // In canvas mode, use the canvas-scoped values; otherwise use global crmSlice.
  const globalSelectedAopIds = useCedarStore((state) => state.selectedAopIds);
  const globalToggleSelectedAopId = useCedarStore((state) => state.toggleSelectedAopId);
  const { data: aopsData } = useAOPs();

  // Resolve which AOP state to use: canvas-scoped or global
  const isCanvasTypeMode = field === 'type' && canvasId && canvasSetSelectedAopIds;
  const selectedAopIds = isCanvasTypeMode ? (canvasSelectedAopIds ?? null) : globalSelectedAopIds;

  // Canvas-scoped toggle: add/remove a single AOP ID from the selection
  const toggleSelectedAopId = useCallback(
    (aopId: string | null) => {
      if (isCanvasTypeMode && canvasSetSelectedAopIds) {
        const current = canvasSelectedAopIds ?? [];
        const isCurrentlySelected = current.includes(aopId);
        if (isCurrentlySelected) {
          const next = current.filter((id) => id !== aopId);
          canvasSetSelectedAopIds(next.length > 0 ? next : []);
        } else {
          canvasSetSelectedAopIds([...current, aopId]);
        }
      } else {
        globalToggleSelectedAopId(aopId);
      }
    },
    [isCanvasTypeMode, canvasSelectedAopIds, canvasSetSelectedAopIds, globalToggleSelectedAopId],
  );

  // Canvas mode: subscribe to viewConfig for sort/filter state and provide patch helper
  const updateCanvasViewConfig = useCedarStore((state) => state.updateCanvasViewConfig);
  const saveCanvasViewConfig = useCedarStore((state) => state.saveCanvasViewConfig);
  const canvasViewConfig = useCedarStore((state) => {
    if (!canvasId) return null;
    const c = state.canvasesById[canvasId];
    return c?.viewConfig as ConversationViewConfig | null | undefined;
  });
  const canvasFilterSortConfig = canvasId ? (canvasViewConfig?.filterSortConfiguration ?? {}) : null;

  const patchFilterSort = useCallback(
    (patcher: (config: CanvasFilterSortConfiguration) => CanvasFilterSortConfiguration) => {
      if (!canvasId) return;
      const state = useCedarStore.getState();
      const canvas = state.canvasesById[canvasId];
      if (!canvas) return;
      const currentViewConfig = (canvas.viewConfig ?? {}) as ConversationViewConfig;
      const newFilterSort = patcher(currentViewConfig.filterSortConfiguration ?? {});
      updateCanvasViewConfig(canvasId, { ...currentViewConfig, filterSortConfiguration: newFilterSort });
      void saveCanvasViewConfig(canvasId);
    },
    [canvasId, updateCanvasViewConfig, saveCanvasViewConfig],
  );

  // Derive selectedAopNames from selectedAopIds + AOPs data (for display only)
  // Includes null if selectedAopIds contains null (for "no AOP" filter)
  const selectedAopNames = useMemo((): (string | null)[] => {
    if (!selectedAopIds || selectedAopIds.length === 0) return [];
    return selectedAopIds
      .map((id) => {
        if (id === null) return null; // Keep null as-is
        return aopsData?.aops?.find((a) => a.id === id)?.name ?? null;
      })
      .filter((name): name is string | null => name !== undefined);
  }, [selectedAopIds, aopsData?.aops]);

  const [activeId, setActiveId] = useState<string | null>(null);
  const [dragOverlayContainer, setDragOverlayContainer] = useState<HTMLElement | null>(null);
  const [localOrdering, setLocalOrdering] = useState<OptionValue[]>([]);

  // Draft-local override (inbox editor) wins over both store-backed targets.
  const override = useColumnFilterOverride(field);

  /** Single write path for this column's filter, whichever target owns it. */
  const applyColumnFilter = useCallback(
    (filter: ColumnFilter | undefined) => {
      if (override) {
        override.applyFilter(filter);
      } else if (canvasId) {
        patchFilterSort((config) => ({ ...config, [field]: { ...config[field], filter } }));
      } else {
        setColumnFilter(field, filter);
      }
    },
    [override, canvasId, field, patchFilterSort, setColumnFilter],
  );

  // Get column state — from the override, canvas viewConfig, or the CRMSlice
  const column = override
    ? override.entry
    : canvasFilterSortConfig !== null
      ? canvasFilterSortConfig[field]
      : columns[field];
  const columnSort = column?.sort;
  const columnFilter = column?.filter;
  const isActiveSortField = columnSort?.active ?? false;
  const sortDirection = columnSort?.direction ?? 'asc';
  const setColumnSort = useCedarStore((state) => state.setColumnSort);
  // Turning sorting on here makes this column the #1 sort (see useActivateColumnSortAtTop).
  const activateSortAtTop = useActivateColumnSortAtTop(field, canvasId);

  // Get filter values:
  // - For 'type' field: derive from selectedAopIds via selectedAopNames (multi-select)
  // - For other fields: use column filter state
  const selectedValues = useMemo(() => {
    if (field === 'type') {
      return selectedAopNames;
    }
    return columnFilter?.selected || [];
  }, [field, selectedAopNames, columnFilter?.selected]);

  // Excluded values (not used for type since it uses selectedAopIds directly)
  const excludedValues = useMemo(() => {
    if (field === 'type') return [];
    return columnFilter?.excluded || [];
  }, [field, columnFilter?.excluded]);

  // Initialize local ordering from store or default to available values
  useEffect(() => {
    if (!enumOptions || enumOptions.length === 0) {
      setLocalOrdering([]);
      return;
    }

    // Create a map of enum options for quick lookup
    const enumOptionsMap = new Map<string | null, CrmFieldEnumOption>();
    enumOptions.forEach((opt) => {
      enumOptionsMap.set(opt.value, opt);
    });

    // Get valid values from current AOP (this is the source of truth)
    const validValues = new Set(enumOptions.map((opt) => opt.value));

    // Check if we have a custom order saved (from user dragging to reorder)
    const customOrder = columnSort?.order;
    if (customOrder && customOrder.length > 0) {
      // Filter custom order to only include values that exist in current AOP
      const validCustomOrder = customOrder.filter((value) => validValues.has(value));

      // Find any new values in enumOptions that aren't in the custom order
      const newValues = enumOptions
        .filter((opt) => !validCustomOrder.includes(opt.value))
        .sort((a, b) => a.enumOrder - b.enumOrder)
        .map((opt) => opt.value);

      // Combine: existing custom order + new values appended at the end
      const combinedOrder = [...validCustomOrder, ...newValues];

      setLocalOrdering(
        combinedOrder.map((value) => ({
          value,
          label: getEnumDisplayText(value, enumOptions),
        })),
      );
    } else {
      // No custom order - use enumOptions sorted by enumOrder (default order from AOP)
      const sorted = sortEnumOptions(enumOptions);
      setLocalOrdering(
        sorted.map((opt) => ({
          value: opt.value,
          label: getEnumDisplayText(opt.value, enumOptions),
        })),
      );
    }
  }, [field, enumOptions, columnSort?.order]);

  // Create a container for the drag overlay
  useEffect(() => {
    const container = document.createElement('div');
    container.style.position = 'fixed';
    container.style.top = '0';
    container.style.left = '0';
    container.style.zIndex = '10001';
    container.style.pointerEvents = 'none';
    document.body.appendChild(container);
    setDragOverlayContainer(container);

    return () => {
      document.body.removeChild(container);
    };
  }, []);

  const sensors = useSensors(
    useSensor(PointerSensor, {
      activationConstraint: {
        distance: 8,
      },
    }),
  );

  const handleDragStart = (event: DragStartEvent) => {
    setActiveId(event.active.id as string);
  };

  const handleDragEnd = (event: DragEndEvent) => {
    const { active, over } = event;
    setActiveId(null);

    if (!over || active.id === over.id) return;

    setLocalOrdering((items) => {
      // Convert null values to '__null__' for comparison with active.id/over.id
      const oldIndex = items.findIndex((item) => (item.value ?? '__null__') === active.id);
      const newIndex = items.findIndex((item) => (item.value ?? '__null__') === over.id);

      if (oldIndex === -1 || newIndex === -1) return items;

      const newItems = [...items];
      const [movedItem] = newItems.splice(oldIndex, 1);
      newItems.splice(newIndex, 0, movedItem);

      // Update the store with the new ordering
      const newOrder = newItems.map((item) => item.value);
      if (canvasId) {
        patchFilterSort((config) => ({
          ...config,
          [field]: { ...config[field], sort: { ...(config[field]?.sort ?? { active: false, direction: 'asc', priority: 0 }), order: newOrder } },
        }));
      } else {
        setColumnSortCustomOrder(field, newOrder);
      }

      return newItems;
    });
  };

  const handleToggleSortingActive = (enabled: boolean) => {
    if (enabled) {
      // If no custom order exists, initialize with default enum order from AOP
      let order = columnSort?.order;
      if (!order && enumOptions && enumOptions.length > 0) {
        const sorted = sortEnumOptions(enumOptions);
        order = sorted.map((opt) => opt.value);
      }

      // Enable sorting as the #1 sort, keeping existing direction or defaulting to 'asc'
      activateSortAtTop({ active: true, direction: sortDirection, order });
    } else {
      // Disable sorting but preserve custom order for next time
      const savedOrder = columnSort?.order;
      const disabledSort = savedOrder
        ? { active: false, direction: 'asc' as const, priority: 0, order: savedOrder }
        : undefined;
      if (canvasId) {
        patchFilterSort((config) => ({ ...config, [field]: { ...config[field], sort: disabledSort } }));
      } else {
        setColumnSort(field, disabledSort);
      }
    }
  };

  const handleToggleDirection = () => {
    if (!columnSort) return;
    const newDirection = sortDirection === 'asc' ? ('desc' as const) : ('asc' as const);
    const updatedSort = { ...columnSort, direction: newDirection };
    if (canvasId) {
      patchFilterSort((config) => ({ ...config, [field]: { ...config[field], sort: updatedSort } }));
    } else {
      setColumnSort(field, updatedSort);
    }
  };

  const handleToggleSelect = useCallback(
    (value: string | null) => {
      const isSelected = selectedValues.includes(value);
      const isExcluded = excludedValues.includes(value);

      // Type field: uses selectedAopIds as single source of truth (multi-select)
      // Toggle this AOP in/out of the selection
      if (field === 'type') {
        if (value === null) {
          // Toggle null (conversations with no AOP) in/out of selection
          toggleSelectedAopId(null);
        } else {
          // Find the AOP ID by name and toggle it
          const aop = aopsData?.aops?.find((a) => a.name === value);
          if (aop) {
            toggleSelectedAopId(aop.id);
          }
        }
        return;
      }

      // Single-select binary fields: selecting the same value again clears the filter
      if (field === 'crmSynced' || field === 'hasFutureCalendar') {
        applyColumnFilter(isSelected ? undefined : { selected: [value] });
        return;
      }

      // Multi-select behavior for status/priority/responsiveness
      let newFilter: ColumnFilter | undefined;
      if (isSelected) {
        const newSelected = selectedValues.filter((v) => v !== value);
        newFilter = newSelected.length === 0 && excludedValues.length === 0
          ? undefined
          : { selected: newSelected.length > 0 ? newSelected : undefined, excluded: excludedValues.length > 0 ? excludedValues : undefined };
      } else {
        const newSelected = [...selectedValues, value];
        const newExcluded = isExcluded ? excludedValues.filter((v) => v !== value) : excludedValues;
        newFilter = { selected: newSelected, excluded: newExcluded.length > 0 ? newExcluded : undefined };
      }
      applyColumnFilter(newFilter);
    },
    [field, selectedValues, excludedValues, applyColumnFilter, aopsData?.aops, toggleSelectedAopId],
  );

  const handleToggleExclude = useCallback(
    (value: string | null) => {
      const isExcluded = excludedValues.includes(value);
      const isSelected = selectedValues.includes(value);

      let newFilter: ColumnFilter | undefined;
      if (isExcluded) {
        const newExcluded = excludedValues.filter((v) => v !== value);
        newFilter = newExcluded.length === 0 && selectedValues.length === 0
          ? undefined
          : { selected: selectedValues.length > 0 ? selectedValues : undefined, excluded: newExcluded.length > 0 ? newExcluded : undefined };
      } else {
        const newExcluded = [...excludedValues, value];
        const newSelected = isSelected ? selectedValues.filter((v) => v !== value) : selectedValues;
        newFilter = { selected: newSelected.length > 0 ? newSelected : undefined, excluded: newExcluded };
      }
      applyColumnFilter(newFilter);
    },
    [selectedValues, excludedValues, applyColumnFilter],
  );

  const handleClearAll = () => {
    // Type field cannot be cleared (always need an AOP selected)
    if (field === 'type') return;
    // Other fields (CRM Synced, history) can be cleared (shows all conversations)
    applyColumnFilter(undefined);
  };

  const activeValue = useMemo(() => {
    if (!activeId) return null;
    // Handle null values that are represented as '__null__' in drag IDs
    return localOrdering.find((item) => (item.value ?? '__null__') === activeId);
  }, [activeId, localOrdering]);

  // Filter count - for type field, don't count as "filters" since it's required AOP selection
  // For crmSynced, count it as a filter (unlike type which is required)
  const filterCount = field === 'type' ? 0 : selectedValues.length + excludedValues.length;

  // History date filter helpers — shared apply for both event date & latest event date
  const applyHistoryFilter = applyColumnFilter;

  // Derive InlineDateSection state from columnFilter (event date fields)
  const eventDateState: InlineDateSectionState = {
    operator: columnFilter?.dateOperator as HistoryDateOp | undefined,
    date: columnFilter?.dateRelative && columnFilter?.dateFromDaysOffset !== undefined
      ? resolveOffsetToDate(columnFilter.dateFromDaysOffset)
      : columnFilter?.dateFrom ? new Date(columnFilter.dateFrom) : undefined,
    dateTo: columnFilter?.dateOperator === 'range'
      ? (columnFilter?.dateRelative && columnFilter?.dateToDaysOffset !== undefined
        ? resolveOffsetToDate(columnFilter.dateToDaysOffset)
        : columnFilter?.dateTo ? new Date(columnFilter.dateTo) : undefined)
      : undefined,
    relative: columnFilter?.dateRelative ?? false,
    daysOffset: columnFilter?.dateFromDaysOffset,
    daysOffsetTo: columnFilter?.dateToDaysOffset,
  };

  // Derive InlineDateSection state from columnFilter (latest event date fields)
  const latestEventState: InlineDateSectionState = {
    operator: columnFilter?.latestEventOperator as HistoryDateOp | undefined,
    date: columnFilter?.latestEventRelative && columnFilter?.latestEventFromDaysOffset !== undefined
      ? resolveOffsetToDate(columnFilter.latestEventFromDaysOffset)
      : columnFilter?.latestEventFrom ? new Date(columnFilter.latestEventFrom) : undefined,
    dateTo: columnFilter?.latestEventOperator === 'range'
      ? (columnFilter?.latestEventRelative && columnFilter?.latestEventToDaysOffset !== undefined
        ? resolveOffsetToDate(columnFilter.latestEventToDaysOffset)
        : columnFilter?.latestEventTo ? new Date(columnFilter.latestEventTo) : undefined)
      : undefined,
    relative: columnFilter?.latestEventRelative ?? false,
    daysOffset: columnFilter?.latestEventFromDaysOffset,
    daysOffsetTo: columnFilter?.latestEventToDaysOffset,
  };

  const handleApplyEventDate = useCallback(
    (next: InlineDateSectionState) => {
      applyHistoryFilter({
        ...columnFilter,
        dateOperator: next.operator,
        dateFrom: !next.relative && next.date ? next.date.toISOString() : undefined,
        dateTo: next.operator === 'range' && !next.relative && next.dateTo ? next.dateTo.toISOString() : undefined,
        dateRelative: next.relative || undefined,
        dateFromDaysOffset: next.relative ? next.daysOffset : undefined,
        dateToDaysOffset: next.operator === 'range' && next.relative ? next.daysOffsetTo : undefined,
      });
    },
    [columnFilter, applyHistoryFilter],
  );

  const handleClearEventDate = useCallback(() => {
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    const { dateFrom, dateTo, dateOperator, dateRelative, dateFromDaysOffset, dateToDaysOffset, ...rest } = columnFilter || {};
    applyHistoryFilter(Object.keys(rest).length === 0 ? undefined : rest as ColumnFilter);
  }, [columnFilter, applyHistoryFilter]);

  const handleApplyLatestEvent = useCallback(
    (next: InlineDateSectionState) => {
      applyHistoryFilter({
        ...columnFilter,
        latestEventOperator: next.operator,
        latestEventFrom: !next.relative && next.date ? next.date.toISOString() : undefined,
        latestEventTo: next.operator === 'range' && !next.relative && next.dateTo ? next.dateTo.toISOString() : undefined,
        latestEventRelative: next.relative || undefined,
        latestEventFromDaysOffset: next.relative ? next.daysOffset : undefined,
        latestEventToDaysOffset: next.operator === 'range' && next.relative ? next.daysOffsetTo : undefined,
      });
    },
    [columnFilter, applyHistoryFilter],
  );

  const handleToggleEventOccurrenceType = useCallback(
    (eventType: string) => {
      const current = columnFilter?.eventOccurrenceType ?? [];
      const isSelected = current.includes(eventType);
      const next = isSelected ? current.filter((t) => t !== eventType) : [...current, eventType];
      applyHistoryFilter({ ...columnFilter, eventOccurrenceType: next.length > 0 ? next : undefined });
    },
    [columnFilter, applyHistoryFilter],
  );

  const handleClearEventOccurrenceType = useCallback(() => {
    applyHistoryFilter({ ...columnFilter, eventOccurrenceType: undefined });
  }, [columnFilter, applyHistoryFilter]);

  // Clear only latest event date fields (type stays)
  const handleClearLatestEventDate = useCallback(() => {
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    const { latestEventFrom, latestEventTo, latestEventOperator, latestEventRelative, latestEventFromDaysOffset, latestEventToDaysOffset, ...rest } = columnFilter || {};
    applyHistoryFilter(Object.keys(rest).length === 0 ? undefined : rest as ColumnFilter);
  }, [columnFilter, applyHistoryFilter]);

  // Clear all latest event filters (type + date)
  const handleClearLatestEvent = useCallback(() => {
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    const { latestEventFrom, latestEventTo, latestEventOperator, latestEventRelative, latestEventFromDaysOffset, latestEventToDaysOffset, latestEventType, ...rest } = columnFilter || {};
    applyHistoryFilter(Object.keys(rest).length === 0 ? undefined : rest as ColumnFilter);
  }, [columnFilter, applyHistoryFilter]);

  const handleToggleLatestEventType = useCallback(
    (eventType: string) => {
      const current = columnFilter?.latestEventType ?? [];
      const isSelected = current.includes(eventType);
      const next = isSelected ? current.filter((t) => t !== eventType) : [...current, eventType];
      applyHistoryFilter({ ...columnFilter, latestEventType: next.length > 0 ? next : undefined });
    },
    [columnFilter, applyHistoryFilter],
  );

  const hasLatestEventFilter =
    !!(columnFilter?.latestEventType?.length) ||
    !!(columnFilter?.latestEventFrom || columnFilter?.latestEventFromDaysOffset !== undefined);

  // "Last [event type]" — MAX(occurred_at WHERE type IN [...]) semantics
  const lastEventByTypeState: InlineDateSectionState = {
    operator: columnFilter?.lastEventByTypeDateOperator as HistoryDateOp | undefined,
    date: columnFilter?.lastEventByTypeRelative && columnFilter?.lastEventByTypeDaysOffset !== undefined
      ? resolveOffsetToDate(columnFilter.lastEventByTypeDaysOffset)
      : columnFilter?.lastEventByTypeFrom ? new Date(columnFilter.lastEventByTypeFrom) : undefined,
    dateTo: columnFilter?.lastEventByTypeDateOperator === 'range'
      ? (columnFilter?.lastEventByTypeRelative && columnFilter?.lastEventByTypeDaysOffsetTo !== undefined
        ? resolveOffsetToDate(columnFilter.lastEventByTypeDaysOffsetTo)
        : columnFilter?.lastEventByTypeTo ? new Date(columnFilter.lastEventByTypeTo) : undefined)
      : undefined,
    relative: columnFilter?.lastEventByTypeRelative ?? false,
    daysOffset: columnFilter?.lastEventByTypeDaysOffset,
    daysOffsetTo: columnFilter?.lastEventByTypeDaysOffsetTo,
  };

  const handleApplyLastEventByType = useCallback(
    (next: InlineDateSectionState) => {
      applyHistoryFilter({
        ...columnFilter,
        lastEventByTypeDateOperator: next.operator,
        lastEventByTypeFrom: !next.relative && next.date ? next.date.toISOString() : undefined,
        lastEventByTypeTo: next.operator === 'range' && !next.relative && next.dateTo ? next.dateTo.toISOString() : undefined,
        lastEventByTypeRelative: next.relative || undefined,
        lastEventByTypeDaysOffset: next.relative ? next.daysOffset : undefined,
        lastEventByTypeDaysOffsetTo: next.operator === 'range' && next.relative ? next.daysOffsetTo : undefined,
      });
    },
    [columnFilter, applyHistoryFilter],
  );

  const handleClearLastEventByTypeDate = useCallback(() => {
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    const { lastEventByTypeFrom, lastEventByTypeTo, lastEventByTypeDateOperator, lastEventByTypeRelative, lastEventByTypeDaysOffset, lastEventByTypeDaysOffsetTo, ...rest } = columnFilter || {};
    applyHistoryFilter(Object.keys(rest).length === 0 ? undefined : rest as ColumnFilter);
  }, [columnFilter, applyHistoryFilter]);

  const handleClearLastEventByType = useCallback(() => {
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    const { lastEventByTypeFrom, lastEventByTypeTo, lastEventByTypeDateOperator, lastEventByTypeRelative, lastEventByTypeDaysOffset, lastEventByTypeDaysOffsetTo, lastEventByTypeTypes, ...rest } = columnFilter || {};
    applyHistoryFilter(Object.keys(rest).length === 0 ? undefined : rest as ColumnFilter);
  }, [columnFilter, applyHistoryFilter]);

  const handleToggleLastEventByTypeType = useCallback(
    (eventType: string) => {
      const current = columnFilter?.lastEventByTypeTypes ?? [];
      const isSelected = current.includes(eventType);
      const next = isSelected ? current.filter((t) => t !== eventType) : [...current, eventType];
      applyHistoryFilter({ ...columnFilter, lastEventByTypeTypes: next.length > 0 ? next : undefined });
    },
    [columnFilter, applyHistoryFilter],
  );

  const hasLastEventByTypeFilter = !!(
    columnFilter?.lastEventByTypeDateOperator ||
    columnFilter?.lastEventByTypeTypes?.length ||
    columnFilter?.lastEventByTypeFrom
  );

  return (
    <div className="space-y-2">
      {/* Top row: Sort enabled toggle, asc/desc, and Clear filters */}
      <div className="flex items-center justify-between gap-2">
        {/* Left: Sort enabled toggle and asc/desc — hidden for history (sort by event type is removed) */}
        {field !== 'history' && mode !== 'filter' && (
          <div className="flex items-center gap-1.5">
            {mode !== 'sort' && (
              <>
                <Label htmlFor={`sort-toggle-${field}`} className="text-sm font-medium">
                  Sort enabled
                </Label>
                <Switch
                  id={`sort-toggle-${field}`}
                  checked={isActiveSortField}
                  onCheckedChange={handleToggleSortingActive}
                />
              </>
            )}

            {/* Asc/Desc toggle button */}
            <button
              onClick={handleToggleDirection}
              disabled={!isActiveSortField}
              className={cn(
                'flex items-center gap-1 rounded-md px-2 py-0.5 text-xs font-medium transition-all',
                isActiveSortField
                  ? 'bg-muted hover:bg-muted/80 text-foreground'
                  : 'bg-muted/50 text-muted-foreground cursor-not-allowed opacity-50',
              )}
            >
              <motion.div
                animate={{ rotate: sortDirection === 'asc' ? 0 : 180 }}
                transition={{ duration: 0.2, ease: 'easeInOut' }}
              >
                <ArrowUp className="h-3 w-3" />
              </motion.div>
              <span>{sortDirection === 'asc' ? 'Asc' : 'Desc'}</span>
            </button>
          </div>
        )}

        {/* Right: Clear filters button */}
        {mode !== 'sort' && filterCount > 0 && field !== 'type' && (
          <Button
            variant="outline"
            size="sm"
            onClick={handleClearAll}
            className="h-6 shrink-0 gap-1 rounded-md border-red-200 bg-red-50 px-2 text-xs text-red-700 hover:bg-red-100 hover:text-red-800 dark:border-red-800 dark:bg-red-950/50 dark:text-red-400 dark:hover:bg-red-900"
          >
            <X className="h-3 w-3" />
            Clear Filters
          </Button>
        )}
      </div>

      {/* Sortable list - no border, no background (hidden for history field) */}
      {field !== 'history' && <DndContext
        sensors={sensors}
        collisionDetection={closestCenter}
        onDragStart={handleDragStart}
        onDragEnd={handleDragEnd}
      >
        <SortableContext
          items={localOrdering.map((v) => v.value ?? '__null__')}
          strategy={verticalListSortingStrategy}
        >
          <div className="space-y-1.5 overflow-show">
            {localOrdering.map((option) => {
              // For 'type' field, get AOP color
              const matchedAop = field === 'type'
                ? aopsData?.aops?.find((a) => a.name === option.value)
                : undefined;
              const aopColor = matchedAop?.color;
              return (
                <SortableFilterItem
                  key=[redacted] ?? '__null__'}
                  id={option.value ?? '__null__'}
                  option={option}
                  field={field}
                  isSelected={selectedValues.includes(option.value)}
                  isExcluded={excludedValues.includes(option.value)}
                  onToggleSelect={() => handleToggleSelect(option.value)}
                  onToggleExclude={() => handleToggleExclude(option.value)}
                  enumOptions={enumOptions}
                  aopColor={aopColor}
                  showFilterControls={mode !== 'sort'}
                  showDragHandle={mode !== 'filter'}
                />
              );
            })}
          </div>
        </SortableContext>

        {dragOverlayContainer &&
          createPortal(
            <DragOverlay
              dropAnimation={{
                duration: 200,
                easing: 'cubic-bezier(0.18, 0.67, 0.6, 1.22)',
              }}
            >
              {activeValue && (() => {
                // For 'type' field, use AOP color if available
                const activeMatchedAop = field === 'type'
                  ? aopsData?.aops?.find((a) => a.name === activeValue.value)
                  : undefined;
                const activeAopColor = activeMatchedAop?.color;
                const overlayColorStyles = field === 'type' ? getAopColorStyles(activeAopColor) : null;
                const overlayColorClass = overlayColorStyles
                  ? ''
                  : field === 'type'
                    ? getTypeColor(activeValue.value)
                    : getEnumColor(activeValue.value, field, enumOptions);
                return (
                  <div
                    className={cn(
                      'ring-primary/20 flex cursor-grabbing items-center gap-2 rounded-sm px-3 py-1.5 text-xs font-semibold shadow-lg ring-2',
                      overlayColorClass,
                    )}
                    style={{ cursor: 'grabbing', ...overlayColorStyles }}
                  >
                    <GripVertical className="h-3.5 w-3.5 shrink-0 opacity-40" />
                    <span>{getEnumDisplayText(activeValue.value, enumOptions)}</span>
                  </div>
                );
              })()}
            </DragOverlay>,
            dragOverlayContainer,
          )}
      </DndContext>}

      {/* Date Filter Sections - only for history field */}
      {field === 'history' && (
        <div className="mt-1 border-t border-border/50">
          {/* Last Meeting sort control */}
          {canvasId && mode !== 'filter' && (
            <div className="pt-2 pb-1 flex items-center gap-1.5">
              <Label htmlFor="sort-toggle-lastMeetingTime" className="text-sm font-medium">
                Sort by Last Meeting
              </Label>
              <Switch
                id="sort-toggle-lastMeetingTime"
                checked={!!(canvasFilterSortConfig?.['lastMeetingTime']?.sort?.active)}
                onCheckedChange={(checked) => {
                  patchFilterSort((config) => ({
                    ...config,
                    lastMeetingTime: {
                      ...config['lastMeetingTime'],
                      sort: {
                        active: checked,
                        direction: config['lastMeetingTime']?.sort?.direction ?? 'desc',
                        priority: checked
                          ? Math.max(0, ...Object.values(config).map((c) => c.sort?.priority ?? 0)) + 1
                          : (config['lastMeetingTime']?.sort?.priority ?? 0),
                      },
                    },
                  }));
                }}
              />
              <button
                onClick={() => {
                  const current = canvasFilterSortConfig?.['lastMeetingTime']?.sort?.direction ?? 'desc';
                  patchFilterSort((config) => ({
                    ...config,
                    lastMeetingTime: {
                      ...config['lastMeetingTime'],
                      sort: {
                        ...config['lastMeetingTime']?.sort,
                        active: config['lastMeetingTime']?.sort?.active ?? false,
                        direction: current === 'asc' ? 'desc' : 'asc',
                        priority: config['lastMeetingTime']?.sort?.priority ?? 0,
                      },
                    },
                  }));
                }}
                disabled={!(canvasFilterSortConfig?.['lastMeetingTime']?.sort?.active)}
                className={cn(
                  'flex items-center gap-1 rounded-md px-2 py-0.5 text-xs font-medium transition-all',
                  canvasFilterSortConfig?.['lastMeetingTime']?.sort?.active
                    ? 'bg-muted hover:bg-muted/80 text-foreground'
                    : 'bg-muted/50 text-muted-foreground cursor-not-allowed opacity-50',
                )}
              >
                <motion.div
                  animate={{
                    rotate: (canvasFilterSortConfig?.['lastMeetingTime']?.sort?.direction ?? 'desc') === 'asc' ? 0 : 180,
                  }}
                  transition={{ duration: 0.2, ease: 'easeInOut' }}
                >
                  <ArrowUp className="h-3 w-3" />
                </motion.div>
                <span>
                  {(canvasFilterSortConfig?.['lastMeetingTime']?.sort?.direction ?? 'desc') === 'asc'
                    ? 'Oldest first'
                    : 'Newest first'}
                </span>
              </button>
            </div>
          )}
          {/* Event occurrence type badges — combine with event date */}
          {mode !== 'sort' && enumOptions && enumOptions.length > 0 && (
            <div className="pt-2 space-y-1.5">
              <div className="flex items-center justify-between">
                <span className="text-xs font-medium">Event Occurrence</span>
                {columnFilter?.eventOccurrenceType?.length ? (
                  <button
                    type="button"
                    onClick={handleClearEventOccurrenceType}
                    className="flex h-5 w-5 items-center justify-center rounded-sm text-red-500 transition-colors hover:bg-red-50 hover:text-red-700 dark:hover:bg-red-950/50"
                  >
                    <X className="h-3 w-3" />
                  </button>
                ) : null}
              </div>
              <div className="flex flex-wrap gap-1.5">
                {enumOptions.map((opt) => {
                  const isSelected = (columnFilter?.eventOccurrenceType ?? []).includes(opt.value ?? '');
                  return (
                    <button
                      key=[redacted] ?? 'null'}
                      type="button"
                      onClick={() => handleToggleEventOccurrenceType(opt.value ?? '')}
                      className={cn(
                        'rounded-sm px-2 py-1 text-xs font-semibold transition-all hover:scale-105',
                        isSelected
                          ? getEnumColor(opt.value, 'history', enumOptions)
                          : 'bg-muted text-muted-foreground hover:bg-muted/80',
                      )}
                    >
                      {opt.label}
                    </button>
                  );
                })}
              </div>
            </div>
          )}
          <InlineDateSection
            label="Event Date"
            state={eventDateState}
            onApply={handleApplyEventDate}
            onClear={handleClearEventDate}
          />

          {/* Latest Event — type picker + date filter */}
          <div className="mt-1 border-t border-border/30 pt-2 space-y-2">
            <div className="flex items-center justify-between">
              <span className="text-xs font-medium">Latest Event</span>
              {hasLatestEventFilter && (
                <button
                  type="button"
                  onClick={handleClearLatestEvent}
                  className="flex h-5 w-5 items-center justify-center rounded-sm text-red-500 transition-colors hover:bg-red-50 hover:text-red-700 dark:hover:bg-red-950/50"
                >
                  <X className="h-3 w-3" />
                </button>
              )}
            </div>

            {/* Event type badges */}
            {enumOptions && enumOptions.length > 0 && (
              <div className="flex flex-wrap gap-1.5">
                {enumOptions.map((opt) => {
                  const isSelected = (columnFilter?.latestEventType ?? []).includes(opt.value ?? '');
                  return (
                    <button
                      key=[redacted] ?? 'null'}
                      type="button"
                      onClick={() => handleToggleLatestEventType(opt.value ?? '')}
                      className={cn(
                        'rounded-sm px-2 py-1 text-xs font-semibold transition-all hover:scale-105',
                        isSelected
                          ? getEnumColor(opt.value, 'history', enumOptions)
                          : 'bg-muted text-muted-foreground hover:bg-muted/80',
                      )}
                    >
                      {opt.label}
                    </button>
                  );
                })}
              </div>
            )}

            {/* Date filter — no header (parent already has "Latest Event" label) */}
            <InlineDateSection
              label=""
              state={latestEventState}
              onApply={handleApplyLatestEvent}
              onClear={handleClearLatestEventDate}
            />
          </div>

          {/* Last [event type] — MAX(occurred_at WHERE type IN [...]) semantics */}
          <div className="mt-1 border-t border-border/30 pt-2 space-y-2">
            <div className="flex items-center justify-between">
              <span className="text-xs font-medium">Last [event type]</span>
              {hasLastEventByTypeFilter && (
                <button
                  type="button"
                  onClick={handleClearLastEventByType}
                  className="flex h-5 w-5 items-center justify-center rounded-sm text-red-500 transition-colors hover:bg-red-50 hover:text-red-700 dark:hover:bg-red-950/50"
                >
                  <X className="h-3 w-3" />
                </button>
              )}
            </div>
            <p className="text-[10px] text-muted-foreground leading-tight">
              When was the last [type] — ignores later events of other types
            </p>

            {/* Event type badges — same options as Latest Event */}
            {enumOptions && enumOptions.length > 0 && (
              <div className="flex flex-wrap gap-1.5">
                {enumOptions.map((opt) => {
                  const isSelected = (columnFilter?.lastEventByTypeTypes ?? []).includes(opt.value ?? '');
                  return (
                    <button
                      key=[redacted] ?? 'null'}
                      type="button"
                      onClick={() => handleToggleLastEventByTypeType(opt.value ?? '')}
                      className={cn(
                        'rounded-sm px-2 py-1 text-xs font-semibold transition-all hover:scale-105',
                        isSelected
                          ? getEnumColor(opt.value, 'history', enumOptions)
                          : 'bg-muted text-muted-foreground hover:bg-muted/80',
                      )}
                    >
                      {opt.label}
                    </button>
                  );
                })}
              </div>
            )}

            <InlineDateSection
              label=""
              state={lastEventByTypeState}
              onApply={handleApplyLastEventByType}
              onClear={handleClearLastEventByTypeDate}
            />
          </div>
        </div>
      )}

      {/* Instructions at the bottom */}
      {field !== 'history' && (
        <p className="text-muted-foreground pt-1 text-xs leading-relaxed">
          {mode === 'sort' ? (
            'Drag to reorder'
          ) : field === 'type' ? (
            'Click to select'
          ) : field === 'crmSynced' ? (
            'Click to filter'
          ) : (
            <>
              Click to filter • <Ban className="inline h-2.5 w-2.5 align-text-bottom" /> exclude
            </>
          )}
          {mode === 'full' && ' • Drag to reorder'}
        </p>
      )}

      {crossLink}
    </div>
  );
}

interface UnifiedColumnPopoverProps {
  field: SortableEnumField;
  enumOptions?: CrmFieldEnumOption[];
  children: React.ReactElement;
  open?: boolean;
  onOpenChange?: (open: boolean) => void;
  canvasId?: string;
  canvasSelectedAopIds?: (string | null)[] | null;
  canvasSetSelectedAopIds?: (aopIds: (string | null)[] | null) => void;
}

export function UnifiedColumnPopover({
  field,
  enumOptions,
  children,
  open: controlledOpen,
  onOpenChange: controlledOnOpenChange,
  canvasId,
  canvasSelectedAopIds,
  canvasSetSelectedAopIds,
}: UnifiedColumnPopoverProps) {
  const [internalOpen, setInternalOpen] = useState(false);

  const isOpen = controlledOpen !== undefined ? controlledOpen : internalOpen;
  const setIsOpen = controlledOnOpenChange || setInternalOpen;

  const availableValues = enumOptions?.map((opt) => opt.value) || [];

  return (
    <Popover open={isOpen} onOpenChange={setIsOpen}>
      <PopoverTrigger asChild>{children}</PopoverTrigger>
      <PopoverContent className="w-96 p-3" align="start">
        {availableValues.length === 0 ? (
          <div className="text-center py-3 text-muted-foreground text-xs">
            No options available for this column
          </div>
        ) : (
          <UnifiedColumnPopoverContent
            field={field}
            enumOptions={enumOptions}
            canvasId={canvasId}
            canvasSelectedAopIds={canvasSelectedAopIds}
            canvasSetSelectedAopIds={canvasSetSelectedAopIds}
          />
        )}
      </PopoverContent>
    </Popover>
  );
}