workflow delete in ui functional

This commit is contained in:
2025-06-28 06:41:49 +00:00
parent f176c40a02
commit 55ae392411
6 changed files with 346 additions and 751 deletions

View File

@@ -14,3 +14,91 @@ body {
font-family: Inter, 'Segoe UI', Roboto, Arial, sans-serif;
}
:root {
--sidebar-bg: #e5d8c7;
--sidebar-fg: #78591c;
--sidebar-border: #d6c3a6;
}
.sidebar-theme {
background-color: var(--sidebar-bg);
color: var(--sidebar-fg);
font-size: 1.15rem;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
}
.sidebar-theme .sidebar-link {
text-decoration: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar-theme .sidebar-logout {
border: none;
cursor: pointer;
text-align: left;
width: 100%;
font-size: 1.15rem;
}
.sidebar-theme .sidebar-btn,
.sidebar-theme .sidebar-link,
.sidebar-theme .sidebar-logout {
background-color: var(--sidebar-bg);
color: var(--sidebar-fg);
transition: background 0.2s, color 0.2s;
font-weight: 500;
}
.sidebar-theme .sidebar-btn:hover,
.sidebar-theme .sidebar-link:hover,
.sidebar-theme .sidebar-logout:hover {
background-color: var(--sidebar-fg);
color: var(--sidebar-bg);
}
/* Kanban-Karten im Sidebar-Style */
.e-kanban .e-card,
.e-kanban .e-card .e-card-content,
.e-kanban .e-card .e-card-header {
background-color: var(--sidebar-bg) !important;
color: var(--sidebar-fg) !important;
}
.e-kanban .e-card:hover,
.e-kanban .e-card.e-selection,
.e-kanban .e-card.e-card-active,
.e-kanban .e-card:hover .e-card-content,
.e-kanban .e-card.e-selection .e-card-content,
.e-kanban .e-card.e-card-active .e-card-content,
.e-kanban .e-card:hover .e-card-header,
.e-kanban .e-card.e-selection .e-card-header,
.e-kanban .e-card.e-card-active .e-card-header {
background-color: var(--sidebar-fg) !important;
color: var(--sidebar-bg) !important;
}
/* Optional: Fokus-Style für Tastatur-Navigation */
.e-kanban .e-card:focus {
outline: 2px solid var(--sidebar-fg);
outline-offset: 2px;
}
/* Kanban-Spaltenheader: Hintergrund und Textfarbe überschreiben */
.e-kanban .e-kanban-table .e-header-cells {
background-color: color-mix(in srgb, var(--sidebar-bg) 80%, #fff 20%) !important;
color: var(--sidebar-fg) !important;
font-weight: 700;
font-size: 1.08rem;
border-bottom: 2px solid var(--sidebar-fg);
box-shadow: 0 2px 6px 0 color-mix(in srgb, #78591c 8%, transparent);
letter-spacing: 0.02em;
}
/* Header-Text noch spezifischer und mit !important */
.e-kanban .e-kanban-table .e-header-cells .e-header-text {
color: color-mix(in srgb, var(--sidebar-fg) 85%, #000 15%) !important;
}

View File

@@ -34,18 +34,11 @@ const Layout: React.FC = () => {
<div className="flex min-h-screen">
{/* Sidebar */}
<aside
className={`flex flex-col transition-all duration-300 ${collapsed ? 'w-20' : 'w-30'}`}
style={{
backgroundColor: '#e5d8c7',
color: '#78591c',
fontSize: '1.15rem',
fontFamily:
'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif',
}}
className={`sidebar-theme flex flex-col transition-all duration-300 ${collapsed ? 'w-20' : 'w-30'}`}
>
<div
className="h-20 flex items-center justify-center border-b"
style={{ borderColor: '#d6c3a6' }}
style={{ borderColor: 'var(--sidebar-border)' }}
>
<img
src={logo}
@@ -55,21 +48,9 @@ const Layout: React.FC = () => {
/>
</div>
<button
className="p-2 focus:outline-none transition-colors"
style={{
backgroundColor: '#e5d8c7',
color: '#78591c',
}}
className="sidebar-btn p-2 focus:outline-none transition-colors"
onClick={() => setCollapsed(!collapsed)}
aria-label={collapsed ? 'Sidebar ausklappen' : 'Sidebar einklappen'}
onMouseEnter={e => {
e.currentTarget.style.backgroundColor = '#78591c';
e.currentTarget.style.color = '#e5d8c7';
}}
onMouseLeave={e => {
e.currentTarget.style.backgroundColor = '#e5d8c7';
e.currentTarget.style.color = '#78591c';
}}
>
<span style={{ fontSize: 20 }}>{collapsed ? '▶' : '◀'}</span>
</button>
@@ -80,24 +61,8 @@ const Layout: React.FC = () => {
<Link
key={item.path}
to={item.path}
className="flex items-center gap-3 px-6 py-3 transition-colors no-underline"
style={{
color: '#78591c',
textDecoration: 'none',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
fontWeight: 500,
}}
className="sidebar-link flex items-center gap-3 px-6 py-3 transition-colors no-underline"
title={collapsed ? item.name : undefined}
onMouseEnter={e => {
e.currentTarget.style.backgroundColor = '#78591c';
e.currentTarget.style.color = '#e5d8c7';
}}
onMouseLeave={e => {
e.currentTarget.style.backgroundColor = '';
e.currentTarget.style.color = '#78591c';
}}
>
<Icon size={22} />
{!collapsed && item.name}
@@ -108,30 +73,12 @@ const Layout: React.FC = () => {
{/* Abmelden-Button immer ganz unten */}
<div className="mb-4 mt-auto">
<button
className="flex items-center gap-3 px-6 py-3 w-full transition-colors no-underline"
style={{
color: '#78591c',
backgroundColor: '#e5d8c7',
border: 'none',
fontWeight: 500,
cursor: 'pointer',
textAlign: 'left',
fontSize: '1.15rem',
width: '100%',
}}
className="sidebar-logout flex items-center gap-3 px-6 py-3 w-full transition-colors no-underline"
title={collapsed ? 'Abmelden' : undefined}
onClick={() => {
// Hier ggf. Logout-Logik einfügen
window.location.href = '/logout';
}}
onMouseEnter={e => {
e.currentTarget.style.backgroundColor = '#78591c';
e.currentTarget.style.color = '#e5d8c7';
}}
onMouseLeave={e => {
e.currentTarget.style.backgroundColor = '#e5d8c7';
e.currentTarget.style.color = '#78591c';
}}
>
<LogOut size={22} />
{!collapsed && 'Abmelden'}

View File

@@ -14,6 +14,19 @@ export async function fetchGroups() {
return await res.json();
}
export async function deleteGroup(groupName: string) {
// Passe ggf. an deine API an
return fetch(`/api/groups/${encodeURIComponent(groupName)}`, { method: 'DELETE' });
}
export async function renameGroup(oldName: string, newName: string) {
// Passe ggf. an deine API an
return fetch(`/api/groups/${encodeURIComponent(oldName)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ newName }),
});
}
// Hier kannst du später weitere Funktionen ergänzen:
// export async function deleteGroup(id: number) { ... }
// export async function updateGroup(id: number, name: string) { ... }

View File

@@ -1,9 +1,10 @@
import React, { useEffect, useState, useRef } from 'react';
import { KanbanComponent, ColumnsDirective, ColumnDirective } from '@syncfusion/ej2-react-kanban';
import { KanbanComponent } from '@syncfusion/ej2-react-kanban';
import { fetchClients, updateClientGroup } from './apiClients';
import { fetchGroups, createGroup } from './apiGroups';
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';
interface KanbanClient extends Client {
Id: string;
@@ -30,6 +31,16 @@ const Infoscreen_groups: React.FC = () => {
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
@@ -71,6 +82,60 @@ const Infoscreen_groups: React.FC = () => {
}
};
// 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) {
await updateClientGroup(
groupClients.map(c => c.Id),
'Nicht zugeordnet'
);
}
await deleteGroup(groupName);
// 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: groupMap[c.group_id] || 'Nicht zugeordnet',
Summary: c.location || `Client ${i + 1}`,
}))
);
} catch {
alert('Fehler beim Löschen der Gruppe');
}
setDeleteDialog({ open: false, groupName: '' });
};
// Umbenennen einer Gruppe
const handleRenameGroup = async () => {
try {
await renameGroup(renameDialog.oldName, renameDialog.newName);
// 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: groupMap[c.group_id] || 'Nicht zugeordnet',
Summary: c.location || `Client ${i + 1}`,
}))
);
} catch {
alert('Fehler beim Umbenennen der Gruppe');
}
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');
@@ -144,15 +209,35 @@ const Infoscreen_groups: React.FC = () => {
setDraggedCard(null);
};
// Spalten-Array ohne Header-Buttons/Template
const kanbanColumns = groups.map(group => ({
keyField: group.keyField,
headerText: group.headerText,
}));
return (
<div>
<div id="dialog-target">
<h2 className="text-xl font-bold mb-4">Gruppen</h2>
<button
className="mb-4 px-4 py-2 bg-blue-500 text-white rounded"
onClick={() => setShowDialog(true)}
>
Neue Raumgruppe
</button>
<div className="flex gap-2 mb-4">
<button
className="px-4 py-2 bg-blue-500 text-white rounded"
onClick={() => setShowDialog(true)}
>
Neue Raumgruppe
</button>
<button
className="px-4 py-2 bg-yellow-500 text-white rounded"
onClick={() => setRenameDialog({ open: true, oldName: '', newName: '' })}
>
Gruppe umbenennen
</button>
<button
className="px-4 py-2 bg-red-500 text-white rounded"
onClick={() => setDeleteDialog({ open: true, groupName: '' })}
>
Gruppe löschen
</button>
</div>
<KanbanComponent
id="kanban"
keyField="Status"
@@ -164,14 +249,9 @@ const Infoscreen_groups: React.FC = () => {
allowDragAndDrop={true}
dragStart={handleDragStart}
dragStop={handleCardDrop}
ref={kanbanRef} // Ref zuweisen
>
<ColumnsDirective>
{groups.map(group => (
<ColumnDirective key={group.keyField} {...group} />
))}
</ColumnsDirective>
</KanbanComponent>
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">
@@ -196,6 +276,135 @@ const Infoscreen_groups: React.FC = () => {
</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">Raumgruppe umbenennen</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="">Gruppe wählen</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="Neuer Name"
/>
<div className="flex gap-2">
<button
className="bg-blue-500 text-white px-4 py-2 rounded"
onClick={handleRenameGroup}
disabled={!renameDialog.oldName || !renameDialog.newName}
>
Umbenennen
</button>
<button
className="bg-gray-300 px-4 py-2 rounded"
onClick={() => setRenameDialog({ open: false, oldName: '', newName: '' })}
>
Abbrechen
</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">Gruppe löschen</h3>
<select
className="border p-2 mb-2 w-full"
value={deleteDialog.groupName}
onChange={e => setDeleteDialog({ ...deleteDialog, groupName: e.target.value })}
>
<option value="">Gruppe wählen</option>
{groups
.filter(g => g.headerText !== 'Nicht zugeordnet')
.map(g => (
<option key={g.keyField} value={g.headerText}>
{g.headerText}
</option>
))}
</select>
<p>Alle Clients werden in "Nicht zugeordnet" verschoben.</p>
{deleteDialog.groupName && (
<div className="bg-yellow-100 text-yellow-800 p-2 rounded mb-2 text-sm">
<strong>Achtung:</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}
>
Löschen
</button>
<button
className="bg-gray-300 px-4 py-2 rounded"
onClick={() => setDeleteDialog({ open: false, groupName: '' })}
>
Abbrechen
</button>
</div>
</div>
{showDeleteConfirm && deleteDialog.groupName && (
<DialogComponent
width="350px"
header="Löschbestätigung"
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);
}}
>
Ja, löschen
</button>
<button
className="bg-gray-300 px-4 py-2 rounded"
onClick={() => {
setShowDeleteConfirm(false);
setDeleteDialog({ open: false, groupName: '' });
}}
>
Abbrechen
</button>
</div>
)}
>
<div>
Möchten Sie die Gruppe <b>{deleteDialog.groupName}</b> wirklich löschen?
<br />
<span className="text-sm text-gray-500">
Alle Clients werden in "Nicht zugeordnet" verschoben.
</span>
</div>
</DialogComponent>
)}
</div>
)}
</div>
);
};