import React, { useEffect, useState, useRef } from 'react'; import { KanbanComponent } from '@syncfusion/ej2-react-kanban'; import { fetchClients, updateClientGroup } from './apiClients'; import { fetchGroups, createGroup, deleteGroup, renameGroup } from './apiGroups'; import type { Client } from './apiClients'; import type { KanbanComponent as KanbanComponentType } from '@syncfusion/ej2-react-kanban'; import { DialogComponent } from '@syncfusion/ej2-react-popups'; import { ButtonComponent } from '@syncfusion/ej2-react-buttons'; import { TextBoxComponent } from '@syncfusion/ej2-react-inputs'; import { DropDownListComponent } from '@syncfusion/ej2-react-dropdowns'; import type { ChangedEventArgs as TextBoxChangedArgs } from '@syncfusion/ej2-react-inputs'; import type { ChangeEventArgs as DropDownChangeArgs } from '@syncfusion/ej2-react-dropdowns'; import { useToast } from './components/ToastProvider'; import { L10n } from '@syncfusion/ej2-base'; interface KanbanClient extends Client { Id: string; Status: string; // Raumgruppe (Gruppenname) Summary: string; // Anzeigename } interface Group { id: number; name: string; // weitere Felder möglich } interface KanbanDragEventArgs { element: HTMLElement | HTMLElement[]; data: KanbanClient | KanbanClient[]; event?: { event?: MouseEvent }; [key: string]: unknown; } interface KanbanComponentWithClear extends KanbanComponentType { clearSelection: () => void; } const de = { title: 'Gruppen', newGroup: 'Neue Raumgruppe', renameGroup: 'Gruppe umbenennen', deleteGroup: 'Gruppe löschen', add: 'Hinzufügen', cancel: 'Abbrechen', rename: 'Umbenennen', confirmDelete: 'Löschbestätigung', reallyDelete: (name: string) => `Möchten Sie die Gruppe ${name} wirklich löschen?`, clientsMoved: 'Alle Clients werden nach "Nicht zugeordnet" verschoben.', groupCreated: 'Gruppe angelegt', groupDeleted: 'Gruppe gelöscht. Alle Clients wurden nach "Nicht zugeordnet" verschoben.', groupRenamed: 'Gruppe umbenannt', selectGroup: 'Gruppe wählen', newName: 'Neuer Name', warning: 'Achtung:', yesDelete: 'Ja, löschen', }; L10n.load({ de: { kanban: { items: 'Clients', addTitle: 'Neue Karte hinzufügen', editTitle: 'Karte bearbeiten', deleteTitle: 'Karte löschen', edit: 'Bearbeiten', delete: 'Löschen', save: 'Speichern', cancel: 'Abbrechen', yes: 'Ja', no: 'Nein', noCard: 'Keine Clients vorhanden', }, }, }); const Infoscreen_groups: React.FC = () => { const toast = useToast(); const [clients, setClients] = useState([]); const [groups, setGroups] = useState<{ keyField: string; headerText: string; id?: number }[]>([]); const [showDialog, setShowDialog] = useState(false); const [newGroupName, setNewGroupName] = useState(''); const [draggedCard, setDraggedCard] = useState<{ id: string; fromColumn: string } | null>(null); const [renameDialog, setRenameDialog] = useState<{ open: boolean; oldName: string; newName: string; }>({ open: false, oldName: '', newName: '' }); const [deleteDialog, setDeleteDialog] = useState<{ open: boolean; groupName: string }>({ open: false, groupName: '', }); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const kanbanRef = useRef(null); // Ref für Kanban // Lade Gruppen und Clients useEffect(() => { let groupMap: Record = {}; fetchGroups().then((groupData: Group[]) => { const kanbanGroups = groupData.map(g => ({ keyField: g.name, headerText: g.name, id: g.id, })); setGroups(kanbanGroups); groupMap = Object.fromEntries(groupData.map(g => [g.id, g.name])); fetchClients().then(data => { setClients( data.map((c, i) => ({ ...c, Id: c.uuid, Status: c.group_id === 1 ? 'Nicht zugeordnet' : typeof c.group_id === 'number' && groupMap[c.group_id] ? groupMap[c.group_id] : 'Nicht zugeordnet', Summary: c.description || `Client ${i + 1}`, })) ); }); }); }, []); // Neue Gruppe anlegen (persistiert per API) const handleAddGroup = async () => { if (!newGroupName.trim()) return; try { const newGroup = await createGroup(newGroupName); toast.show({ content: de.groupCreated, cssClass: 'e-toast-success', timeOut: 5000, showCloseButton: false, }); setGroups([ ...groups, { keyField: newGroup.name, headerText: newGroup.name, id: newGroup.id }, ]); setNewGroupName(''); setShowDialog(false); } catch (err) { toast.show({ content: (err as Error).message, cssClass: 'e-toast-danger', timeOut: 0, showCloseButton: true, }); } }; // Löschen einer Gruppe const handleDeleteGroup = async (groupName: string) => { try { // Clients der Gruppe in "Nicht zugeordnet" verschieben const groupClients = clients.filter(c => c.Status === groupName); if (groupClients.length > 0) { // Ermittle die ID der Zielgruppe "Nicht zugeordnet" const target = groups.find(g => g.headerText === 'Nicht zugeordnet'); if (!target || !target.id) throw new Error('Zielgruppe "Nicht zugeordnet" nicht gefunden'); await updateClientGroup( groupClients.map(c => c.Id), target.id ); } await deleteGroup(groupName); toast.show({ content: de.groupDeleted, cssClass: 'e-toast-success', timeOut: 5000, showCloseButton: false, }); // Gruppen und Clients neu laden const groupData = await fetchGroups(); const groupMap = Object.fromEntries(groupData.map((g: Group) => [g.id, g.name])); setGroups(groupData.map((g: Group) => ({ keyField: g.name, headerText: g.name, id: g.id }))); const data = await fetchClients(); setClients( data.map((c, i) => ({ ...c, Id: c.uuid, Status: typeof c.group_id === 'number' && groupMap[c.group_id] ? groupMap[c.group_id] : 'Nicht zugeordnet', Summary: c.description || `Client ${i + 1}`, })) ); } catch (err) { toast.show({ content: (err as Error).message, cssClass: 'e-toast-danger', timeOut: 0, showCloseButton: true, }); } setDeleteDialog({ open: false, groupName: '' }); }; // Umbenennen einer Gruppe const handleRenameGroup = async () => { try { await renameGroup(renameDialog.oldName, renameDialog.newName); toast.show({ content: de.groupRenamed, cssClass: 'e-toast-success', timeOut: 5000, showCloseButton: false, }); // Gruppen und Clients neu laden const groupData = await fetchGroups(); const groupMap = Object.fromEntries(groupData.map((g: Group) => [g.id, g.name])); setGroups(groupData.map((g: Group) => ({ keyField: g.name, headerText: g.name, id: g.id }))); const data = await fetchClients(); setClients( data.map((c, i) => ({ ...c, Id: c.uuid, Status: typeof c.group_id === 'number' && groupMap[c.group_id] ? groupMap[c.group_id] : 'Nicht zugeordnet', Summary: c.description || `Client ${i + 1}`, })) ); } catch (err) { toast.show({ content: (err as Error).message, cssClass: 'e-toast-danger', timeOut: 0, showCloseButton: true, }); } setRenameDialog({ open: false, oldName: '', newName: '' }); }; const handleDragStart = (args: KanbanDragEventArgs) => { const element = Array.isArray(args.element) ? args.element[0] : args.element; const cardId = element.getAttribute('data-id'); const fromColumn = element.getAttribute('data-key'); setDraggedCard({ id: cardId || '', fromColumn: fromColumn || '' }); }; const handleCardDrop = async (args: KanbanDragEventArgs) => { if (!draggedCard) return; const mouseEvent = args.event?.event; let targetGroupName = ''; if (mouseEvent && mouseEvent.clientX && mouseEvent.clientY) { const targetElement = document.elementFromPoint(mouseEvent.clientX, mouseEvent.clientY); const kanbanColumn = targetElement?.closest('[data-key]') || targetElement?.closest('.e-content-row'); if (kanbanColumn) { const columnKey = kanbanColumn.getAttribute('data-key'); if (columnKey) { targetGroupName = columnKey; } else { const headerElement = kanbanColumn.querySelector('.e-header-text'); targetGroupName = headerElement?.textContent?.trim() || ''; } } } // Fallback if (!targetGroupName) { const targetElement = Array.isArray(args.element) ? args.element[0] : args.element; const cardWrapper = targetElement.closest('.e-card-wrapper'); const contentRow = cardWrapper?.closest('.e-content-row'); const headerText = contentRow?.querySelector('.e-header-text'); targetGroupName = headerText?.textContent?.trim() || ''; } if (!targetGroupName || targetGroupName === draggedCard.fromColumn) { setDraggedCard(null); return; } const dropped = Array.isArray(args.data) ? args.data : [args.data]; const clientIds = dropped.map((card: KanbanClient) => card.Id); try { // Ermittle Zielgruppen-ID anhand des Namens const target = groups.find(g => g.headerText === targetGroupName); if (!target || !target.id) throw new Error('Zielgruppe nicht gefunden'); await updateClientGroup(clientIds, target.id); fetchGroups().then((groupData: Group[]) => { const groupMap = Object.fromEntries(groupData.map(g => [g.id, g.name])); setGroups( groupData.map(g => ({ keyField: g.name, headerText: g.name, id: g.id, })) ); fetchClients().then(data => { setClients( data.map((c, i) => ({ ...c, Id: c.uuid, Status: typeof c.group_id === 'number' && groupMap[c.group_id] ? groupMap[c.group_id] : 'Nicht zugeordnet', Summary: c.description || `Client ${i + 1}`, })) ); // Nach dem Laden: Karten deselektieren setTimeout(() => { (kanbanRef.current as KanbanComponentWithClear)?.clearSelection(); setTimeout(() => { (kanbanRef.current as KanbanComponentWithClear)?.clearSelection(); }, 100); }, 50); }); }); } catch { toast.show({ content: 'Fehler beim Aktualisieren der Clients', cssClass: 'e-toast-danger', timeOut: 0, showCloseButton: true, }); } setDraggedCard(null); }; // Spalten-Array ohne Header-Buttons/Template const kanbanColumns = groups.map(group => ({ keyField: group.keyField, headerText: group.headerText, })); return (

{de.title}

setShowDialog(true)}> {de.newGroup} setRenameDialog({ open: true, oldName: '', newName: '' })}> {de.renameGroup} setDeleteDialog({ open: true, groupName: '' })}> {de.deleteGroup}
{showDialog && ( setShowDialog(false)} target="#dialog-target" width="420px" footerTemplate={() => (
{de.add} setShowDialog(false)}>{de.cancel}
)} >
setNewGroupName(String(args.value ?? ''))} />
)} {renameDialog.open && ( setRenameDialog({ open: false, oldName: '', newName: '' })} target="#dialog-target" width="480px" footerTemplate={() => (
{de.rename} setRenameDialog({ open: false, oldName: '', newName: '' })}> {de.cancel}
)} >
g.headerText !== 'Nicht zugeordnet').map(g => g.headerText)} value={renameDialog.oldName} change={(e: DropDownChangeArgs) => setRenameDialog({ ...renameDialog, oldName: String(e.value ?? ''), newName: String(e.value ?? ''), }) } /> setRenameDialog({ ...renameDialog, newName: String(args.value ?? '') }) } />
)} {deleteDialog.open && ( setDeleteDialog({ open: false, groupName: '' })} target="#dialog-target" width="520px" footerTemplate={() => (
setShowDeleteConfirm(true)} disabled={!deleteDialog.groupName} > {de.deleteGroup} setDeleteDialog({ open: false, groupName: '' })}> {de.cancel}
)} >
g.headerText !== 'Nicht zugeordnet').map(g => g.headerText)} value={deleteDialog.groupName} change={(e: DropDownChangeArgs) => setDeleteDialog({ ...deleteDialog, groupName: String(e.value ?? '') }) } />

{de.clientsMoved}

{deleteDialog.groupName && (
{de.warning} Möchten Sie die Gruppe {deleteDialog.groupName} wirklich löschen?
)}
)} {showDeleteConfirm && deleteDialog.groupName && ( setShowDeleteConfirm(false)} target="#dialog-target" footerTemplate={() => (
{ handleDeleteGroup(deleteDialog.groupName!); setShowDeleteConfirm(false); }} > {de.yesDelete} { setShowDeleteConfirm(false); setDeleteDialog({ open: false, groupName: '' }); }} > {de.cancel}
)} >
Möchten Sie die Gruppe {deleteDialog.groupName} wirklich löschen?
{de.clientsMoved}
)}
); }; export default Infoscreen_groups;