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,
};
}