// TODO: P2-F16 — Replace hardcoded GROUP_FIELDS with shared constant /** * SmartSuite-style Group Panel for Mail. * Multi-field grouping. * Based on contacts/GroupPanel.tsx */ import React, { useState, useRef, useEffect } from 'react'; import { Group as GroupIcon, Plus, X } from 'lucide-react'; type FieldType = 'text' | 'number' | 'date'; interface GroupFieldDef { key: string; label: string; type: FieldType; group?: string; } const GROUP_FIELDS: GroupFieldDef[] = [ { key: 'from', label: 'Absender', type: 'text', group: 'Allgemein' }, { key: 'folder_name', label: 'Ordner', type: 'text', group: 'Allgemein' }, { key: 'is_read', label: 'Gelesen', type: 'text', group: 'Allgemein' }, { key: 'is_flagged', label: 'Markiert', type: 'text', group: 'Allgemein' }, { key: 'has_attachments', label: 'Anhänge', type: 'text', group: 'Allgemein' }, { key: 'date_day', label: 'Datum (Tag)', type: 'date', group: 'Datum' }, { key: 'date_week', label: 'Kalenderwoche', type: 'date', group: 'Datum' }, { key: 'date_month', label: 'Monat', type: 'date', group: 'Datum' }, { key: 'date_year', label: 'Jahr', type: 'date', group: 'Datum' }, ]; export interface GroupCondition { id: string; field: string; } export interface GroupState { conditions: GroupCondition[]; } export const emptyGroupState: GroupState = { conditions: [], }; function getFieldValue(mail: any, field: string): any { // Virtual date grouping fields if (field === 'date_day') { const d = new Date(mail.date); return d.toISOString().split('T')[0]; // YYYY-MM-DD } if (field === 'date_week') { const d = new Date(mail.date); const onejan = new Date(d.getFullYear(), 0, 1); const week = Math.ceil(((d.getTime() - onejan.getTime()) / 86400000 + onejan.getDay() + 1) / 7); return `KW ${week} ${d.getFullYear()}`; } if (field === 'date_month') { const d = new Date(mail.date); const months = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember']; return `${months[d.getMonth()]} ${d.getFullYear()}`; } if (field === 'date_year') { const d = new Date(mail.date); return String(d.getFullYear()); } return (mail as any)[field]; } export interface GroupedMails { key: string; label: string; mails: any[]; subGroups?: GroupedMails[]; } export function applyGrouping(mails: any[], groupState: GroupState): GroupedMails[] { if (!(groupState.conditions?.length ?? 0)) return [{ key: 'all', label: 'Alle', mails }]; const defs = GROUP_FIELDS; function groupRecursive(items: any[], conditions: GroupCondition[], depth: number): GroupedMails[] { if (depth >= conditions.length) return [{ key: 'all', label: 'Alle', mails: items }]; const cond = conditions[depth]; const def = defs.find((f) => f.key === cond.field); if (!def) return [{ key: 'all', label: 'Alle', mails: items }]; const subGroups = new Map(); for (const mail of items) { const val = getFieldValue(mail, cond.field); const groupKey = val == null || val === '' ? '(leer)' : String(val); if (!subGroups.has(groupKey)) subGroups.set(groupKey, []); subGroups.get(groupKey)!.push(mail); } const sortedKeys = Array.from(subGroups.keys()).sort(); return sortedKeys.map((key) => { const groupMails = subGroups.get(key)!; const childGroups = groupRecursive(groupMails, conditions, depth + 1); if (childGroups.length === 1 && childGroups[0].key === 'all') { return { key, label: key, mails: groupMails }; } return { key, label: key, mails: [], subGroups: childGroups }; }); } return groupRecursive(mails, groupState.conditions, 0); } let groupIdCounter = 0; function newGroupId() { return `mgrp-${Date.now()}-${++groupIdCounter}`; } interface MailGroupPanelProps { groupState: GroupState; onGroupChange: (groupState: GroupState) => void; } export function MailGroupPanel({ groupState, onGroupChange }: MailGroupPanelProps) { const [open, setOpen] = useState(false); const btnRef = useRef(null); const panelRef = useRef(null); const [panelPos, setPanelPos] = useState({ top: 0, left: 0 }); useEffect(() => { if (!open) return; const handler = (e: MouseEvent) => { const target = e.target as Node; if (btnRef.current?.contains(target) || panelRef.current?.contains(target)) return; setOpen(false); }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, [open]); const handleToggle = () => { if (!open && btnRef.current) { const rect = btnRef.current.getBoundingClientRect(); const panelWidth = Math.min(400, 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 activeCount = groupState.conditions?.length ?? 0; const addCondition = () => { onGroupChange({ conditions: [...groupState.conditions, { id: newGroupId(), field: 'from' }] }); }; const removeCondition = (id: string) => { onGroupChange({ conditions: groupState.conditions.filter((c) => c.id !== id) }); }; const updateCondition = (id: string, updates: Partial) => { onGroupChange({ conditions: groupState.conditions.map((c) => (c.id === id ? { ...c, ...updates } : c)) }); }; const clearAll = () => onGroupChange({ ...emptyGroupState }); const groupedFields: Record = {}; for (const f of GROUP_FIELDS) { const g = f.group || 'Allgemein'; if (!groupedFields[g]) groupedFields[g] = []; groupedFields[g].push(f); } return ( <>
{open && (
Gruppierung
{activeCount > 0 && }
{groupState.conditions.length === 0 &&
Keine Gruppierung aktiv. Klicke unten um ein Feld hinzuzufügen.
} {groupState.conditions.map((cond, idx) => (
{idx + 1}.
))}
)} ); }