Initial commit - copied workspace after database cleanup

This commit is contained in:
RobbStarkAustria
2025-10-10 15:20:14 +00:00
commit 1efe40a03b
142 changed files with 23625 additions and 0 deletions

View File

@@ -0,0 +1,519 @@
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 { 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 <b>${name}</b> wirklich löschen?`,
clientsMoved: 'Alle Clients werden in "Nicht zugeordnet" verschoben.',
groupCreated: 'Gruppe angelegt',
groupDeleted: 'Gruppe gelöscht. Clients in "Nicht zugeordnet" verschoben',
groupRenamed: 'Gruppenname geändert',
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<KanbanClient[]>([]);
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<KanbanComponentType | null>(null); // Ref für Kanban
// Lade Gruppen und Clients
useEffect(() => {
let groupMap: Record<number, string> = {};
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 {
alert('Fehler beim Aktualisieren der Clients');
}
setDraggedCard(null);
};
// Spalten-Array ohne Header-Buttons/Template
const kanbanColumns = groups.map(group => ({
keyField: group.keyField,
headerText: group.headerText,
}));
return (
<div id="dialog-target">
<h2 className="text-xl font-bold mb-4">{de.title}</h2>
<div className="flex gap-2 mb-4">
<button
className="px-4 py-2 bg-blue-500 text-white rounded"
onClick={() => setShowDialog(true)}
>
{de.newGroup}
</button>
<button
className="px-4 py-2 bg-yellow-500 text-white rounded"
onClick={() => setRenameDialog({ open: true, oldName: '', newName: '' })}
>
{de.renameGroup}
</button>
<button
className="px-4 py-2 bg-red-500 text-white rounded"
onClick={() => setDeleteDialog({ open: true, groupName: '' })}
>
{de.deleteGroup}
</button>
</div>
<KanbanComponent
locale="de"
id="kanban"
keyField="Status"
dataSource={clients}
cardSettings={{
headerField: 'Summary',
selectionType: 'Multiple',
}}
allowDragAndDrop={true}
dragStart={handleDragStart}
dragStop={handleCardDrop}
ref={kanbanRef}
columns={kanbanColumns}
/>
{showDialog && (
<div className="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center">
<div className="bg-white p-6 rounded shadow">
<h3 className="mb-2 font-bold">{de.newGroup}</h3>
<input
className="border p-2 mb-2 w-full"
value={newGroupName}
onChange={e => setNewGroupName(e.target.value)}
placeholder="Raumname"
/>
<div className="flex gap-2">
<button className="bg-blue-500 text-white px-4 py-2 rounded" onClick={handleAddGroup}>
{de.add}
</button>
<button
className="bg-gray-300 px-4 py-2 rounded"
onClick={() => setShowDialog(false)}
>
{de.cancel}
</button>
</div>
</div>
</div>
)}
{renameDialog.open && (
<div className="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center">
<div className="bg-white p-6 rounded shadow">
<h3 className="mb-2 font-bold">{de.renameGroup}</h3>
<select
className="border p-2 mb-2 w-full"
value={renameDialog.oldName}
onChange={e =>
setRenameDialog({
...renameDialog,
oldName: e.target.value,
newName: e.target.value,
})
}
>
<option value="">{de.selectGroup}</option>
{groups
.filter(g => g.headerText !== 'Nicht zugeordnet')
.map(g => (
<option key={g.keyField} value={g.headerText}>
{g.headerText}
</option>
))}
</select>
<input
className="border p-2 mb-2 w-full"
value={renameDialog.newName}
onChange={e => setRenameDialog({ ...renameDialog, newName: e.target.value })}
placeholder={de.newName}
/>
<div className="flex gap-2">
<button
className="bg-blue-500 text-white px-4 py-2 rounded"
onClick={handleRenameGroup}
disabled={!renameDialog.oldName || !renameDialog.newName}
>
{de.rename}
</button>
<button
className="bg-gray-300 px-4 py-2 rounded"
onClick={() => setRenameDialog({ open: false, oldName: '', newName: '' })}
>
{de.cancel}
</button>
</div>
</div>
</div>
)}
{deleteDialog.open && (
<div className="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center">
<div className="bg-white p-6 rounded shadow">
<h3 className="mb-2 font-bold">{de.deleteGroup}</h3>
<select
className="border p-2 mb-2 w-full"
value={deleteDialog.groupName}
onChange={e => setDeleteDialog({ ...deleteDialog, groupName: e.target.value })}
>
<option value="">{de.selectGroup}</option>
{groups
.filter(g => g.headerText !== 'Nicht zugeordnet')
.map(g => (
<option key={g.keyField} value={g.headerText}>
{g.headerText}
</option>
))}
</select>
<p>{de.clientsMoved}</p>
{deleteDialog.groupName && (
<div className="bg-yellow-100 text-yellow-800 p-2 rounded mb-2 text-sm">
<strong>{de.warning}</strong> Möchten Sie die Gruppe <b>{deleteDialog.groupName}</b>{' '}
wirklich löschen?
</div>
)}
<div className="flex gap-2 mt-2">
<button
className="bg-red-500 text-white px-4 py-2 rounded"
onClick={() => setShowDeleteConfirm(true)}
disabled={!deleteDialog.groupName}
>
{de.deleteGroup}
</button>
<button
className="bg-gray-300 px-4 py-2 rounded"
onClick={() => setDeleteDialog({ open: false, groupName: '' })}
>
{de.cancel}
</button>
</div>
</div>
{showDeleteConfirm && deleteDialog.groupName && (
<DialogComponent
width="350px"
header={de.confirmDelete}
visible={showDeleteConfirm}
close={() => setShowDeleteConfirm(false)}
footerTemplate={() => (
<div className="flex gap-2 justify-end">
<button
className="bg-red-500 text-white px-4 py-2 rounded"
onClick={() => {
handleDeleteGroup(deleteDialog.groupName);
setShowDeleteConfirm(false);
}}
>
{de.yesDelete}
</button>
<button
className="bg-gray-300 px-4 py-2 rounded"
onClick={() => {
setShowDeleteConfirm(false);
setDeleteDialog({ open: false, groupName: '' });
}}
>
{de.cancel}
</button>
</div>
)}
>
<div>
Möchten Sie die Gruppe <b>{deleteDialog.groupName}</b> wirklich löschen?
<br />
<span className="text-sm text-gray-500">{de.clientsMoved}</span>
</div>
</DialogComponent>
)}
</div>
)}
</div>
);
};
export default Infoscreen_groups;