use-crm-presets.ts17.2 KBView on GitHub
/**
 * useCRMPresets Hook
 *
 * Manages CRM filter/sort presets:
 * - Save current configuration as a preset
 * - Load and apply saved presets
 * - Delete presets
 *
 * Presets are stored in userSettings.crmPresets
 */

import { useSettings } from '@/modules/userSettings/hooks/use-settings';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import type { ColumnFilter, ColumnSort } from '../store/crmSlice';
import { useAOPs } from '@/modules/aop/hooks/use-aops';
import type { CRMPreset } from '@zero/server/schemas';
import { useTRPC } from '@/providers/query-provider';
import { useCedarStore } from '@/modules/store';
import { useCallback, useMemo } from 'react';
import { toast } from 'sonner';

/**
 * Preset configuration to save (extracted from current state)
 */
export interface PresetConfiguration {
  columns: Record<string, { filter?: ColumnFilter; sort?: ColumnSort }>;
  searchQuery?: string;
  // null in array = filter for "no AOP" conversations
  selectedAopIds?: (string | null)[] | null;
  // Visible column IDs (order preserved)
  visibleColumnIds?: string[];
}

/**
 * Hook to manage CRM presets
 */
export function useCRMPresets() {
  const trpc = useTRPC();
  const queryClient = useQueryClient();
  const { data: settingsData, refetch: refetchSettings } = useSettings();
  const { data: aopsData } = useAOPs();

  // Get current column configurations from store
  const columns = useCedarStore((state) => state.columns);
  const searchQuery = useCedarStore((state) => state.searchQuery);
  const selectedAopIds = useCedarStore((state) => state.selectedAopIds);

  // Get the current AOP names for display (includes null for "no AOP" option)
  const selectedAopNames = useMemo((): (string | null)[] => {
    if (!selectedAopIds || selectedAopIds.length === 0) return [];
    return selectedAopIds
      .map((id) => {
        if (id === null) return null; // Preserve null for "no AOP" option
        return aopsData?.aops?.find((a) => a.id === id)?.name ?? null;
      })
      .filter((name): name is string | null => name !== undefined);
  }, [selectedAopIds, aopsData?.aops]);

  // Helper to get AOP names by IDs (includes null for "no AOP" option)
  const getAopNamesByIds = useCallback(
    (aopIds: (string | null)[] | null | undefined): (string | null)[] => {
      if (!aopIds || aopIds.length === 0) return [];
      return aopIds
        .map((id) => {
          if (id === null) return null; // Preserve null for "no AOP" option
          return aopsData?.aops?.find((a) => a.id === id)?.name ?? null;
        })
        .filter((name): name is string | null => name !== undefined);
    },
    [aopsData?.aops],
  );

  // Actions to apply presets
  const setColumnFilter = useCedarStore((state) => state.setColumnFilter);
  const setColumnSort = useCedarStore((state) => state.setColumnSort);
  const setSearchQuery = useCedarStore((state) => state.setSearchQuery);
  const setSelectedAopIds = useCedarStore((state) => state.setSelectedAopIds);
  const clearAllColumnFilters = useCedarStore((state) => state.clearAllColumnFilters);
  const clearAllSorts = useCedarStore((state) => state.clearAllSorts);
  const setColumnVisibilities = useCedarStore((state) => state.setColumnVisibilities);
  const reorderColumns = useCedarStore((state) => state.reorderColumns);
  const getAllColumns = useCedarStore((state) => state.getAllColumns);

  // Save settings mutation
  const { mutateAsync: saveUserSettings, isPending: isSaving } = useMutation({
    ...trpc.settings.save.mutationOptions(),
    onSuccess: () => {
      queryClient.invalidateQueries({
        queryKey=[redacted],
      });
    },
  });

  // Get current presets from settings
  const presets = useMemo(() => {
    return settingsData?.settings?.crmPresets ?? [];
  }, [settingsData?.settings?.crmPresets]);

  // Extract current configuration for saving
  const getCurrentConfiguration = useCallback((): PresetConfiguration => {
    const columnConfigs: Record<string, { filter?: ColumnFilter; sort?: ColumnSort }> = {};

    Object.entries(columns).forEach(([columnId, column]) => {
      // Only save columns that have active filters or sorts
      if (column.filter || column.sort?.active) {
        columnConfigs[columnId] = {
          filter: column.filter,
          sort: column.sort,
        };
      }
    });

    // Get visible column IDs in order
    const visibleColumnIds = Object.values(columns)
      .filter((col) => col.visible)
      .sort((a, b) => a.order - b.order)
      .map((col) => col.id);

    return {
      columns: columnConfigs,
      searchQuery,
      selectedAopIds,
      visibleColumnIds,
    };
  }, [columns, searchQuery, selectedAopIds]);

  // Check if there are any active filters, sorts, or AOP selection
  // Always allow saving if there's an AOP selected (which there usually is)
  const hasActiveConfiguration = useMemo(() => {
    const config = getCurrentConfiguration();
    return (
      Object.keys(config.columns).length > 0 ||
      (config.searchQuery && config.searchQuery.length > 0) ||
      (config.selectedAopIds && config.selectedAopIds.length > 0)
    );
  }, [getCurrentConfiguration]);

  // Save current configuration as a new preset
  const savePreset = useCallback(
    async (name: string) => {
      const currentConfig = getCurrentConfiguration();
      const newPreset: CRMPreset = {
        id: crypto.randomUUID(),
        name,
        createdAt: new Date().toISOString(),
        columns: currentConfig.columns,
        searchQuery: currentConfig.searchQuery,
        selectedAopIds: currentConfig.selectedAopIds,
        visibleColumnIds: currentConfig.visibleColumnIds,
      };

      const existingPresets = settingsData?.settings?.crmPresets ?? [];
      const updatedPresets = [...existingPresets, newPreset];

      try {
        await saveUserSettings({
          crmPresets: updatedPresets,
        });
        await refetchSettings();
        return newPreset;
      } catch (error) {
        console.error('Failed to save preset:', error);
        toast.error('Failed to save preset');
        throw error;
      }
    },
    [getCurrentConfiguration, settingsData?.settings, saveUserSettings, refetchSettings],
  );

  // Apply a preset (load its configuration into the store)
  const applyPreset = useCallback(
    (preset: CRMPreset) => {
      // First clear all existing filters and sorts
      clearAllColumnFilters();
      clearAllSorts();

      // Apply AOP selection if preset has one
      if (preset.selectedAopIds !== undefined) {
        setSelectedAopIds(preset.selectedAopIds);
      }

      // Apply search query
      setSearchQuery(preset.searchQuery);

      // Apply column filters and sorts
      Object.entries(preset.columns).forEach(([columnId, config]) => {
        if (config.filter) {
          setColumnFilter(columnId, config.filter);
        }
        if (config.sort) {
          setColumnSort(columnId, config.sort);
        }
      });

      // Apply visible columns if preset has them
      if (preset.visibleColumnIds && preset.visibleColumnIds.length > 0) {
        const allColumns = getAllColumns();
        const visibleSet = new Set(preset.visibleColumnIds);
        const visibilities: Record<string, boolean> = {};

        allColumns.forEach((col) => {
          // primaryCompany is always visible
          if (col.id === 'primaryCompany') {
            visibilities[col.id] = true;
          } else {
            visibilities[col.id] = visibleSet.has(col.id);
          }
        });

        setColumnVisibilities(visibilities);

        // Also restore column order to match the preset's saved order
        reorderColumns(preset.visibleColumnIds);
      }

    },
    [
      clearAllColumnFilters,
      clearAllSorts,
      setSelectedAopIds,
      setSearchQuery,
      setColumnFilter,
      setColumnSort,
      getAllColumns,
      setColumnVisibilities,
      reorderColumns,
    ],
  );

  // Delete a preset
  const deletePreset = useCallback(
    async (presetId: string) => {
      const existingPresets = settingsData?.settings?.crmPresets ?? [];
      const updatedPresets = existingPresets.filter((p) => p.id !== presetId);

      try {
        await saveUserSettings({
          crmPresets: updatedPresets,
        });
        await refetchSettings();
      } catch (error) {
        console.error('Failed to delete preset:', error);
        toast.error('Failed to delete preset');
        throw error;
      }
    },
    [settingsData?.settings, saveUserSettings, refetchSettings],
  );

  /**
   * Structured summary item for preset display
   * Values type now includes both included and excluded values in the same item
   */
  type PresetSummaryItem =
    | {
        type: 'values';
        field: string;
        label: string;
        includedValues: (string | null)[];
        excludedValues: (string | null)[];
      }
    | { type: 'text'; field: string; label: string; text: string }
    | { type: 'range'; field: string; label: string; min: string; max: string }
    | {
        type: 'sort';
        field: string;
        label: string;
        direction: string;
        order: (string | null)[] | undefined;
      };

  // Get structured summary items for a preset (for colored badge display)
  const getPresetSummaryItems = useCallback(
    (preset: CRMPreset): PresetSummaryItem[] => {
      const items: PresetSummaryItem[] = [];

      // AOP/Type as a filter
      if (preset.selectedAopIds && preset.selectedAopIds.length > 0) {
        const aopNames = getAopNamesByIds(preset.selectedAopIds);
        if (aopNames.length > 0) {
          items.push({
            type: 'values',
            field: 'type',
            label: 'types',
            includedValues: aopNames,
            excludedValues: [],
          });
        }
      }

      // Add each filter with structured data - group included and excluded by field
      Object.entries(preset.columns).forEach(([columnId, config]) => {
        if (config.filter) {
          const label = columnId.replace('wm_', '');
          const hasSelected = config.filter.selected && config.filter.selected.length > 0;
          const hasExcluded = config.filter.excluded && config.filter.excluded.length > 0;

          if (hasSelected || hasExcluded) {
            items.push({
              type: 'values',
              field: columnId,
              label,
              includedValues: config.filter.selected || [],
              excludedValues: config.filter.excluded || [],
            });
          }

          if (config.filter.searchText) {
            items.push({
              type: 'text',
              field: columnId,
              label,
              text: config.filter.searchText,
            });
          }
          if (config.filter.min || config.filter.max) {
            items.push({
              type: 'range',
              field: columnId,
              label,
              min: config.filter.min || '0',
              max: config.filter.max || '∞',
            });
          }
          if (config.filter.dateFrom || config.filter.dateTo) {
            items.push({
              type: 'range',
              field: columnId,
              label,
              min: config.filter.dateFrom || 'any',
              max: config.filter.dateTo || 'any',
            });
          }
        }
      });

      // Add sorts
      Object.entries(preset.columns).forEach(([columnId, config]) => {
        if (config.sort?.active) {
          items.push({
            type: 'sort',
            field: columnId,
            label: columnId.replace('wm_', ''),
            direction: config.sort.direction,
            order: config.sort.order,
          });
        }
      });

      // Search query
      if (preset.searchQuery) {
        items.push({
          type: 'text',
          field: 'search',
          label: 'search',
          text: preset.searchQuery,
        });
      }

      return items;
    },
    [getAopNamesByIds],
  );

  // Format filter values for display (simple text version)
  const formatFilterValues = useCallback((values: (string | null)[] | undefined): string => {
    if (!values || values.length === 0) return '';
    return values.map((v) => (v === null ? '(empty)' : v)).join(', ');
  }, []);

  // Get a simple text summary of a preset's configuration (for backward compatibility)
  const getPresetSummary = useCallback(
    (preset: CRMPreset): string[] => {
      const summary: string[] = [];

      // AOP/Type as a filter
      if (preset.selectedAopIds && preset.selectedAopIds.length > 0) {
        const aopNames = getAopNamesByIds(preset.selectedAopIds);
        if (aopNames.length > 0) {
          summary.push(`types: [${aopNames.join(', ')}]`);
        }
      }

      // Add each filter with its values
      Object.entries(preset.columns).forEach(([columnId, config]) => {
        if (config.filter) {
          const label = columnId.replace('wm_', '');

          if (config.filter.selected?.length) {
            summary.push(`${label}: [${formatFilterValues(config.filter.selected)}]`);
          }
          if (config.filter.excluded?.length) {
            summary.push(`${label}: [⊘ ${formatFilterValues(config.filter.excluded)}]`);
          }
          if (config.filter.searchText) {
            summary.push(`${label}: ["${config.filter.searchText}"]`);
          }
          if (config.filter.min || config.filter.max) {
            const min = config.filter.min || '0';
            const max = config.filter.max || '∞';
            summary.push(`${label}: [${min} - ${max}]`);
          }
          if (config.filter.dateFrom || config.filter.dateTo) {
            const from = config.filter.dateFrom || 'any';
            const to = config.filter.dateTo || 'any';
            summary.push(`${label}: [${from} to ${to}]`);
          }
        }
      });

      // Add sorts with direction
      Object.entries(preset.columns).forEach(([columnId, config]) => {
        if (config.sort?.active) {
          const label = columnId.replace('wm_', '');
          summary.push(`sort: ${label} [${config.sort.direction}]`);
        }
      });

      // Search query
      if (preset.searchQuery) {
        summary.push(`search: ["${preset.searchQuery}"]`);
      }

      return summary;
    },
    [getAopNamesByIds, formatFilterValues],
  );

  // Check if a preset matches the current configuration
  const isPresetActive = useCallback(
    (preset: CRMPreset): boolean => {
      const currentConfig = getCurrentConfiguration();

      // Check if AOPs match
      const presetAopIds = preset.selectedAopIds;
      const currentAopIds = currentConfig.selectedAopIds;

      // Compare AOP arrays
      if (presetAopIds === null && currentAopIds !== null) return false;
      if (presetAopIds !== null && currentAopIds === null) return false;
      if (presetAopIds === undefined && currentAopIds !== null && currentAopIds !== undefined)
        return false;
      if (presetAopIds && currentAopIds) {
        if (presetAopIds.length !== currentAopIds.length) return false;
        const sortedPreset = [...presetAopIds].sort();
        const sortedCurrent = [...currentAopIds].sort();
        if (!sortedPreset.every((id, i) => id === sortedCurrent[i])) return false;
      }

      // Check if search query matches
      if (preset.searchQuery !== currentConfig.searchQuery) {
        return false;
      }

      // Get all column IDs from both current and preset
      const currentColumnIds = new Set(Object.keys(currentConfig.columns));
      const presetColumnIds = new Set(Object.keys(preset.columns));

      // Check if the same columns have filters/sorts
      if (currentColumnIds.size !== presetColumnIds.size) {
        return false;
      }

      for (const columnId of currentColumnIds) {
        if (!presetColumnIds.has(columnId)) {
          return false;
        }
      }

      // Compare each column's filter and sort configuration
      for (const columnId of currentColumnIds) {
        const currentCol = currentConfig.columns[columnId];
        const presetCol = preset.columns[columnId];

        // Compare filters
        const currentFilter = currentCol.filter;
        const presetFilter = presetCol.filter;

        if (JSON.stringify(currentFilter) !== JSON.stringify(presetFilter)) {
          return false;
        }

        // Compare sorts
        const currentSort = currentCol.sort;
        const presetSort = presetCol.sort;

        if (JSON.stringify(currentSort) !== JSON.stringify(presetSort)) {
          return false;
        }
      }

      // Check if visible columns match (if preset has visibleColumnIds)
      if (preset.visibleColumnIds && preset.visibleColumnIds.length > 0) {
        const currentVisibleIds = currentConfig.visibleColumnIds || [];
        if (currentVisibleIds.length !== preset.visibleColumnIds.length) {
          return false;
        }
        const sortedPresetVisible = [...preset.visibleColumnIds].sort();
        const sortedCurrentVisible = [...currentVisibleIds].sort();
        if (!sortedPresetVisible.every((id, i) => id === sortedCurrentVisible[i])) {
          return false;
        }
      }

      return true;
    },
    [getCurrentConfiguration],
  );

  return {
    presets,
    isSaving,
    hasActiveConfiguration,
    getCurrentConfiguration,
    savePreset,
    applyPreset,
    deletePreset,
    getPresetSummary,
    getPresetSummaryItems,
    isPresetActive,
    selectedAopNames,
    getAopNamesByIds,
  };
}