feat: column visibility, bulk actions, custom sort drag-drop, custom fields in filter/sort/group, mobile optimization
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
import React, { useState, useRef, useEffect, useMemo } from 'react';
|
||||
import { ArrowDownAZ, ArrowUpZA, Plus, X } from 'lucide-react';
|
||||
import type { UnifiedContact } from '@/api/unifiedContacts';
|
||||
import { useCustomFieldDefinitions } from '@/api/customFieldDefinitions';
|
||||
|
||||
// ─── Field definitions (reuse from FilterPanel) ────────────────────────────────
|
||||
|
||||
@@ -15,10 +16,10 @@ interface SortFieldDef {
|
||||
key: string;
|
||||
label: string;
|
||||
type: FieldType;
|
||||
group?: 'general' | 'company' | 'person' | 'address' | 'notes' | 'meta';
|
||||
group?: 'general' | 'company' | 'person' | 'address' | 'notes' | 'meta' | 'custom';
|
||||
}
|
||||
|
||||
const SORT_FIELDS: SortFieldDef[] = [
|
||||
let SORT_FIELDS: SortFieldDef[] = [
|
||||
// General
|
||||
{ key: 'displayname', label: 'Anzeigename', type: 'text', group: 'general' },
|
||||
{ key: 'type', label: 'Typ', type: 'select', group: 'general' },
|
||||
@@ -86,6 +87,10 @@ export const emptySortState: SortState = {
|
||||
// ─── Sort logic ───────────────────────────────────────────────────────────────
|
||||
|
||||
function getFieldValue(contact: UnifiedContact, field: string): any {
|
||||
if (field.startsWith('custom.')) {
|
||||
const customField = field.slice(7);
|
||||
return contact.custom?.[customField];
|
||||
}
|
||||
return (contact as any)[field];
|
||||
}
|
||||
|
||||
@@ -109,13 +114,14 @@ function compareValues(a: any, b: any, fieldType: FieldType): number {
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function applySorting(contacts: UnifiedContact[], sortState: SortState): UnifiedContact[] {
|
||||
export function applySorting(contacts: UnifiedContact[], sortState: SortState, allDefs?: SortFieldDef[]): UnifiedContact[] {
|
||||
if (!sortState.conditions.length) return contacts;
|
||||
|
||||
const defs = allDefs || SORT_FIELDS;
|
||||
const sorted = [...contacts];
|
||||
sorted.sort((a, b) => {
|
||||
for (const cond of sortState.conditions) {
|
||||
const def = SORT_FIELDS.find((f) => f.key === cond.field);
|
||||
const def = defs.find((f) => f.key === cond.field);
|
||||
if (!def) continue;
|
||||
const cmp = compareValues(getFieldValue(a, cond.field), getFieldValue(b, cond.field), def.type);
|
||||
if (cmp !== 0) {
|
||||
@@ -129,17 +135,18 @@ export function applySorting(contacts: UnifiedContact[], sortState: SortState):
|
||||
|
||||
// ─── Context-sensitive field ordering ──────────────────────────────────────────
|
||||
|
||||
function getOrderedFields(contactType?: 'company' | 'person' | undefined): SortFieldDef[] {
|
||||
function getOrderedFields(contactType?: 'company' | 'person' | undefined, allDefs?: SortFieldDef[]): SortFieldDef[] {
|
||||
const defs = allDefs || SORT_FIELDS;
|
||||
if (contactType === 'company') {
|
||||
const order = ['general', 'company', 'address', 'notes', 'meta', 'person'];
|
||||
return [...SORT_FIELDS].sort((a, b) => order.indexOf(a.group || 'general') - order.indexOf(b.group || 'general'));
|
||||
const order = ['general', 'company', 'address', 'notes', 'meta', 'custom', 'person'];
|
||||
return [...defs].sort((a, b) => order.indexOf(a.group || 'general') - order.indexOf(b.group || 'general'));
|
||||
}
|
||||
if (contactType === 'person') {
|
||||
const order = ['general', 'person', 'address', 'notes', 'meta', 'company'];
|
||||
return [...SORT_FIELDS].sort((a, b) => order.indexOf(a.group || 'general') - order.indexOf(b.group || 'general'));
|
||||
const order = ['general', 'person', 'address', 'notes', 'meta', 'custom', 'company'];
|
||||
return [...defs].sort((a, b) => order.indexOf(a.group || 'general') - order.indexOf(b.group || 'general'));
|
||||
}
|
||||
const order = ['general', 'company', 'person', 'address', 'notes', 'meta'];
|
||||
return [...SORT_FIELDS].sort((a, b) => order.indexOf(a.group || 'general') - order.indexOf(b.group || 'general'));
|
||||
const order = ['general', 'company', 'person', 'address', 'notes', 'meta', 'custom'];
|
||||
return [...defs].sort((a, b) => order.indexOf(a.group || 'general') - order.indexOf(b.group || 'general'));
|
||||
}
|
||||
|
||||
// ─── Component ────────────────────────────────────────────────────────────────
|
||||
@@ -161,6 +168,31 @@ export function SortPanel({ sortState, onSortChange, contactType }: SortPanelPro
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
const [panelPos, setPanelPos] = useState({ top: 0, left: 0 });
|
||||
|
||||
// Fetch custom field definitions (Feature 4)
|
||||
const { data: customFieldDefs } = useCustomFieldDefinitions('contact');
|
||||
|
||||
// Merge custom field definitions into SORT_FIELDS
|
||||
const allSortFields = useMemo(() => {
|
||||
const baseFields = SORT_FIELDS.filter((f) => !f.key.startsWith('custom.'));
|
||||
if (!customFieldDefs || customFieldDefs.items.length === 0) return baseFields;
|
||||
const customFields: SortFieldDef[] = customFieldDefs.items
|
||||
.filter((def) => def.is_active)
|
||||
.map((def) => {
|
||||
const fieldType: FieldType =
|
||||
def.field_type === 'number' ? 'number' :
|
||||
def.field_type === 'date' ? 'date' :
|
||||
def.field_type === 'select' || def.field_type === 'multiselect' ? 'select' :
|
||||
'text';
|
||||
return {
|
||||
key: `custom.${def.name}`,
|
||||
label: def.label || def.name,
|
||||
type: fieldType,
|
||||
group: 'custom' as const,
|
||||
};
|
||||
});
|
||||
return [...baseFields, ...customFields];
|
||||
}, [customFieldDefs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handler = (e: MouseEvent) => {
|
||||
@@ -175,12 +207,14 @@ export function SortPanel({ sortState, onSortChange, contactType }: SortPanelPro
|
||||
const handleToggle = () => {
|
||||
if (!open && btnRef.current) {
|
||||
const rect = btnRef.current.getBoundingClientRect();
|
||||
setPanelPos({ top: rect.bottom + 4, left: rect.left });
|
||||
const panelWidth = Math.min(420, window.innerWidth * 0.9);
|
||||
const left = Math.min(rect.left, window.innerWidth - panelWidth - 10);
|
||||
setPanelPos({ top: rect.bottom + 4, left: Math.max(10, left) });
|
||||
}
|
||||
setOpen(!open);
|
||||
};
|
||||
|
||||
const orderedFields = useMemo(() => getOrderedFields(contactType), [contactType]);
|
||||
const orderedFields = useMemo(() => getOrderedFields(contactType, allSortFields), [contactType, allSortFields]);
|
||||
const activeCount = sortState.conditions.length;
|
||||
|
||||
const addCondition = () => {
|
||||
@@ -239,6 +273,7 @@ export function SortPanel({ sortState, onSortChange, contactType }: SortPanelPro
|
||||
address: 'Adresse',
|
||||
notes: 'Notizen',
|
||||
meta: 'Metadaten',
|
||||
custom: 'Benutzerdefinierte Felder',
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -273,7 +308,7 @@ export function SortPanel({ sortState, onSortChange, contactType }: SortPanelPro
|
||||
style={{
|
||||
top: panelPos.top,
|
||||
left: panelPos.left,
|
||||
width: '420px',
|
||||
width: 'min(420px, 90vw)',
|
||||
maxHeight: '70vh',
|
||||
overflowY: 'auto',
|
||||
zIndex: 3000,
|
||||
@@ -313,7 +348,7 @@ export function SortPanel({ sortState, onSortChange, contactType }: SortPanelPro
|
||||
{activeCount > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 px-4 py-2 border-b border-secondary-100">
|
||||
{sortState.conditions.map((cond, idx) => {
|
||||
const def = SORT_FIELDS.find((f) => f.key === cond.field);
|
||||
const def = allSortFields.find((f) => f.key === cond.field);
|
||||
return (
|
||||
<span
|
||||
key={cond.id}
|
||||
@@ -334,7 +369,7 @@ export function SortPanel({ sortState, onSortChange, contactType }: SortPanelPro
|
||||
{/* Sort rows */}
|
||||
<div className="px-4 py-3 space-y-2">
|
||||
{sortState.conditions.map((cond, idx) => {
|
||||
const def = SORT_FIELDS.find((f) => f.key === cond.field);
|
||||
const def = allSortFields.find((f) => f.key === cond.field);
|
||||
return (
|
||||
<div key={cond.id} className="flex items-center gap-1.5">
|
||||
{/* Priority number + reorder */}
|
||||
|
||||
Reference in New Issue
Block a user