workflow delete in ui functional
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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) { ... }
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user