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 { Group as GroupIcon, Plus, X } from 'lucide-react';
|
||||
import type { UnifiedContact } from '@/api/unifiedContacts';
|
||||
import { useCustomFieldDefinitions } from '@/api/customFieldDefinitions';
|
||||
|
||||
// ─── Field definitions ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -15,10 +16,10 @@ interface GroupFieldDef {
|
||||
key: string;
|
||||
label: string;
|
||||
type: FieldType;
|
||||
group?: 'general' | 'company' | 'person' | 'address' | 'notes' | 'meta';
|
||||
group?: 'general' | 'company' | 'person' | 'address' | 'notes' | 'meta' | 'custom';
|
||||
}
|
||||
|
||||
const GROUP_FIELDS: GroupFieldDef[] = [
|
||||
let GROUP_FIELDS: GroupFieldDef[] = [
|
||||
// General
|
||||
{ key: 'type', label: 'Typ', type: 'select', group: 'general' },
|
||||
{ key: 'displayname', label: 'Anzeigename', type: 'text', group: 'general' },
|
||||
@@ -84,6 +85,10 @@ export const emptyGroupState: GroupState = {
|
||||
// ─── Group 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];
|
||||
}
|
||||
|
||||
@@ -93,7 +98,8 @@ export interface GroupedContacts {
|
||||
contacts: UnifiedContact[];
|
||||
}
|
||||
|
||||
export function applyGrouping(contacts: UnifiedContact[], groupState: GroupState): GroupedContacts[] {
|
||||
export function applyGrouping(contacts: UnifiedContact[], groupState: GroupState, allDefs?: GroupFieldDef[]): GroupedContacts[] {
|
||||
const defs = allDefs || GROUP_FIELDS;
|
||||
if (!groupState.conditions.length) {
|
||||
return [{ key: 'all', label: 'Alle', contacts }];
|
||||
}
|
||||
@@ -102,7 +108,7 @@ export function applyGrouping(contacts: UnifiedContact[], groupState: GroupState
|
||||
let groups: GroupedContacts[] = [{ key: 'all', label: 'Alle', contacts: [...contacts] }];
|
||||
|
||||
for (const cond of groupState.conditions) {
|
||||
const def = GROUP_FIELDS.find((f) => f.key === cond.field);
|
||||
const def = defs.find((f) => f.key === cond.field);
|
||||
if (!def) continue;
|
||||
|
||||
const newGroups: GroupedContacts[] = [];
|
||||
@@ -137,17 +143,18 @@ export function applyGrouping(contacts: UnifiedContact[], groupState: GroupState
|
||||
|
||||
// ─── Context-sensitive field ordering ──────────────────────────────────────────
|
||||
|
||||
function getOrderedFields(contactType?: 'company' | 'person' | undefined): GroupFieldDef[] {
|
||||
function getOrderedFields(contactType?: 'company' | 'person' | undefined, allDefs?: GroupFieldDef[]): GroupFieldDef[] {
|
||||
const defs = allDefs || GROUP_FIELDS;
|
||||
if (contactType === 'company') {
|
||||
const order = ['general', 'company', 'address', 'notes', 'meta', 'person'];
|
||||
return [...GROUP_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 [...GROUP_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 [...GROUP_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 ────────────────────────────────────────────────────────────────
|
||||
@@ -169,6 +176,31 @@ export function GroupPanel({ groupState, onGroupChange, contactType }: GroupPane
|
||||
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 GROUP_FIELDS
|
||||
const allGroupFields = useMemo(() => {
|
||||
const baseFields = GROUP_FIELDS.filter((f) => !f.key.startsWith('custom.'));
|
||||
if (!customFieldDefs || customFieldDefs.items.length === 0) return baseFields;
|
||||
const customFields: GroupFieldDef[] = 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) => {
|
||||
@@ -183,12 +215,14 @@ export function GroupPanel({ groupState, onGroupChange, contactType }: GroupPane
|
||||
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, allGroupFields), [contactType, allGroupFields]);
|
||||
const activeCount = groupState.conditions.length;
|
||||
|
||||
const addCondition = () => {
|
||||
@@ -246,6 +280,7 @@ export function GroupPanel({ groupState, onGroupChange, contactType }: GroupPane
|
||||
address: 'Adresse',
|
||||
notes: 'Notizen',
|
||||
meta: 'Metadaten',
|
||||
custom: 'Benutzerdefinierte Felder',
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -280,7 +315,7 @@ export function GroupPanel({ groupState, onGroupChange, contactType }: GroupPane
|
||||
style={{
|
||||
top: panelPos.top,
|
||||
left: panelPos.left,
|
||||
width: '420px',
|
||||
width: 'min(420px, 90vw)',
|
||||
maxHeight: '70vh',
|
||||
overflowY: 'auto',
|
||||
zIndex: 3000,
|
||||
@@ -320,7 +355,7 @@ export function GroupPanel({ groupState, onGroupChange, contactType }: GroupPane
|
||||
{activeCount > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 px-4 py-2 border-b border-secondary-100">
|
||||
{groupState.conditions.map((cond, idx) => {
|
||||
const def = GROUP_FIELDS.find((f) => f.key === cond.field);
|
||||
const def = allGroupFields.find((f) => f.key === cond.field);
|
||||
return (
|
||||
<span
|
||||
key={cond.id}
|
||||
|
||||
Reference in New Issue
Block a user