infoscreen-overview in cards
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -73,3 +73,4 @@ dashboard/pages/test.py
|
|||||||
dashboard/sidebar_test.py
|
dashboard/sidebar_test.py
|
||||||
dashboard/assets/responsive-sidebar.css
|
dashboard/assets/responsive-sidebar.css
|
||||||
certs/
|
certs/
|
||||||
|
sync.ffs_db
|
||||||
|
|||||||
97
dashboard/package-lock.json
generated
97
dashboard/package-lock.json
generated
@@ -13,8 +13,10 @@
|
|||||||
"@syncfusion/ej2-react-grids": "^29.2.11",
|
"@syncfusion/ej2-react-grids": "^29.2.11",
|
||||||
"@syncfusion/ej2-react-schedule": "^29.2.10",
|
"@syncfusion/ej2-react-schedule": "^29.2.10",
|
||||||
"cldr-data": "^36.0.4",
|
"cldr-data": "^36.0.4",
|
||||||
|
"lucide-react": "^0.522.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0"
|
"react-dom": "^19.1.0",
|
||||||
|
"react-router-dom": "^7.6.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.25.0",
|
"@eslint/js": "^9.25.0",
|
||||||
@@ -23,6 +25,7 @@
|
|||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.6",
|
||||||
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.34.1",
|
"@typescript-eslint/eslint-plugin": "^8.34.1",
|
||||||
"@typescript-eslint/parser": "^8.34.1",
|
"@typescript-eslint/parser": "^8.34.1",
|
||||||
"@vitejs/plugin-react": "^4.4.1",
|
"@vitejs/plugin-react": "^4.4.1",
|
||||||
@@ -1985,6 +1988,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/history": {
|
||||||
|
"version": "4.7.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz",
|
||||||
|
"integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.15",
|
"version": "7.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
@@ -2012,6 +2022,29 @@
|
|||||||
"@types/react": "^19.0.0"
|
"@types/react": "^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/react-router": {
|
||||||
|
"version": "5.1.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz",
|
||||||
|
"integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/history": "^4.7.11",
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/react-router-dom": {
|
||||||
|
"version": "5.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz",
|
||||||
|
"integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/history": "^4.7.11",
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-router": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.34.1",
|
"version": "8.34.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz",
|
||||||
@@ -3038,6 +3071,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cosmiconfig": {
|
"node_modules/cosmiconfig": {
|
||||||
"version": "9.0.0",
|
"version": "9.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
|
||||||
@@ -5357,6 +5399,15 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lucide-react": {
|
||||||
|
"version": "0.522.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.522.0.tgz",
|
||||||
|
"integrity": "sha512-jnJbw974yZ7rQHHEFKJOlWAefG3ATSCZHANZxIdx8Rk/16siuwjgA4fBULpXEAWx/RlTs3FzmKW/udWUuO0aRw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@@ -6267,6 +6318,44 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-router": {
|
||||||
|
"version": "7.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.2.tgz",
|
||||||
|
"integrity": "sha512-U7Nv3y+bMimgWjhlT5CRdzHPu2/KVmqPwKUCChW8en5P3znxUqwlYFlbmyj8Rgp1SF6zs5X4+77kBVknkg6a0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cookie": "^1.0.1",
|
||||||
|
"set-cookie-parser": "^2.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-router-dom": {
|
||||||
|
"version": "7.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.2.tgz",
|
||||||
|
"integrity": "sha512-Q8zb6VlTbdYKK5JJBLQEN06oTUa/RAbG/oQS1auK1I0TbJOXktqm+QENEVJU6QvWynlXPRBXI3fiOQcSEA78rA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"react-router": "7.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/read-cache": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
@@ -6521,6 +6610,12 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-cookie-parser": {
|
||||||
|
"version": "2.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||||
|
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/set-function-length": {
|
"node_modules/set-function-length": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||||
|
|||||||
@@ -15,8 +15,10 @@
|
|||||||
"@syncfusion/ej2-react-grids": "^29.2.11",
|
"@syncfusion/ej2-react-grids": "^29.2.11",
|
||||||
"@syncfusion/ej2-react-schedule": "^29.2.10",
|
"@syncfusion/ej2-react-schedule": "^29.2.10",
|
||||||
"cldr-data": "^36.0.4",
|
"cldr-data": "^36.0.4",
|
||||||
|
"lucide-react": "^0.522.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0"
|
"react-dom": "^19.1.0",
|
||||||
|
"react-router-dom": "^7.6.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.25.0",
|
"@eslint/js": "^9.25.0",
|
||||||
@@ -25,6 +27,7 @@
|
|||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.6",
|
||||||
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.34.1",
|
"@typescript-eslint/eslint-plugin": "^8.34.1",
|
||||||
"@typescript-eslint/parser": "^8.34.1",
|
"@typescript-eslint/parser": "^8.34.1",
|
||||||
"@vitejs/plugin-react": "^4.4.1",
|
"@vitejs/plugin-react": "^4.4.1",
|
||||||
|
|||||||
@@ -1,113 +1,156 @@
|
|||||||
// import 'react-app-polyfill/ie11'; // optional, falls benötigt
|
import React, { useState } from 'react';
|
||||||
|
import { BrowserRouter as Router, Routes, Route, Link, Outlet } from 'react-router-dom';
|
||||||
|
import logo from './assets/logo.png';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import React from 'react';
|
|
||||||
|
// Lucide Icons importieren
|
||||||
import {
|
import {
|
||||||
ScheduleComponent,
|
LayoutDashboard,
|
||||||
Day,
|
Calendar,
|
||||||
Week,
|
Boxes,
|
||||||
WorkWeek,
|
Users,
|
||||||
Month,
|
UserSquare,
|
||||||
Agenda,
|
Image,
|
||||||
TimelineViews,
|
User,
|
||||||
TimelineMonth,
|
Settings,
|
||||||
Inject,
|
} from 'lucide-react';
|
||||||
ViewsDirective,
|
|
||||||
ViewDirective,
|
|
||||||
ResourcesDirective,
|
|
||||||
ResourceDirective,
|
|
||||||
} from '@syncfusion/ej2-react-schedule';
|
|
||||||
import { L10n, loadCldr, setCulture } from '@syncfusion/ej2-base';
|
|
||||||
import * as de from 'cldr-data/main/de/ca-gregorian.json';
|
|
||||||
import * as numbers from 'cldr-data/main/de/numbers.json';
|
|
||||||
import * as timeZoneNames from 'cldr-data/main/de/timeZoneNames.json';
|
|
||||||
import * as numberingSystems from 'cldr-data/supplemental/numberingSystems.json';
|
|
||||||
|
|
||||||
// CLDR-Daten laden
|
const sidebarItems = [
|
||||||
loadCldr(
|
{ name: 'Dashboard', path: '/', icon: LayoutDashboard },
|
||||||
(de as unknown as { default: object }).default,
|
{ name: 'Termine', path: '/termine', icon: Calendar },
|
||||||
(numbers as unknown as { default: object }).default,
|
{ name: 'Ressourcen', path: '/ressourcen', icon: Boxes },
|
||||||
(timeZoneNames as unknown as { default: object }).default,
|
{ name: 'Infoscreens', path: '/Infoscreens', icon: Users },
|
||||||
(numberingSystems as unknown as { default: object }).default
|
{ name: 'Gruppen', path: '/gruppen', icon: UserSquare },
|
||||||
);
|
{ name: 'Medien', path: '/medien', icon: Image },
|
||||||
|
{ name: 'Benutzer', path: '/benutzer', icon: User },
|
||||||
// Deutsche Lokalisierung für den Scheduler
|
{ name: 'Einstellungen', path: '/einstellungen', icon: Settings },
|
||||||
L10n.load({
|
|
||||||
de: {
|
|
||||||
schedule: {
|
|
||||||
day: 'Tag',
|
|
||||||
week: 'Woche',
|
|
||||||
workWeek: 'Arbeitswoche',
|
|
||||||
month: 'Monat',
|
|
||||||
agenda: 'Agenda',
|
|
||||||
today: 'Heute',
|
|
||||||
noEvents: 'Keine Termine',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Kultur setzen
|
|
||||||
setCulture('de');
|
|
||||||
|
|
||||||
// Ressourcen-Daten
|
|
||||||
const resources = [
|
|
||||||
{ text: 'Raum A', id: 1, color: '#1aaa55' },
|
|
||||||
{ text: 'Raum B', id: 2, color: '#357cd2' },
|
|
||||||
{ text: 'Raum C', id: 3, color: '#7fa900' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Dummy-Termine generieren
|
const Layout: React.FC = () => {
|
||||||
const now = new Date();
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
const appointments = Array.from({ length: 10 }).map((_, i) => {
|
|
||||||
const dayOffset = Math.floor(i * 1.4); // verteilt auf 14 Tage
|
|
||||||
const start = new Date(now);
|
|
||||||
start.setDate(now.getDate() + dayOffset);
|
|
||||||
start.setHours(9 + (i % 4), 0, 0, 0);
|
|
||||||
const end = new Date(start);
|
|
||||||
end.setHours(start.getHours() + 1);
|
|
||||||
|
|
||||||
return {
|
|
||||||
Id: i + 1,
|
|
||||||
Subject: `Termin ${i + 1}`,
|
|
||||||
StartTime: start,
|
|
||||||
EndTime: end,
|
|
||||||
ResourceId: (i % 3) + 1,
|
|
||||||
Location: resources[i % 3].text,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const App: React.FC = () => {
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8 bg-gray-100 min-h-screen">
|
<div className="flex min-h-screen">
|
||||||
<h1 className="text-2xl font-bold mb-4">Infoscreen Kalendersteuerung</h1>
|
{/* Sidebar */}
|
||||||
<ScheduleComponent
|
<aside
|
||||||
height="650px"
|
className={`flex flex-col transition-all duration-300 ${collapsed ? 'w-20' : 'w-30'}`}
|
||||||
locale="de"
|
style={{
|
||||||
currentView="TimelineWeek"
|
backgroundColor: '#e5d8c7',
|
||||||
eventSettings={{ dataSource: appointments }}
|
color: '#78591c',
|
||||||
group={{ resources: ['Räume'] }}
|
fontSize: '1.15rem',
|
||||||
|
fontFamily:
|
||||||
|
'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ViewsDirective>
|
<div
|
||||||
<ViewDirective option="Week" />
|
className="h-20 flex items-center justify-center border-b"
|
||||||
<ViewDirective option="TimelineWeek" />
|
style={{ borderColor: '#d6c3a6' }}
|
||||||
<ViewDirective option="Month" />
|
>
|
||||||
<ViewDirective option="TimelineMonth" />
|
<img
|
||||||
</ViewsDirective>
|
src={logo}
|
||||||
<ResourcesDirective>
|
alt="Logo"
|
||||||
<ResourceDirective
|
className="h-12"
|
||||||
field="ResourceId"
|
style={{ display: collapsed ? 'none' : 'block' }}
|
||||||
title="Räume"
|
|
||||||
name="Räume"
|
|
||||||
allowMultiple={false}
|
|
||||||
dataSource={resources}
|
|
||||||
textField="text"
|
|
||||||
idField="id"
|
|
||||||
colorField="color"
|
|
||||||
/>
|
/>
|
||||||
</ResourcesDirective>
|
</div>
|
||||||
<Inject services={[Day, Week, WorkWeek, Month, Agenda, TimelineViews, TimelineMonth]} />
|
<button
|
||||||
</ScheduleComponent>
|
className="p-2 focus:outline-none hover:bg-[#d6c3a6] transition-colors"
|
||||||
|
onClick={() => setCollapsed(!collapsed)}
|
||||||
|
aria-label={collapsed ? 'Sidebar ausklappen' : 'Sidebar einklappen'}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: 20 }}>{collapsed ? '▶' : '◀'}</span>
|
||||||
|
</button>
|
||||||
|
<nav className="flex-1 mt-4">
|
||||||
|
{sidebarItems.map(item => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
return (
|
||||||
|
<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,
|
||||||
|
}}
|
||||||
|
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}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="flex-1 flex flex-col">
|
||||||
|
{/* Header */}
|
||||||
|
<header
|
||||||
|
className="flex items-center px-8 shadow"
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#e5d8c7',
|
||||||
|
color: '#78591c',
|
||||||
|
height: 'calc(48px + 20px)',
|
||||||
|
fontSize: '1.15rem',
|
||||||
|
fontFamily:
|
||||||
|
'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={logo}
|
||||||
|
alt="Logo"
|
||||||
|
className="h-12 mr-4"
|
||||||
|
style={{ marginTop: 10, marginBottom: 10 }}
|
||||||
|
/>
|
||||||
|
<span className="text-2xl font-bold mr-8">Infoscreen-Management</span>
|
||||||
|
<span className="ml-auto" style={{ color: '#78591c' }}>
|
||||||
|
[Organisationsname]
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
<main className="flex-1 p-8 bg-gray-100">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const App: React.FC = () => (
|
||||||
|
<Router>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Layout />}>
|
||||||
|
<Route index element={<Dashboard />} />
|
||||||
|
<Route path="termine" element={<Appointments />} />
|
||||||
|
<Route path="ressourcen" element={<Ressourcen />} />
|
||||||
|
<Route path="Infoscreens" element={<Infoscreens />} />
|
||||||
|
<Route path="gruppen" element={<Gruppen />} />
|
||||||
|
<Route path="medien" element={<Medien />} />
|
||||||
|
<Route path="benutzer" element={<Benutzer />} />
|
||||||
|
<Route path="einstellungen" element={<Einstellungen />} />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
||||||
|
// Dummy Components (können in eigene Dateien ausgelagert werden)
|
||||||
|
import Dashboard from './dashboard';
|
||||||
|
import Appointments from './appointments';
|
||||||
|
import Ressourcen from './ressourcen';
|
||||||
|
import Infoscreens from './clients';
|
||||||
|
import Gruppen from './gruppen';
|
||||||
|
import Medien from './medien';
|
||||||
|
import Benutzer from './benutzer';
|
||||||
|
import Einstellungen from './einstellungen';
|
||||||
|
|||||||
17
dashboard/src/apiClients.ts
Normal file
17
dashboard/src/apiClients.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// Funktion zum Laden der Clients von der API
|
||||||
|
|
||||||
|
export interface Client {
|
||||||
|
uuid: string;
|
||||||
|
location: string;
|
||||||
|
hardware_hash: string;
|
||||||
|
ip_address: string;
|
||||||
|
last_alive: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchClients(): Promise<Client[]> {
|
||||||
|
const response = await fetch('/api/clients');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Fehler beim Laden der Clients');
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
106
dashboard/src/appointments.tsx
Normal file
106
dashboard/src/appointments.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
ScheduleComponent,
|
||||||
|
Day,
|
||||||
|
Week,
|
||||||
|
WorkWeek,
|
||||||
|
Month,
|
||||||
|
Agenda,
|
||||||
|
TimelineViews,
|
||||||
|
TimelineMonth,
|
||||||
|
Inject,
|
||||||
|
ViewsDirective,
|
||||||
|
ViewDirective,
|
||||||
|
ResourcesDirective,
|
||||||
|
ResourceDirective,
|
||||||
|
} from '@syncfusion/ej2-react-schedule';
|
||||||
|
import { L10n, loadCldr, setCulture } from '@syncfusion/ej2-base';
|
||||||
|
import * as de from 'cldr-data/main/de/ca-gregorian.json';
|
||||||
|
import * as numbers from 'cldr-data/main/de/numbers.json';
|
||||||
|
import * as timeZoneNames from 'cldr-data/main/de/timeZoneNames.json';
|
||||||
|
import * as numberingSystems from 'cldr-data/supplemental/numberingSystems.json';
|
||||||
|
|
||||||
|
// CLDR-Daten laden
|
||||||
|
loadCldr(
|
||||||
|
(de as unknown as { default: object }).default,
|
||||||
|
(numbers as unknown as { default: object }).default,
|
||||||
|
(timeZoneNames as unknown as { default: object }).default,
|
||||||
|
(numberingSystems as unknown as { default: object }).default
|
||||||
|
);
|
||||||
|
|
||||||
|
// Deutsche Lokalisierung für den Scheduler
|
||||||
|
L10n.load({
|
||||||
|
de: {
|
||||||
|
schedule: {
|
||||||
|
day: 'Tag',
|
||||||
|
week: 'Woche',
|
||||||
|
workWeek: 'Arbeitswoche',
|
||||||
|
month: 'Monat',
|
||||||
|
agenda: 'Agenda',
|
||||||
|
today: 'Heute',
|
||||||
|
noEvents: 'Keine Termine',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Kultur setzen
|
||||||
|
setCulture('de');
|
||||||
|
|
||||||
|
// Ressourcen-Daten
|
||||||
|
const resources = [
|
||||||
|
{ text: 'Raum A', id: 1, color: '#1aaa55' },
|
||||||
|
{ text: 'Raum B', id: 2, color: '#357cd2' },
|
||||||
|
{ text: 'Raum C', id: 3, color: '#7fa900' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Dummy-Termine generieren
|
||||||
|
const now = new Date();
|
||||||
|
const appointments = Array.from({ length: 10 }).map((_, i) => {
|
||||||
|
const dayOffset = Math.floor(i * 1.4); // verteilt auf 14 Tage
|
||||||
|
const start = new Date(now);
|
||||||
|
start.setDate(now.getDate() + dayOffset);
|
||||||
|
start.setHours(9 + (i % 4), 0, 0, 0);
|
||||||
|
const end = new Date(start);
|
||||||
|
end.setHours(start.getHours() + 1);
|
||||||
|
|
||||||
|
return {
|
||||||
|
Id: i + 1,
|
||||||
|
Subject: `Termin ${i + 1}`,
|
||||||
|
StartTime: start,
|
||||||
|
EndTime: end,
|
||||||
|
ResourceId: (i % 3) + 1,
|
||||||
|
Location: resources[i % 3].text,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const Appointments: React.FC = () => (
|
||||||
|
<ScheduleComponent
|
||||||
|
height="650px"
|
||||||
|
locale="de"
|
||||||
|
currentView="TimelineWeek"
|
||||||
|
eventSettings={{ dataSource: appointments }}
|
||||||
|
group={{ resources: ['Räume'] }}
|
||||||
|
>
|
||||||
|
<ViewsDirective>
|
||||||
|
<ViewDirective option="Week" />
|
||||||
|
<ViewDirective option="TimelineWeek" />
|
||||||
|
<ViewDirective option="Month" />
|
||||||
|
<ViewDirective option="TimelineMonth" />
|
||||||
|
</ViewsDirective>
|
||||||
|
<ResourcesDirective>
|
||||||
|
<ResourceDirective
|
||||||
|
field="ResourceId"
|
||||||
|
title="Räume"
|
||||||
|
name="Räume"
|
||||||
|
allowMultiple={false}
|
||||||
|
dataSource={resources}
|
||||||
|
textField="text"
|
||||||
|
idField="id"
|
||||||
|
colorField="color"
|
||||||
|
/>
|
||||||
|
</ResourcesDirective>
|
||||||
|
<Inject services={[Day, Week, WorkWeek, Month, Agenda, TimelineViews, TimelineMonth]} />
|
||||||
|
</ScheduleComponent>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Appointments;
|
||||||
BIN
dashboard/src/assets/TAA_Logo.png
Normal file
BIN
dashboard/src/assets/TAA_Logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
BIN
dashboard/src/assets/logo.png
Normal file
BIN
dashboard/src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 225 KiB |
8
dashboard/src/benutzer.tsx
Normal file
8
dashboard/src/benutzer.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import React from 'react';
|
||||||
|
const Benutzer: React.FC = () => (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold mb-4">Benutzer</h2>
|
||||||
|
<p>Willkommen im Infoscreen-Management Benutzer.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
export default Benutzer;
|
||||||
8
dashboard/src/clients.tsx
Normal file
8
dashboard/src/clients.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import React from 'react';
|
||||||
|
const Infoscreens: React.FC = () => (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold mb-4">Infoscreens</h2>
|
||||||
|
<p>Willkommen im Infoscreen-Management Infoscreens.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
export default Infoscreens;
|
||||||
46
dashboard/src/dashboard.tsx
Normal file
46
dashboard/src/dashboard.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { fetchClients } from './apiClients';
|
||||||
|
import type { Client } from './apiClients';
|
||||||
|
|
||||||
|
const Dashboard: React.FC = () => {
|
||||||
|
const [clients, setClients] = useState<Client[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchClients().then(setClients).catch(console.error);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
<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">
|
||||||
|
<h4 className="text-lg font-bold mb-2">{client.location || 'Unbekannter Standort'}</h4>
|
||||||
|
<img
|
||||||
|
src={`/screenshots/${client.uuid}`}
|
||||||
|
alt={`Screenshot ${client.location}`}
|
||||||
|
className="w-full h-48 object-contain bg-gray-100 mb-2"
|
||||||
|
onError={e => (e.currentTarget.style.display = 'none')}
|
||||||
|
/>
|
||||||
|
<div className="text-sm text-gray-700 mb-1">
|
||||||
|
<span className="font-semibold">IP:</span> {client.ip_address}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-700">
|
||||||
|
<span className="font-semibold">Letztes Lebenszeichen:</span>{' '}
|
||||||
|
{client.last_alive ? new Date(client.last_alive).toLocaleString() : '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{clients.length === 0 && (
|
||||||
|
<div className="col-span-full text-center text-gray-400">Keine Clients gefunden.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
8
dashboard/src/einstellungen.tsx
Normal file
8
dashboard/src/einstellungen.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import React from 'react';
|
||||||
|
const Einstellungen: React.FC = () => (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold mb-4">Einstellungen</h2>
|
||||||
|
<p>Willkommen im Infoscreen-Management Einstellungen.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
export default Einstellungen;
|
||||||
8
dashboard/src/gruppen.tsx
Normal file
8
dashboard/src/gruppen.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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;
|
||||||
8
dashboard/src/medien.tsx
Normal file
8
dashboard/src/medien.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import React from 'react';
|
||||||
|
const Medien: React.FC = () => (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold mb-4">Medien</h2>
|
||||||
|
<p>Willkommen im Infoscreen-Management Medien.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
export default Medien;
|
||||||
8
dashboard/src/ressourcen.tsx
Normal file
8
dashboard/src/ressourcen.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import React from 'react';
|
||||||
|
const Ressourcen: React.FC = () => (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold mb-4">Ressourcen</h2>
|
||||||
|
<p>Willkommen im Infoscreen-Management Ressourcen.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
export default Ressourcen;
|
||||||
0
dashboard/src/termine.tsx
Normal file
0
dashboard/src/termine.tsx
Normal file
@@ -1,7 +1,13 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite';
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
})
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': 'http://localhost:8000',
|
||||||
|
'/screenshots': 'http://localhost:8000',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
27
nginx.conf
27
nginx.conf
@@ -3,34 +3,21 @@ http {
|
|||||||
upstream dashboard {
|
upstream dashboard {
|
||||||
server 127.0.0.1:3000;
|
server 127.0.0.1:3000;
|
||||||
}
|
}
|
||||||
|
upstream infoscreen_api {
|
||||||
|
server infoscreen-api:8000;
|
||||||
|
}
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name _;
|
server_name _;
|
||||||
|
|
||||||
# Optional: HTTP auf HTTPS weiterleiten
|
# Leitet /api/ und /screenshots/ an den API-Server weiter
|
||||||
# return 301 https://$host$request_uri;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://dashboard;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name _;
|
|
||||||
|
|
||||||
ssl_certificate /etc/nginx/certs/dev.crt;
|
|
||||||
ssl_certificate_key /etc/nginx/certs/dev.key;
|
|
||||||
|
|
||||||
location /api/ {
|
location /api/ {
|
||||||
proxy_pass http://infoscreen-api:8000/api/;
|
proxy_pass http://infoscreen_api/api/;
|
||||||
}
|
}
|
||||||
location /screenshots/ {
|
location /screenshots/ {
|
||||||
proxy_pass http://infoscreen-api:8000/screenshots/;
|
proxy_pass http://infoscreen_api/screenshots/;
|
||||||
}
|
}
|
||||||
|
# Alles andere geht ans Frontend
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://dashboard;
|
proxy_pass http://dashboard;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
|||||||
Reference in New Issue
Block a user