first kanban-view integration for client groups

This commit is contained in:
2025-06-27 08:22:01 +00:00
parent 9b78db8223
commit f176c40a02
15 changed files with 751 additions and 380 deletions

View File

@@ -8,4 +8,9 @@
@import "../node_modules/@syncfusion/ej2-popups/styles/material.css";
@import "../node_modules/@syncfusion/ej2-splitbuttons/styles/material.css";
@import "../node_modules/@syncfusion/ej2-react-schedule/styles/material.css";
@import "../node_modules/@syncfusion/ej2-kanban/styles/material.css";
body {
font-family: Inter, 'Segoe UI', Roboto, Arial, sans-serif;
}

View File

@@ -12,7 +12,8 @@ import {
User,
Settings,
Monitor,
MonitorDotIcon
MonitorDotIcon,
LogOut,
} from 'lucide-react';
const sidebarItems = [
@@ -20,7 +21,7 @@ const sidebarItems = [
{ name: 'Termine', path: '/termine', icon: Calendar },
{ name: 'Ressourcen', path: '/ressourcen', icon: Boxes },
{ name: 'Infoscreens', path: '/Infoscreens', icon: Monitor },
{ name: 'Gruppen', path: '/gruppen', icon: MonitorDotIcon },
{ name: 'Gruppen', path: '/infoscr_groups', icon: MonitorDotIcon },
{ name: 'Medien', path: '/medien', icon: Image },
{ name: 'Benutzer', path: '/benutzer', icon: User },
{ name: 'Einstellungen', path: '/einstellungen', icon: Settings },
@@ -54,9 +55,21 @@ const Layout: React.FC = () => {
/>
</div>
<button
className="p-2 focus:outline-none hover:bg-[#d6c3a6] transition-colors"
className="p-2 focus:outline-none transition-colors"
style={{
backgroundColor: '#e5d8c7',
color: '#78591c',
}}
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>
@@ -92,6 +105,38 @@ const Layout: React.FC = () => {
);
})}
</nav>
{/* 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%',
}}
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'}
</button>
</div>
</aside>
{/* Main Content */}
<div className="flex-1 flex flex-col">
@@ -134,7 +179,7 @@ const App: React.FC = () => (
<Route path="termine" element={<Appointments />} />
<Route path="ressourcen" element={<Ressourcen />} />
<Route path="Infoscreens" element={<Infoscreens />} />
<Route path="gruppen" element={<Gruppen />} />
<Route path="infoscr_groups" element={<Infoscreen_groups />} />
<Route path="medien" element={<Medien />} />
<Route path="benutzer" element={<Benutzer />} />
<Route path="einstellungen" element={<Einstellungen />} />
@@ -150,7 +195,7 @@ import Dashboard from './dashboard';
import Appointments from './appointments';
import Ressourcen from './ressourcen';
import Infoscreens from './clients';
import Gruppen from './gruppen';
import Infoscreen_groups from './infoscreen_groups';
import Medien from './medien';
import Benutzer from './benutzer';
import Einstellungen from './einstellungen';

View File

@@ -6,6 +6,7 @@ export interface Client {
hardware_hash: string;
ip_address: string;
last_alive: string | null;
group_id: number; // <--- Dieses Feld ergänzen
}
export async function fetchClients(): Promise<Client[]> {
@@ -15,3 +16,13 @@ export async function fetchClients(): Promise<Client[]> {
}
return await response.json();
}
export async function updateClientGroup(clientIds: string[], groupName: string) {
const res = await fetch('/api/clients/group', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ client_ids: clientIds, group_name: groupName }),
});
if (!res.ok) throw new Error('Fehler beim Aktualisieren der Clients');
return await res.json();
}

View File

@@ -0,0 +1,19 @@
export async function createGroup(name: string) {
const res = await fetch('/api/groups', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }),
});
if (!res.ok) throw new Error('Fehler beim Erstellen der Gruppe');
return await res.json();
}
export async function fetchGroups() {
const res = await fetch('/api/groups');
if (!res.ok) throw new Error('Fehler beim Laden der Gruppen');
return await res.json();
}
// 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

@@ -13,9 +13,8 @@ const Dashboard: React.FC = () => {
<div>
<header className="mb-8 pb-4 border-b-2 border-[#d6c3a6]">
<h2 className="text-3xl font-extrabold mb-2">Dashboard</h2>
<p className="text-lg">Willkommen im Infoscreen-Management Dashboard.</p>
</header>
<h3 className="text-lg font-semibold mt-6 mb-4">Clients</h3>
<h3 className="text-lg font-semibold mt-6 mb-4">Infoscreens</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{clients.map(client => (
<div key={client.uuid} className="bg-white rounded shadow p-4 flex flex-col items-center">

View File

@@ -1,8 +0,0 @@
import React from 'react';
const Gruppen: React.FC = () => (
<div>
<h2 className="text-xl font-bold mb-4">Gruppen</h2>
<p>Willkommen im Infoscreen-Management Gruppen.</p>
</div>
);
export default Gruppen;

View File

@@ -0,0 +1,203 @@
import React, { useEffect, useState, useRef } from 'react';
import { KanbanComponent, ColumnsDirective, ColumnDirective } from '@syncfusion/ej2-react-kanban';
import { fetchClients, updateClientGroup } from './apiClients';
import { fetchGroups, createGroup } from './apiGroups';
import type { Client } from './apiClients';
import type { KanbanComponent as KanbanComponentType } from '@syncfusion/ej2-react-kanban';
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;
}
const Infoscreen_groups: React.FC = () => {
const [clients, setClients] = useState<KanbanClient[]>([]);
const [groups, setGroups] = useState<{ keyField: string; headerText: string }[]>([]);
const [showDialog, setShowDialog] = useState(false);
const [newGroupName, setNewGroupName] = useState('');
const [draggedCard, setDraggedCard] = useState<{ id: string; fromColumn: string } | null>(null);
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: groupMap[c.group_id] || 'Nicht zugeordnet',
Summary: c.location || `Client ${i + 1}`,
}))
);
});
});
}, []);
// Neue Gruppe anlegen (persistiert per API)
const handleAddGroup = async () => {
if (!newGroupName.trim()) return;
try {
const newGroup = await createGroup(newGroupName);
setGroups([...groups, { keyField: newGroup.name, headerText: newGroup.name }]);
setNewGroupName('');
setShowDialog(false);
} catch {
alert('Fehler beim Erstellen der Gruppe');
}
};
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 {
await updateClientGroup(clientIds, targetGroupName);
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: groupMap[c.group_id] || 'Nicht zugeordnet',
Summary: c.location || `Client ${i + 1}`,
}))
);
});
});
} catch {
alert('Fehler beim Aktualisieren der Clients');
}
setDraggedCard(null);
};
return (
<div>
<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>
<KanbanComponent
id="kanban"
keyField="Status"
dataSource={clients}
cardSettings={{
headerField: 'Summary',
selectionType: 'Multiple',
}}
allowDragAndDrop={true}
dragStart={handleDragStart}
dragStop={handleCardDrop}
ref={kanbanRef} // Ref zuweisen
>
<ColumnsDirective>
{groups.map(group => (
<ColumnDirective key={group.keyField} {...group} />
))}
</ColumnsDirective>
</KanbanComponent>
{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">Neue Raumgruppe</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}>
Hinzufügen
</button>
<button
className="bg-gray-300 px-4 py-2 rounded"
onClick={() => setShowDialog(false)}
>
Abbrechen
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default Infoscreen_groups;

View File

@@ -5,7 +5,7 @@ import App from './App.tsx';
import { registerLicense } from '@syncfusion/ej2-base';
// Setze hier deinen Lizenzschlüssel ein
registerLicense('Ngo9BigBOggjHTQxAR8/V1NNaF1cWWhPYVFxWmFZfVtgd19FaFZRQ2Y/P1ZhSXxWdkNhWX5bc3xVQWZUUkF9XUs=');
registerLicense('ORg4AjUWIQA/Gnt3VVhhQlJDfV5AQmBIYVp/TGpJfl96cVxMZVVBJAtUQF1hTH5VdENiXX1dcHxUQWNVWkd2');
createRoot(document.getElementById('root')!).render(
<StrictMode>