implement functionality to delete clients in
clients and SetupMode components
This commit is contained in:
118
dashboard/package-lock.json
generated
118
dashboard/package-lock.json
generated
@@ -12,10 +12,11 @@
|
|||||||
"@syncfusion/ej2-react-calendars": "^30.1.37",
|
"@syncfusion/ej2-react-calendars": "^30.1.37",
|
||||||
"@syncfusion/ej2-react-dropdowns": "^30.1.37",
|
"@syncfusion/ej2-react-dropdowns": "^30.1.37",
|
||||||
"@syncfusion/ej2-react-filemanager": "^30.1.38",
|
"@syncfusion/ej2-react-filemanager": "^30.1.38",
|
||||||
"@syncfusion/ej2-react-grids": "^30.1.37",
|
"@syncfusion/ej2-react-grids": "^30.1.40",
|
||||||
"@syncfusion/ej2-react-inputs": "^30.1.38",
|
"@syncfusion/ej2-react-inputs": "^30.1.38",
|
||||||
"@syncfusion/ej2-react-kanban": "^30.1.37",
|
"@syncfusion/ej2-react-kanban": "^30.1.37",
|
||||||
"@syncfusion/ej2-react-layouts": "^30.1.40",
|
"@syncfusion/ej2-react-layouts": "^30.1.40",
|
||||||
|
"@syncfusion/ej2-react-navigations": "^30.1.39",
|
||||||
"@syncfusion/ej2-react-notifications": "^30.1.37",
|
"@syncfusion/ej2-react-notifications": "^30.1.37",
|
||||||
"@syncfusion/ej2-react-popups": "^30.1.37",
|
"@syncfusion/ej2-react-popups": "^30.1.37",
|
||||||
"@syncfusion/ej2-react-schedule": "^30.1.37",
|
"@syncfusion/ej2-react-schedule": "^30.1.37",
|
||||||
@@ -1027,9 +1028,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@syncfusion/ej2-data": {
|
"node_modules/@syncfusion/ej2-data": {
|
||||||
"version": "30.1.38",
|
"version": "30.1.40",
|
||||||
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-data/-/ej2-data-30.1.38.tgz",
|
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-data/-/ej2-data-30.1.40.tgz",
|
||||||
"integrity": "sha512-BdqvjLzzK4OuUR1YlzPSG3SmeGg1mrLz/6ih5oD9dSpRXDoMG24bpO1rwCK7mjy8Dp9IJ8mliyCbPfoDycxM9Q==",
|
"integrity": "sha512-jxQll5rn3yailAP/wcCnIw96ROfafwFzMJ6BIcI5FTn+/mk3hLQTp/GiZ/In7uDGgXQll2yQeolPJcOkILtWkw==",
|
||||||
"license": "SEE LICENSE IN license",
|
"license": "SEE LICENSE IN license",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@syncfusion/ej2-base": "~30.1.38"
|
"@syncfusion/ej2-base": "~30.1.38"
|
||||||
@@ -1084,50 +1085,64 @@
|
|||||||
"@syncfusion/ej2-splitbuttons": "~30.1.37"
|
"@syncfusion/ej2-splitbuttons": "~30.1.37"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@syncfusion/ej2-filemanager/node_modules/@syncfusion/ej2-grids": {
|
"node_modules/@syncfusion/ej2-grids": {
|
||||||
"version": "30.1.38",
|
"version": "30.1.40",
|
||||||
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-grids/-/ej2-grids-30.1.38.tgz",
|
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-grids/-/ej2-grids-30.1.40.tgz",
|
||||||
"integrity": "sha512-0ULWC/P8AsYco3fhUfrkppEdU+IMzrIWyoP057/yp0Mktq9UI5mgvQ12ruZbEMQXl0vK5S5DKaWMDJU2vBTDWQ==",
|
"integrity": "sha512-t6WHFhF2dD0vusdn9rEOFi8Opony/TDIFlu9iBRYd2g+RvKDzK4jo97qWQbAzOnbgvJGhSy3DLnVnIMwFdnqRw==",
|
||||||
"license": "SEE LICENSE IN license",
|
"license": "SEE LICENSE IN license",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@syncfusion/ej2-base": "~30.1.38",
|
"@syncfusion/ej2-base": "~30.1.38",
|
||||||
"@syncfusion/ej2-buttons": "~30.1.37",
|
"@syncfusion/ej2-buttons": "~30.1.37",
|
||||||
"@syncfusion/ej2-calendars": "~30.1.37",
|
"@syncfusion/ej2-calendars": "~30.1.37",
|
||||||
"@syncfusion/ej2-compression": "~30.1.37",
|
"@syncfusion/ej2-compression": "~30.1.37",
|
||||||
"@syncfusion/ej2-data": "~30.1.38",
|
"@syncfusion/ej2-data": "~30.1.40",
|
||||||
"@syncfusion/ej2-dropdowns": "~30.1.37",
|
"@syncfusion/ej2-dropdowns": "~30.1.40",
|
||||||
"@syncfusion/ej2-excel-export": "~30.1.37",
|
"@syncfusion/ej2-excel-export": "~30.1.37",
|
||||||
"@syncfusion/ej2-file-utils": "~30.1.37",
|
"@syncfusion/ej2-file-utils": "~30.1.37",
|
||||||
"@syncfusion/ej2-inputs": "~30.1.38",
|
"@syncfusion/ej2-inputs": "~30.1.40",
|
||||||
"@syncfusion/ej2-lists": "~30.1.37",
|
"@syncfusion/ej2-lists": "~30.1.37",
|
||||||
"@syncfusion/ej2-navigations": "~30.1.37",
|
"@syncfusion/ej2-navigations": "~30.1.39",
|
||||||
"@syncfusion/ej2-notifications": "~30.1.37",
|
"@syncfusion/ej2-notifications": "~30.1.37",
|
||||||
"@syncfusion/ej2-pdf-export": "~30.1.38",
|
"@syncfusion/ej2-pdf-export": "~30.1.38",
|
||||||
"@syncfusion/ej2-popups": "~30.1.37",
|
"@syncfusion/ej2-popups": "~30.1.40",
|
||||||
"@syncfusion/ej2-splitbuttons": "~30.1.37"
|
"@syncfusion/ej2-splitbuttons": "~30.1.39"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@syncfusion/ej2-grids": {
|
"node_modules/@syncfusion/ej2-grids/node_modules/@syncfusion/ej2-dropdowns": {
|
||||||
"version": "30.1.37",
|
"version": "30.1.40",
|
||||||
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-grids/-/ej2-grids-30.1.37.tgz",
|
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-dropdowns/-/ej2-dropdowns-30.1.40.tgz",
|
||||||
"integrity": "sha512-EG8RzzPCML9UULAN+SZdo/G5j8AsmzKzwQvocvZoL/YOfDPWom8WaGXNbE5pssMmIEnF2YnQkMuwRWg1tLHyog==",
|
"integrity": "sha512-8vpO0+X4OnA+CLPfVBqOBon2AbDHXUTK5UdP/A2JEJQw0ktNpURm8ux3K1VT9OXvYmuNgyN/WrM6Pf5m3Oexkw==",
|
||||||
"license": "SEE LICENSE IN license",
|
"license": "SEE LICENSE IN license",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@syncfusion/ej2-base": "~30.1.37",
|
"@syncfusion/ej2-base": "~30.1.38",
|
||||||
"@syncfusion/ej2-buttons": "~30.1.37",
|
"@syncfusion/ej2-data": "~30.1.40",
|
||||||
"@syncfusion/ej2-calendars": "~30.1.37",
|
"@syncfusion/ej2-inputs": "~30.1.40",
|
||||||
"@syncfusion/ej2-compression": "~30.1.37",
|
|
||||||
"@syncfusion/ej2-data": "~30.1.37",
|
|
||||||
"@syncfusion/ej2-dropdowns": "~30.1.37",
|
|
||||||
"@syncfusion/ej2-excel-export": "~30.1.37",
|
|
||||||
"@syncfusion/ej2-file-utils": "~30.1.37",
|
|
||||||
"@syncfusion/ej2-inputs": "~30.1.37",
|
|
||||||
"@syncfusion/ej2-lists": "~30.1.37",
|
"@syncfusion/ej2-lists": "~30.1.37",
|
||||||
"@syncfusion/ej2-navigations": "~30.1.37",
|
"@syncfusion/ej2-navigations": "~30.1.39",
|
||||||
"@syncfusion/ej2-notifications": "~30.1.37",
|
"@syncfusion/ej2-notifications": "~30.1.37",
|
||||||
"@syncfusion/ej2-pdf-export": "~30.1.37",
|
"@syncfusion/ej2-popups": "~30.1.40"
|
||||||
"@syncfusion/ej2-popups": "~30.1.37",
|
}
|
||||||
"@syncfusion/ej2-splitbuttons": "~30.1.37"
|
},
|
||||||
|
"node_modules/@syncfusion/ej2-grids/node_modules/@syncfusion/ej2-inputs": {
|
||||||
|
"version": "30.1.40",
|
||||||
|
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-inputs/-/ej2-inputs-30.1.40.tgz",
|
||||||
|
"integrity": "sha512-qMbwX8x6s+pS4C9BKz6Dy9z7t+GYCSDvbJXVykvqmdpU54EjmIwb8l7LV9P2B4ooiXQ4QgkFf/CtGjP3z3H1yg==",
|
||||||
|
"license": "SEE LICENSE IN license",
|
||||||
|
"dependencies": {
|
||||||
|
"@syncfusion/ej2-base": "~30.1.38",
|
||||||
|
"@syncfusion/ej2-buttons": "~30.1.37",
|
||||||
|
"@syncfusion/ej2-popups": "~30.1.40",
|
||||||
|
"@syncfusion/ej2-splitbuttons": "~30.1.39"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@syncfusion/ej2-grids/node_modules/@syncfusion/ej2-popups": {
|
||||||
|
"version": "30.1.40",
|
||||||
|
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-popups/-/ej2-popups-30.1.40.tgz",
|
||||||
|
"integrity": "sha512-yf3FKJW+jdZeargitw9CkOF2Jxg6jCw4PSq9L6aypDiAm9H3GEuZGGRwmnyTLEi+M14oxFSyKOpw5oMSPoZKuw==",
|
||||||
|
"license": "SEE LICENSE IN license",
|
||||||
|
"dependencies": {
|
||||||
|
"@syncfusion/ej2-base": "~30.1.38",
|
||||||
|
"@syncfusion/ej2-buttons": "~30.1.37"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@syncfusion/ej2-icons": {
|
"node_modules/@syncfusion/ej2-icons": {
|
||||||
@@ -1187,15 +1202,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@syncfusion/ej2-navigations": {
|
"node_modules/@syncfusion/ej2-navigations": {
|
||||||
"version": "30.1.37",
|
"version": "30.1.39",
|
||||||
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-navigations/-/ej2-navigations-30.1.37.tgz",
|
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-navigations/-/ej2-navigations-30.1.39.tgz",
|
||||||
"integrity": "sha512-FD3hsT4a8fMdS3S7EFu9tnu741980HRK97fDAUgUtLGqDANC6Ee2HyeZZTjVeUpVAHfdomb/n7oYvtDUXZxwYw==",
|
"integrity": "sha512-mehYOsIcgGEMHbSwdnxxSqcPwhyQpVCJ6VgLESBikkTIww3NAFwRkhMcs4o4n0z+3KyySUVKNi1zU13mzlYkoQ==",
|
||||||
"license": "SEE LICENSE IN license",
|
"license": "SEE LICENSE IN license",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@syncfusion/ej2-base": "~30.1.37",
|
"@syncfusion/ej2-base": "~30.1.38",
|
||||||
"@syncfusion/ej2-buttons": "~30.1.37",
|
"@syncfusion/ej2-buttons": "~30.1.37",
|
||||||
"@syncfusion/ej2-data": "~30.1.37",
|
"@syncfusion/ej2-data": "~30.1.38",
|
||||||
"@syncfusion/ej2-inputs": "~30.1.37",
|
"@syncfusion/ej2-inputs": "~30.1.38",
|
||||||
"@syncfusion/ej2-lists": "~30.1.37",
|
"@syncfusion/ej2-lists": "~30.1.37",
|
||||||
"@syncfusion/ej2-popups": "~30.1.37"
|
"@syncfusion/ej2-popups": "~30.1.37"
|
||||||
}
|
}
|
||||||
@@ -1284,13 +1299,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@syncfusion/ej2-react-grids": {
|
"node_modules/@syncfusion/ej2-react-grids": {
|
||||||
"version": "30.1.37",
|
"version": "30.1.40",
|
||||||
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-react-grids/-/ej2-react-grids-30.1.37.tgz",
|
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-react-grids/-/ej2-react-grids-30.1.40.tgz",
|
||||||
"integrity": "sha512-QNnvwD88GYezX9glpGUEltFmUCKZORXz7w93BS2CfgTxsjJjdaZ8xzDi98J+aAUrUh6I44pMut5Sfb9QOBFH+g==",
|
"integrity": "sha512-NaqR/r8yN1tomNkp4X5lUQe55wLmtKBvX2Qj0Sm6+SAb2taRR2v2MSKX+MHWToV1cGpUhmVDKo/PikbW98r2eA==",
|
||||||
"license": "SEE LICENSE IN license",
|
"license": "SEE LICENSE IN license",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@syncfusion/ej2-base": "~30.1.37",
|
"@syncfusion/ej2-base": "~30.1.38",
|
||||||
"@syncfusion/ej2-grids": "30.1.37",
|
"@syncfusion/ej2-grids": "30.1.40",
|
||||||
"@syncfusion/ej2-react-base": "~30.1.37"
|
"@syncfusion/ej2-react-base": "~30.1.37"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1327,6 +1342,17 @@
|
|||||||
"@syncfusion/ej2-react-base": "~30.1.37"
|
"@syncfusion/ej2-react-base": "~30.1.37"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@syncfusion/ej2-react-navigations": {
|
||||||
|
"version": "30.1.39",
|
||||||
|
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-react-navigations/-/ej2-react-navigations-30.1.39.tgz",
|
||||||
|
"integrity": "sha512-wXrfM3f1h3IfTjp+HxiKhmUu4wjzd+wSeCCaekTG2yVaKekfzTno/8dVh+c/PhLwJ3wearf+rW8st5nfhAeQdA==",
|
||||||
|
"license": "SEE LICENSE IN license",
|
||||||
|
"dependencies": {
|
||||||
|
"@syncfusion/ej2-base": "~30.1.38",
|
||||||
|
"@syncfusion/ej2-navigations": "30.1.39",
|
||||||
|
"@syncfusion/ej2-react-base": "~30.1.37"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@syncfusion/ej2-react-notifications": {
|
"node_modules/@syncfusion/ej2-react-notifications": {
|
||||||
"version": "30.1.37",
|
"version": "30.1.37",
|
||||||
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-react-notifications/-/ej2-react-notifications-30.1.37.tgz",
|
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-react-notifications/-/ej2-react-notifications-30.1.37.tgz",
|
||||||
@@ -1379,12 +1405,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@syncfusion/ej2-splitbuttons": {
|
"node_modules/@syncfusion/ej2-splitbuttons": {
|
||||||
"version": "30.1.37",
|
"version": "30.1.39",
|
||||||
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-splitbuttons/-/ej2-splitbuttons-30.1.37.tgz",
|
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-splitbuttons/-/ej2-splitbuttons-30.1.39.tgz",
|
||||||
"integrity": "sha512-V4MNbw8qBNumd7B4AlbmndxXGqILcSUoCrA+wBfLt6KZArZnuK8Y2zaD7fN9xoR1c/hoxlgo9sXyIxdPfi9Lyg==",
|
"integrity": "sha512-MViD8imxEHukX7tvtHWquc76+pL95NB86YPUFb2lx3gxwLUBTIA/KVS2ZmAlQXYaFI7RZQPGopgIbwfb/Zhz5A==",
|
||||||
"license": "SEE LICENSE IN license",
|
"license": "SEE LICENSE IN license",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@syncfusion/ej2-base": "~30.1.37",
|
"@syncfusion/ej2-base": "~30.1.38",
|
||||||
"@syncfusion/ej2-popups": "~30.1.37"
|
"@syncfusion/ej2-popups": "~30.1.37"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,10 +14,11 @@
|
|||||||
"@syncfusion/ej2-react-calendars": "^30.1.37",
|
"@syncfusion/ej2-react-calendars": "^30.1.37",
|
||||||
"@syncfusion/ej2-react-dropdowns": "^30.1.37",
|
"@syncfusion/ej2-react-dropdowns": "^30.1.37",
|
||||||
"@syncfusion/ej2-react-filemanager": "^30.1.38",
|
"@syncfusion/ej2-react-filemanager": "^30.1.38",
|
||||||
"@syncfusion/ej2-react-grids": "^30.1.37",
|
"@syncfusion/ej2-react-grids": "^30.1.40",
|
||||||
"@syncfusion/ej2-react-inputs": "^30.1.38",
|
"@syncfusion/ej2-react-inputs": "^30.1.38",
|
||||||
"@syncfusion/ej2-react-kanban": "^30.1.37",
|
"@syncfusion/ej2-react-kanban": "^30.1.37",
|
||||||
"@syncfusion/ej2-react-layouts": "^30.1.40",
|
"@syncfusion/ej2-react-layouts": "^30.1.40",
|
||||||
|
"@syncfusion/ej2-react-navigations": "^30.1.39",
|
||||||
"@syncfusion/ej2-react-notifications": "^30.1.37",
|
"@syncfusion/ej2-react-notifications": "^30.1.37",
|
||||||
"@syncfusion/ej2-react-popups": "^30.1.37",
|
"@syncfusion/ej2-react-popups": "^30.1.37",
|
||||||
"@syncfusion/ej2-react-schedule": "^30.1.37",
|
"@syncfusion/ej2-react-schedule": "^30.1.37",
|
||||||
|
|||||||
@@ -25,11 +25,22 @@ body {
|
|||||||
--sidebar-border: #d6c3a6;
|
--sidebar-border: #d6c3a6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Layout-Container für Sidebar und Content */
|
||||||
|
.layout-container {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
overflow: hidden; /* Verhindert, dass der Scrollbalken den gesamten Container betrifft */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar fixieren, keine Scrollbalken */
|
||||||
.sidebar-theme {
|
.sidebar-theme {
|
||||||
background-color: var(--sidebar-bg);
|
background-color: var(--sidebar-bg);
|
||||||
color: var(--sidebar-fg);
|
color: var(--sidebar-fg);
|
||||||
font-size: 1.15rem;
|
font-size: 1.15rem;
|
||||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
||||||
|
width: 240px; /* Feste Breite für die Sidebar */
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-theme .sidebar-link {
|
.sidebar-theme .sidebar-link {
|
||||||
@@ -63,6 +74,16 @@ body {
|
|||||||
color: var(--sidebar-bg);
|
color: var(--sidebar-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Nur der Page-Content bekommt den horizontalen Scrollbalken */
|
||||||
|
.page-content-scroll {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 16px; /* Optional: Abstand innerhalb des Contents */
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box; /* Padding wird in die Breite/Höhe einbezogen */
|
||||||
|
}
|
||||||
|
|
||||||
/* Kanban-Karten im Sidebar-Style */
|
/* Kanban-Karten im Sidebar-Style */
|
||||||
.e-kanban .e-card,
|
.e-kanban .e-card,
|
||||||
.e-kanban .e-card .e-card-content,
|
.e-kanban .e-card .e-card-content,
|
||||||
@@ -106,4 +127,11 @@ body {
|
|||||||
color: color-mix(in srgb, var(--sidebar-fg) 85%, #000 15%) !important;
|
color: color-mix(in srgb, var(--sidebar-fg) 85%, #000 15%) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Entferne den globalen Scrollbalken von .main-content! */
|
||||||
|
.main-content {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto; /* Wiederherstellen des ursprünglichen Scroll-Verhaltens */
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { BrowserRouter as Router, Routes, Route, Link, Outlet } from 'react-router-dom';
|
import {
|
||||||
|
BrowserRouter as Router,
|
||||||
|
Routes,
|
||||||
|
Route,
|
||||||
|
Link,
|
||||||
|
Outlet,
|
||||||
|
} from 'react-router-dom';
|
||||||
import logo from './assets/logo.png';
|
import logo from './assets/logo.png';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
@@ -30,17 +36,25 @@ const sidebarItems = [
|
|||||||
{ name: 'Einstellungen', path: '/einstellungen', icon: Settings },
|
{ name: 'Einstellungen', path: '/einstellungen', icon: Settings },
|
||||||
];
|
];
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
// Dummy Components (können in eigene Dateien ausgelagert werden)
|
||||||
import { useNavigate } from 'react-router-dom';
|
import Dashboard from './dashboard';
|
||||||
|
import Appointments from './appointments';
|
||||||
|
import Ressourcen from './ressourcen';
|
||||||
|
import Infoscreens from './clients';
|
||||||
|
import Infoscreen_groups from './infoscreen_groups';
|
||||||
|
import Media from './media';
|
||||||
|
import Benutzer from './benutzer';
|
||||||
|
import Einstellungen from './einstellungen';
|
||||||
|
import SetupMode from './SetupMode';
|
||||||
|
|
||||||
// ENV aus .env holen (Platzhalter, im echten Projekt über process.env oder API)
|
// ENV aus .env holen (Platzhalter, im echten Projekt über process.env oder API)
|
||||||
const ENV = import.meta.env.VITE_ENV || 'development';
|
// const ENV = import.meta.env.VITE_ENV || 'development';
|
||||||
|
|
||||||
const Layout: React.FC = () => {
|
const Layout: React.FC = () => {
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen">
|
<div className="layout-container">
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<aside
|
<aside
|
||||||
className={`sidebar-theme flex flex-col transition-all duration-300 ${collapsed ? 'w-20' : 'w-30'}`}
|
className={`sidebar-theme flex flex-col transition-all duration-300 ${collapsed ? 'w-20' : 'w-30'}`}
|
||||||
@@ -60,6 +74,7 @@ const Layout: React.FC = () => {
|
|||||||
className="sidebar-btn p-2 focus:outline-none transition-colors"
|
className="sidebar-btn p-2 focus:outline-none transition-colors"
|
||||||
onClick={() => setCollapsed(!collapsed)}
|
onClick={() => setCollapsed(!collapsed)}
|
||||||
aria-label={collapsed ? 'Sidebar ausklappen' : 'Sidebar einklappen'}
|
aria-label={collapsed ? 'Sidebar ausklappen' : 'Sidebar einklappen'}
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<span style={{ fontSize: 20 }}>{collapsed ? '▶' : '◀'}</span>
|
<span style={{ fontSize: 20 }}>{collapsed ? '▶' : '◀'}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -88,6 +103,7 @@ const Layout: React.FC = () => {
|
|||||||
// Hier ggf. Logout-Logik einfügen
|
// Hier ggf. Logout-Logik einfügen
|
||||||
window.location.href = '/logout';
|
window.location.href = '/logout';
|
||||||
}}
|
}}
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<LogOut size={22} />
|
<LogOut size={22} />
|
||||||
{!collapsed && 'Abmelden'}
|
{!collapsed && 'Abmelden'}
|
||||||
@@ -119,7 +135,7 @@ const Layout: React.FC = () => {
|
|||||||
[Organisationsname]
|
[Organisationsname]
|
||||||
</span>
|
</span>
|
||||||
</header>
|
</header>
|
||||||
<main className="flex-1 p-8 bg-gray-100">
|
<main className="flex-1 p-8 bg-gray-100 page-content-scroll">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
@@ -127,26 +143,8 @@ const Layout: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function useLoginCheck() {
|
|
||||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (ENV === 'development') {
|
|
||||||
setIsLoggedIn(true); // Im Development immer eingeloggt
|
|
||||||
console.log('[Login] Development-Modus: User automatisch eingeloggt');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Hier echte Loginlogik einbauen (z.B. Token prüfen)
|
|
||||||
// setIsLoggedIn(...)
|
|
||||||
// console.log('[Login] Produktiv-Modus: Login-Check ausgeführt');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return isLoggedIn;
|
|
||||||
}
|
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
|
||||||
const isLoggedIn = useLoginCheck();
|
|
||||||
|
|
||||||
// Automatische Navigation zu /clients bei leerer Beschreibung entfernt
|
// Automatische Navigation zu /clients bei leerer Beschreibung entfernt
|
||||||
|
|
||||||
@@ -176,14 +174,3 @@ const AppWrapper: React.FC = () => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
export default AppWrapper;
|
export default AppWrapper;
|
||||||
|
|
||||||
// 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 Infoscreen_groups from './infoscreen_groups';
|
|
||||||
import Media from './media';
|
|
||||||
import Benutzer from './benutzer';
|
|
||||||
import Einstellungen from './einstellungen';
|
|
||||||
import SetupMode from './SetupMode';
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { fetchClientsWithoutDescription, setClientDescription } from './apiClien
|
|||||||
import { ButtonComponent } from '@syncfusion/ej2-react-buttons';
|
import { ButtonComponent } from '@syncfusion/ej2-react-buttons';
|
||||||
import { TextBoxComponent } from '@syncfusion/ej2-react-inputs';
|
import { TextBoxComponent } from '@syncfusion/ej2-react-inputs';
|
||||||
import { GridComponent, ColumnsDirective, ColumnDirective } from '@syncfusion/ej2-react-grids';
|
import { GridComponent, ColumnsDirective, ColumnDirective } from '@syncfusion/ej2-react-grids';
|
||||||
|
import { DialogComponent } from '@syncfusion/ej2-react-popups';
|
||||||
|
import { useClientDelete } from './hooks/useClientDelete';
|
||||||
|
|
||||||
type Client = {
|
type Client = {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
@@ -14,48 +16,67 @@ type Client = {
|
|||||||
const SetupMode: React.FC = () => {
|
const SetupMode: React.FC = () => {
|
||||||
const [clients, setClients] = useState<Client[]>([]);
|
const [clients, setClients] = useState<Client[]>([]);
|
||||||
const [descriptions, setDescriptions] = useState<Record<string, string>>({});
|
const [descriptions, setDescriptions] = useState<Record<string, string>>({});
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading /* setLoading */] = useState(false);
|
||||||
|
const [inputActive, setInputActive] = useState(false);
|
||||||
|
|
||||||
|
// Lösch-Logik aus Hook (analog zu clients.tsx)
|
||||||
|
const { showDialog, deleteClientId, handleDelete, confirmDelete, cancelDelete } = useClientDelete(
|
||||||
|
async uuid => {
|
||||||
|
// Nach dem Löschen neu laden!
|
||||||
|
const updated = await fetchClientsWithoutDescription();
|
||||||
|
setClients(updated);
|
||||||
|
setDescriptions(prev => {
|
||||||
|
const copy = { ...prev };
|
||||||
|
delete copy[uuid];
|
||||||
|
return copy;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Hilfsfunktion zum Vergleich der Clients
|
||||||
|
const isEqual = (a: Client[], b: Client[]) => {
|
||||||
|
if (a.length !== b.length) return false;
|
||||||
|
const aSorted = [...a].sort((x, y) => x.uuid.localeCompare(y.uuid));
|
||||||
|
const bSorted = [...b].sort((x, y) => x.uuid.localeCompare(y.uuid));
|
||||||
|
for (let i = 0; i < aSorted.length; i++) {
|
||||||
|
if (aSorted[i].uuid !== bSorted[i].uuid) return false;
|
||||||
|
if (aSorted[i].hostname !== bSorted[i].hostname) return false;
|
||||||
|
if (aSorted[i].ip_address !== bSorted[i].ip_address) return false;
|
||||||
|
if (aSorted[i].last_alive !== bSorted[i].last_alive) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let polling: ReturnType<typeof setInterval> | null = null;
|
let polling: ReturnType<typeof setInterval> | null = null;
|
||||||
const isEqual = (a: Client[], b: Client[]) => {
|
|
||||||
if (a.length !== b.length) return false;
|
|
||||||
const aSorted = [...a].sort((x, y) => x.uuid.localeCompare(y.uuid));
|
|
||||||
const bSorted = [...b].sort((x, y) => x.uuid.localeCompare(y.uuid));
|
|
||||||
for (let i = 0; i < aSorted.length; i++) {
|
|
||||||
if (aSorted[i].uuid !== bSorted[i].uuid) return false;
|
|
||||||
if (aSorted[i].hostname !== bSorted[i].hostname) return false;
|
|
||||||
if (aSorted[i].ip_address !== bSorted[i].ip_address) return false;
|
|
||||||
if (aSorted[i].last_alive !== bSorted[i].last_alive) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
let firstLoad = true;
|
|
||||||
const fetchClients = () => {
|
const fetchClients = () => {
|
||||||
if (firstLoad) setLoading(true);
|
if (inputActive) return;
|
||||||
fetchClientsWithoutDescription().then(list => {
|
fetchClientsWithoutDescription().then(list => {
|
||||||
if (firstLoad) {
|
|
||||||
setLoading(false);
|
|
||||||
firstLoad = false;
|
|
||||||
}
|
|
||||||
setClients(prev => (isEqual(prev, list) ? prev : list));
|
setClients(prev => (isEqual(prev, list) ? prev : list));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchClients();
|
fetchClients();
|
||||||
polling = setInterval(fetchClients, 5000); // alle 5 Sekunden
|
polling = setInterval(fetchClients, 5000);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (polling) clearInterval(polling);
|
if (polling) clearInterval(polling);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [inputActive]);
|
||||||
|
|
||||||
const handleDescriptionChange = (uuid: string, value: string) => {
|
const handleDescriptionChange = (uuid: string, value: string) => {
|
||||||
setDescriptions(prev => ({ ...prev, [uuid]: value }));
|
setDescriptions(prev => ({ ...prev, [uuid]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = (uuid: string) => {
|
const handleSave = (uuid: string) => {
|
||||||
setClientDescription(uuid, descriptions[uuid] || '').then(() => {
|
setClientDescription(uuid, descriptions[uuid] || '')
|
||||||
setClients(prev => prev.filter(c => c.uuid !== uuid));
|
.then(() => {
|
||||||
});
|
setClients(prev => prev.filter(c => c.uuid !== uuid));
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Fehler beim Speichern der Beschreibung:', err);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) return <div>Lade neue Clients ...</div>;
|
if (loading) return <div>Lade neue Clients ...</div>;
|
||||||
@@ -68,14 +89,16 @@ const SetupMode: React.FC = () => {
|
|||||||
allowPaging={true}
|
allowPaging={true}
|
||||||
pageSettings={{ pageSize: 10 }}
|
pageSettings={{ pageSize: 10 }}
|
||||||
rowHeight={50}
|
rowHeight={50}
|
||||||
|
width="100%"
|
||||||
|
allowTextWrap={false}
|
||||||
>
|
>
|
||||||
<ColumnsDirective>
|
<ColumnsDirective>
|
||||||
<ColumnDirective field="uuid" headerText="UUID" width="180" />
|
<ColumnDirective field="uuid" headerText="UUID" width="180" />
|
||||||
<ColumnDirective field="hostname" headerText="Hostname" width="140" />
|
<ColumnDirective field="hostname" headerText="Hostname" width="90" />
|
||||||
<ColumnDirective field="ip" headerText="IP" width="120" />
|
<ColumnDirective field="ip_address" headerText="IP" width="80" />
|
||||||
<ColumnDirective
|
<ColumnDirective
|
||||||
headerText="Letzter Kontakt"
|
headerText="Letzter Kontakt"
|
||||||
width="160"
|
width="120"
|
||||||
template={(props: Client) => {
|
template={(props: Client) => {
|
||||||
if (!props.last_alive) return '';
|
if (!props.last_alive) return '';
|
||||||
let iso = props.last_alive;
|
let iso = props.last_alive;
|
||||||
@@ -93,23 +116,57 @@ const SetupMode: React.FC = () => {
|
|||||||
value={descriptions[props.uuid] || ''}
|
value={descriptions[props.uuid] || ''}
|
||||||
placeholder="Beschreibung eingeben"
|
placeholder="Beschreibung eingeben"
|
||||||
change={e => handleDescriptionChange(props.uuid, e.value as string)}
|
change={e => handleDescriptionChange(props.uuid, e.value as string)}
|
||||||
|
focus={() => setInputActive(true)}
|
||||||
|
blur={() => setInputActive(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<ColumnDirective
|
<ColumnDirective
|
||||||
headerText="Aktion"
|
headerText="Aktion"
|
||||||
width="120"
|
width="180"
|
||||||
template={(props: Client) => (
|
template={(props: Client) => (
|
||||||
<ButtonComponent
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
content="Speichern"
|
<ButtonComponent
|
||||||
disabled={!descriptions[props.uuid]}
|
content="Speichern"
|
||||||
onClick={() => handleSave(props.uuid)}
|
disabled={!descriptions[props.uuid]}
|
||||||
/>
|
onClick={() => handleSave(props.uuid)}
|
||||||
|
/>
|
||||||
|
<ButtonComponent
|
||||||
|
content="Entfernen"
|
||||||
|
cssClass="e-danger"
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDelete(props.uuid);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</ColumnsDirective>
|
</ColumnsDirective>
|
||||||
</GridComponent>
|
</GridComponent>
|
||||||
{clients.length === 0 && <div>Keine neuen Clients ohne Beschreibung.</div>}
|
{clients.length === 0 && <div>Keine neuen Clients ohne Beschreibung.</div>}
|
||||||
|
|
||||||
|
{/* Syncfusion Dialog für Sicherheitsabfrage */}
|
||||||
|
{showDialog && deleteClientId && (
|
||||||
|
<DialogComponent
|
||||||
|
visible={showDialog}
|
||||||
|
header="Bestätigung"
|
||||||
|
content={(() => {
|
||||||
|
const client = clients.find(c => c.uuid === deleteClientId);
|
||||||
|
const hostname = client?.hostname ? ` (${client.hostname})` : '';
|
||||||
|
return client
|
||||||
|
? `Möchten Sie diesen Client${hostname} wirklich entfernen?`
|
||||||
|
: 'Client nicht gefunden.';
|
||||||
|
})()}
|
||||||
|
showCloseIcon={true}
|
||||||
|
width="400px"
|
||||||
|
buttons={[
|
||||||
|
{ click: confirmDelete, buttonModel: { content: 'Ja', isPrimary: true } },
|
||||||
|
{ click: cancelDelete, buttonModel: { content: 'Abbrechen' } },
|
||||||
|
]}
|
||||||
|
close={cancelDelete}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,24 @@ export interface Client {
|
|||||||
last_alive?: string;
|
last_alive?: string;
|
||||||
is_active?: boolean;
|
is_active?: boolean;
|
||||||
group_id?: number;
|
group_id?: number;
|
||||||
|
// Für Health-Status
|
||||||
|
is_alive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Group {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
created_at?: string;
|
||||||
|
is_active?: boolean;
|
||||||
|
clients: Client[];
|
||||||
|
}
|
||||||
|
// Liefert alle Gruppen mit zugehörigen Clients
|
||||||
|
export async function fetchGroupsWithClients(): Promise<Group[]> {
|
||||||
|
const response = await fetch('/api/groups/with_clients');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Fehler beim Laden der Gruppen mit Clients');
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchClients(): Promise<Client[]> {
|
export async function fetchClients(): Promise<Client[]> {
|
||||||
@@ -60,3 +78,22 @@ export async function updateClient(uuid: string, data: { description?: string; m
|
|||||||
if (!res.ok) throw new Error('Fehler beim Aktualisieren des Clients');
|
if (!res.ok) throw new Error('Fehler beim Aktualisieren des Clients');
|
||||||
return await res.json();
|
return await res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function restartClient(uuid: string): Promise<{ success: boolean; message?: string }> {
|
||||||
|
const response = await fetch(`/api/clients/${uuid}/restart`, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.error || 'Fehler beim Neustart des Clients');
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteClient(uuid: string) {
|
||||||
|
const res = await fetch(`/api/clients/${uuid}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Fehler beim Entfernen des Clients');
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import SetupModeButton from './components/SetupModeButton';
|
import SetupModeButton from './components/SetupModeButton';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
// ...ButtonComponent entfernt...
|
import { useClientDelete } from './hooks/useClientDelete';
|
||||||
// Card-Komponente wird als eigenes Layout umgesetzt
|
|
||||||
import { fetchClients, updateClient } from './apiClients';
|
import { fetchClients, updateClient } from './apiClients';
|
||||||
import type { Client } from './apiClients';
|
import type { Client } from './apiClients';
|
||||||
import {
|
import {
|
||||||
@@ -15,6 +14,7 @@ import {
|
|||||||
Sort,
|
Sort,
|
||||||
Edit,
|
Edit,
|
||||||
} from '@syncfusion/ej2-react-grids';
|
} from '@syncfusion/ej2-react-grids';
|
||||||
|
import { DialogComponent } from '@syncfusion/ej2-react-popups';
|
||||||
|
|
||||||
// Raumgruppen werden dynamisch aus der API geladen
|
// Raumgruppen werden dynamisch aus der API geladen
|
||||||
|
|
||||||
@@ -88,9 +88,7 @@ function DetailsModal({ open, client, groupIdToName, onClose }: DetailsModalProp
|
|||||||
(value as string).endsWith('Z') ? (value as string) : value + 'Z'
|
(value as string).endsWith('Z') ? (value as string) : value + 'Z'
|
||||||
).toLocaleString()
|
).toLocaleString()
|
||||||
: key === 'last_alive' && value
|
: key === 'last_alive' && value
|
||||||
? new Date(
|
? String(value) // Wert direkt anzeigen, nicht erneut parsen
|
||||||
(value as string).endsWith('Z') ? (value as string) : value + 'Z'
|
|
||||||
).toLocaleString()
|
|
||||||
: String(value)}
|
: String(value)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -111,7 +109,11 @@ function DetailsModal({ open, client, groupIdToName, onClose }: DetailsModalProp
|
|||||||
const Clients: React.FC = () => {
|
const Clients: React.FC = () => {
|
||||||
const [clients, setClients] = useState<Client[]>([]);
|
const [clients, setClients] = useState<Client[]>([]);
|
||||||
const [groups, setGroups] = useState<{ id: number; name: string }[]>([]);
|
const [groups, setGroups] = useState<{ id: number; name: string }[]>([]);
|
||||||
const [selectedClient, setSelectedClient] = useState<Client | null>(null);
|
const [detailsClient, setDetailsClient] = useState<Client | null>(null);
|
||||||
|
|
||||||
|
const { showDialog, deleteClientId, handleDelete, confirmDelete, cancelDelete } = useClientDelete(
|
||||||
|
uuid => setClients(prev => prev.filter(c => c.uuid !== uuid))
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchClients().then(setClients);
|
fetchClients().then(setClients);
|
||||||
@@ -136,15 +138,27 @@ const Clients: React.FC = () => {
|
|||||||
: '',
|
: '',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// DataGrid row template für Details-Button
|
// DataGrid row template für Details- und Entfernen-Button
|
||||||
const detailsButtonTemplate = (props: Client) => (
|
const detailsButtonTemplate = (props: Client) => (
|
||||||
<button
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
className="e-btn e-primary"
|
<button
|
||||||
onClick={() => setSelectedClient(props)}
|
className="e-btn e-primary"
|
||||||
style={{ minWidth: 80 }}
|
onClick={() => setDetailsClient(props)}
|
||||||
>
|
style={{ minWidth: 80 }}
|
||||||
Details
|
>
|
||||||
</button>
|
Details
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="e-btn e-danger"
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDelete(props.uuid);
|
||||||
|
}}
|
||||||
|
style={{ minWidth: 80 }}
|
||||||
|
>
|
||||||
|
Entfernen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -169,9 +183,16 @@ const Clients: React.FC = () => {
|
|||||||
allowDeleting: false,
|
allowDeleting: false,
|
||||||
mode: 'Normal',
|
mode: 'Normal',
|
||||||
}}
|
}}
|
||||||
actionComplete={async (args: { requestType: string; data: any }) => {
|
actionComplete={async (args: {
|
||||||
|
requestType: string;
|
||||||
|
data: Record<string, unknown>;
|
||||||
|
}) => {
|
||||||
if (args.requestType === 'save') {
|
if (args.requestType === 'save') {
|
||||||
const { uuid, description, model } = args.data;
|
const { uuid, description, model } = args.data as {
|
||||||
|
uuid: string;
|
||||||
|
description: string;
|
||||||
|
model: string;
|
||||||
|
};
|
||||||
// API-Aufruf zum Speichern
|
// API-Aufruf zum Speichern
|
||||||
await updateClient(uuid, { description, model });
|
await updateClient(uuid, { description, model });
|
||||||
// Nach dem Speichern neu laden
|
// Nach dem Speichern neu laden
|
||||||
@@ -183,32 +204,27 @@ const Clients: React.FC = () => {
|
|||||||
<ColumnDirective
|
<ColumnDirective
|
||||||
field="description"
|
field="description"
|
||||||
headerText="Beschreibung"
|
headerText="Beschreibung"
|
||||||
width="150"
|
|
||||||
allowEditing={true}
|
allowEditing={true}
|
||||||
|
width="180"
|
||||||
/>
|
/>
|
||||||
<ColumnDirective
|
<ColumnDirective
|
||||||
field="group_name"
|
field="group_name"
|
||||||
headerText="Raumgruppe"
|
headerText="Raumgruppe"
|
||||||
width="120"
|
|
||||||
allowEditing={false}
|
|
||||||
/>
|
|
||||||
<ColumnDirective field="uuid" headerText="UUID" width="220" allowEditing={false} />
|
|
||||||
<ColumnDirective
|
|
||||||
field="ip"
|
|
||||||
headerText="IP-Adresse"
|
|
||||||
width="150"
|
|
||||||
allowEditing={false}
|
allowEditing={false}
|
||||||
|
width="140"
|
||||||
/>
|
/>
|
||||||
|
<ColumnDirective field="uuid" headerText="UUID" allowEditing={false} width="160" />
|
||||||
|
<ColumnDirective field="ip" headerText="IP-Adresse" allowEditing={false} width="80" />
|
||||||
<ColumnDirective
|
<ColumnDirective
|
||||||
field="last_alive"
|
field="last_alive"
|
||||||
headerText="Last Alive"
|
headerText="Last Alive"
|
||||||
width="120"
|
|
||||||
allowEditing={false}
|
allowEditing={false}
|
||||||
|
width="120"
|
||||||
/>
|
/>
|
||||||
<ColumnDirective field="model" headerText="Model" width="120" allowEditing={true} />
|
<ColumnDirective field="model" headerText="Model" allowEditing={true} width="120" />
|
||||||
<ColumnDirective
|
<ColumnDirective
|
||||||
headerText="Details"
|
headerText="Aktion"
|
||||||
width="100"
|
width="190"
|
||||||
template={detailsButtonTemplate}
|
template={detailsButtonTemplate}
|
||||||
textAlign="Center"
|
textAlign="Center"
|
||||||
allowEditing={false}
|
allowEditing={false}
|
||||||
@@ -217,15 +233,30 @@ const Clients: React.FC = () => {
|
|||||||
<Inject services={[Page, Toolbar, Search, Sort, Edit]} />
|
<Inject services={[Page, Toolbar, Search, Sort, Edit]} />
|
||||||
</GridComponent>
|
</GridComponent>
|
||||||
<DetailsModal
|
<DetailsModal
|
||||||
open={!!selectedClient}
|
open={!!detailsClient}
|
||||||
client={selectedClient}
|
client={detailsClient}
|
||||||
groupIdToName={groupIdToName}
|
groupIdToName={groupIdToName}
|
||||||
onClose={() => setSelectedClient(null)}
|
onClose={() => setDetailsClient(null)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-gray-500">Raumgruppen werden geladen ...</div>
|
<div className="text-gray-500">Raumgruppen werden geladen ...</div>
|
||||||
)}
|
)}
|
||||||
|
{/* DialogComponent für Bestätigung */}
|
||||||
|
{showDialog && deleteClientId && (
|
||||||
|
<DialogComponent
|
||||||
|
visible={showDialog}
|
||||||
|
header="Bestätigung"
|
||||||
|
content="Möchten Sie diesen Client wirklich entfernen?"
|
||||||
|
showCloseIcon={true}
|
||||||
|
width="400px"
|
||||||
|
buttons={[
|
||||||
|
{ click: confirmDelete, buttonModel: { content: 'Ja', isPrimary: true } },
|
||||||
|
{ click: cancelDelete, buttonModel: { content: 'Abbrechen' } },
|
||||||
|
]}
|
||||||
|
close={cancelDelete}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,47 +1,188 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
import { fetchClients, fetchClientsWithoutDescription } from './apiClients';
|
import { fetchGroupsWithClients, restartClient } from './apiClients';
|
||||||
import type { Client } from './apiClients';
|
import type { Group } from './apiClients';
|
||||||
|
import {
|
||||||
|
GridComponent,
|
||||||
|
ColumnsDirective,
|
||||||
|
ColumnDirective,
|
||||||
|
Page,
|
||||||
|
DetailRow,
|
||||||
|
Inject,
|
||||||
|
Sort,
|
||||||
|
} from '@syncfusion/ej2-react-grids';
|
||||||
|
|
||||||
|
const REFRESH_INTERVAL = 15000; // 15 Sekunden
|
||||||
|
|
||||||
const Dashboard: React.FC = () => {
|
const Dashboard: React.FC = () => {
|
||||||
const [clients, setClients] = useState<Client[]>([]);
|
const [groups, setGroups] = useState<Group[]>([]);
|
||||||
|
const [expandedGroupIds, setExpandedGroupIds] = useState<string[]>([]);
|
||||||
|
const gridRef = useRef<any>(null);
|
||||||
|
|
||||||
|
// Funktion für das Schließen einer Gruppe (Collapse)
|
||||||
|
const onDetailCollapse = (args: any) => {
|
||||||
|
if (args && args.data && args.data.id) {
|
||||||
|
const groupId = String(args.data.id);
|
||||||
|
setExpandedGroupIds(prev => prev.filter(id => String(id) !== groupId));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Registriere das Event nach dem Mount am Grid
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchClients().then(setClients).catch(console.error);
|
if (gridRef.current) {
|
||||||
|
gridRef.current.detailCollapse = onDetailCollapse;
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Optimiertes Update: Nur bei echten Datenänderungen wird das Grid aktualisiert
|
||||||
|
useEffect(() => {
|
||||||
|
let lastGroups: Group[] = [];
|
||||||
|
const fetchAndUpdate = async () => {
|
||||||
|
const newGroups = await fetchGroupsWithClients();
|
||||||
|
// Vergleiche nur die relevanten Felder (id, clients, is_alive)
|
||||||
|
const changed =
|
||||||
|
lastGroups.length !== newGroups.length ||
|
||||||
|
lastGroups.some((g, i) => {
|
||||||
|
const ng = newGroups[i];
|
||||||
|
if (!ng || g.id !== ng.id || g.clients.length !== ng.clients.length) return true;
|
||||||
|
// Optional: Vergleiche tiefer, z.B. Alive-Status
|
||||||
|
for (let j = 0; j < g.clients.length; j++) {
|
||||||
|
if (
|
||||||
|
g.clients[j].uuid !== ng.clients[j].uuid ||
|
||||||
|
g.clients[j].is_alive !== ng.clients[j].is_alive
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
if (changed) {
|
||||||
|
setGroups(newGroups);
|
||||||
|
lastGroups = newGroups;
|
||||||
|
setTimeout(() => {
|
||||||
|
expandedGroupIds.forEach(id => {
|
||||||
|
const rowIndex = newGroups.findIndex(g => String(g.id) === String(id));
|
||||||
|
if (rowIndex !== -1 && gridRef.current) {
|
||||||
|
gridRef.current.detailRowModule.expand(rowIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchAndUpdate();
|
||||||
|
const interval = setInterval(fetchAndUpdate, REFRESH_INTERVAL);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [expandedGroupIds]);
|
||||||
|
|
||||||
|
// Health-Badge
|
||||||
|
const getHealthBadge = (group: Group) => {
|
||||||
|
const total = group.clients.length;
|
||||||
|
const alive = group.clients.filter((c: any) => c.is_alive).length;
|
||||||
|
const ratio = total === 0 ? 0 : alive / total;
|
||||||
|
let color = 'danger';
|
||||||
|
let text = `${alive} / ${total} offline`;
|
||||||
|
if (ratio === 1) {
|
||||||
|
color = 'success';
|
||||||
|
text = `${alive} / ${total} alive`;
|
||||||
|
} else if (ratio >= 0.5) {
|
||||||
|
color = 'warning';
|
||||||
|
text = `${alive} / ${total} teilw. alive`;
|
||||||
|
}
|
||||||
|
return <span className={`e-badge e-badge-${color}`}>{text}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Einfache Tabelle für Clients einer Gruppe
|
||||||
|
const getClientTable = (group: Group) => (
|
||||||
|
<div style={{ maxHeight: 300, overflowY: 'auto', marginBottom: '5px' }}>
|
||||||
|
<GridComponent dataSource={group.clients} allowSorting={true} height={'auto'}>
|
||||||
|
<ColumnsDirective>
|
||||||
|
<ColumnDirective field="description" headerText="Beschreibung" width="150" />
|
||||||
|
<ColumnDirective field="ip" headerText="IP" width="120" />
|
||||||
|
<ColumnDirective
|
||||||
|
field="last_alive"
|
||||||
|
headerText="Letztes Lebenszeichen"
|
||||||
|
width="180"
|
||||||
|
template={(props: { last_alive: string | null }) => {
|
||||||
|
if (!props.last_alive) return '-';
|
||||||
|
const dateStr = props.last_alive.endsWith('Z')
|
||||||
|
? props.last_alive
|
||||||
|
: props.last_alive + 'Z';
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
return isNaN(date.getTime()) ? props.last_alive : date.toLocaleString();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ColumnDirective
|
||||||
|
field="is_alive"
|
||||||
|
headerText="Alive"
|
||||||
|
width="100"
|
||||||
|
template={(props: { is_alive: boolean }) => (
|
||||||
|
<span className={`e-badge e-badge-${props.is_alive ? 'success' : 'danger'}`}>
|
||||||
|
{props.is_alive ? 'alive' : 'offline'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
sortComparer={(a, b) => (a === b ? 0 : a ? -1 : 1)}
|
||||||
|
/>
|
||||||
|
<ColumnDirective
|
||||||
|
headerText="Aktionen"
|
||||||
|
width="150"
|
||||||
|
template={(props: { uuid: string }) => (
|
||||||
|
<button className="e-btn e-primary" onClick={() => handleRestartClient(props.uuid)}>
|
||||||
|
Neustart
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</ColumnsDirective>
|
||||||
|
<Inject services={[Sort]} />
|
||||||
|
</GridComponent>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Neustart-Logik
|
||||||
|
const handleRestartClient = async (uuid: string) => {
|
||||||
|
try {
|
||||||
|
const result = await restartClient(uuid);
|
||||||
|
alert(`Neustart erfolgreich: ${result.message}`);
|
||||||
|
} catch (error: any) {
|
||||||
|
alert(`Fehler beim Neustart: ${error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// SyncFusion Grid liefert im Event die Zeile/Gruppe
|
||||||
|
const onDetailDataBound = (args: any) => {
|
||||||
|
if (args && args.data && args.data.id) {
|
||||||
|
const groupId = args.data.id;
|
||||||
|
setExpandedGroupIds(prev => (prev.includes(groupId) ? prev : [...prev, groupId]));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<header className="mb-8 pb-4 border-b-2 border-[#d6c3a6]">
|
<header className="mb-8 pb-4 border-b-2 border-[#d6c3a6]">
|
||||||
<h2 className="text-3xl font-extrabold mb-2">Dashboard</h2>
|
<h2 className="text-3xl font-extrabold mb-2">Dashboard</h2>
|
||||||
</header>
|
</header>
|
||||||
<h3 className="text-lg font-semibold mt-6 mb-4">Infoscreens</h3>
|
<h3 className="text-lg font-semibold mt-6 mb-4">Raumgruppen Übersicht</h3>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<GridComponent
|
||||||
{clients.map(client => (
|
dataSource={groups}
|
||||||
<div key={client.uuid} className="bg-white rounded shadow p-4 flex flex-col items-center">
|
allowPaging={true}
|
||||||
<h4 className="text-lg font-bold mb-2">{client.description || 'Unbekannter Standort'}</h4>
|
pageSettings={{ pageSize: 5 }}
|
||||||
<img
|
height={400}
|
||||||
src={`/screenshots/${client.uuid}`}
|
detailTemplate={(props: Group) => getClientTable(props)}
|
||||||
alt={`Screenshot ${client.description || 'Unbekannt'}`}
|
detailDataBound={onDetailDataBound}
|
||||||
className="w-full h-48 object-contain bg-gray-100 mb-2"
|
ref={gridRef}
|
||||||
onError={e => {
|
>
|
||||||
e.currentTarget.onerror = null; // verhindert Endlosschleife
|
<Inject services={[Page, DetailRow]} />
|
||||||
e.currentTarget.src = "https://placehold.co/400x300?text=No+Screenshot";
|
<ColumnsDirective>
|
||||||
}}
|
<ColumnDirective field="name" headerText="Raumgruppe" width="180" />
|
||||||
/>
|
<ColumnDirective
|
||||||
<div className="text-sm text-gray-700 mb-1">
|
headerText="Health"
|
||||||
<span className="font-semibold">IP:</span> {client.ip}
|
width="160"
|
||||||
</div>
|
template={(props: any) => getHealthBadge(props)}
|
||||||
<div className="text-sm text-gray-700">
|
/>
|
||||||
<span className="font-semibold">Letztes Lebenszeichen:</span>{' '}
|
</ColumnsDirective>
|
||||||
{client.last_alive ? new Date(client.last_alive).toLocaleString() : '-'}
|
</GridComponent>
|
||||||
</div>
|
{groups.length === 0 && (
|
||||||
</div>
|
<div className="col-span-full text-center text-gray-400">Keine Gruppen gefunden.</div>
|
||||||
))}
|
)}
|
||||||
{clients.length === 0 && (
|
|
||||||
<div className="col-span-full text-center text-gray-400">Keine Clients gefunden.</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
36
dashboard/src/hooks/useClientDelete.ts
Normal file
36
dashboard/src/hooks/useClientDelete.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { deleteClient } from '../apiClients';
|
||||||
|
|
||||||
|
export function useClientDelete(onDeleted?: (uuid: string) => void) {
|
||||||
|
const [showDialog, setShowDialog] = useState(false);
|
||||||
|
const [deleteClientId, setDeleteClientId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Details-Modal separat im Parent verwalten!
|
||||||
|
|
||||||
|
const handleDelete = (uuid: string) => {
|
||||||
|
setDeleteClientId(uuid);
|
||||||
|
setShowDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmDelete = async () => {
|
||||||
|
if (deleteClientId) {
|
||||||
|
await deleteClient(deleteClientId);
|
||||||
|
setShowDialog(false);
|
||||||
|
if (onDeleted) onDeleted(deleteClientId);
|
||||||
|
setDeleteClientId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelDelete = () => {
|
||||||
|
setShowDialog(false);
|
||||||
|
setDeleteClientId(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
showDialog,
|
||||||
|
deleteClientId,
|
||||||
|
handleDelete,
|
||||||
|
confirmDelete,
|
||||||
|
cancelDelete,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -45,3 +45,4 @@ USER infoscreen_taa
|
|||||||
|
|
||||||
# Default Command: Flask Development Server
|
# Default Command: Flask Development Server
|
||||||
CMD ["python", "-m", "debugpy", "--listen", "0.0.0.0:5678", "wsgi.py"]
|
CMD ["python", "-m", "debugpy", "--listen", "0.0.0.0:5678", "wsgi.py"]
|
||||||
|
# CMD ["sleep", "infinity"]
|
||||||
|
|||||||
@@ -131,3 +131,70 @@ def get_client_group(uuid):
|
|||||||
group_id = client.group_id
|
group_id = client.group_id
|
||||||
session.close()
|
session.close()
|
||||||
return jsonify({"group_id": group_id})
|
return jsonify({"group_id": group_id})
|
||||||
|
|
||||||
|
# Neue Route: Liefert alle Clients mit Alive-Status
|
||||||
|
|
||||||
|
|
||||||
|
@clients_bp.route("/with_alive_status", methods=["GET"])
|
||||||
|
def get_clients_with_alive_status():
|
||||||
|
session = Session()
|
||||||
|
clients = session.query(Client).all()
|
||||||
|
result = []
|
||||||
|
for c in clients:
|
||||||
|
result.append({
|
||||||
|
"uuid": c.uuid,
|
||||||
|
"description": c.description,
|
||||||
|
"ip": c.ip,
|
||||||
|
"last_alive": c.last_alive.isoformat() if c.last_alive else None,
|
||||||
|
"is_active": c.is_active,
|
||||||
|
"is_alive": bool(c.last_alive and c.is_active),
|
||||||
|
})
|
||||||
|
session.close()
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
|
|
||||||
|
@clients_bp.route("/<uuid>/restart", methods=["POST"])
|
||||||
|
def restart_client(uuid):
|
||||||
|
"""
|
||||||
|
Route to restart a specific client by UUID.
|
||||||
|
Sends an MQTT message to the broker to trigger the restart.
|
||||||
|
"""
|
||||||
|
import paho.mqtt.client as mqtt
|
||||||
|
import json
|
||||||
|
|
||||||
|
# MQTT broker configuration
|
||||||
|
MQTT_BROKER = "mqtt"
|
||||||
|
MQTT_PORT = 1883
|
||||||
|
MQTT_TOPIC = f"clients/{uuid}/restart"
|
||||||
|
|
||||||
|
# Connect to the database to check if the client exists
|
||||||
|
session = Session()
|
||||||
|
client = session.query(Client).filter_by(uuid=uuid).first()
|
||||||
|
if not client:
|
||||||
|
session.close()
|
||||||
|
return jsonify({"error": "Client nicht gefunden"}), 404
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
# Send MQTT message
|
||||||
|
try:
|
||||||
|
mqtt_client = mqtt.Client()
|
||||||
|
mqtt_client.connect(MQTT_BROKER, MQTT_PORT)
|
||||||
|
payload = {"action": "restart"}
|
||||||
|
mqtt_client.publish(MQTT_TOPIC, json.dumps(payload))
|
||||||
|
mqtt_client.disconnect()
|
||||||
|
return jsonify({"success": True, "message": f"Restart signal sent to client {uuid}"}), 200
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": f"Failed to send MQTT message: {str(e)}"}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@clients_bp.route("/<uuid>", methods=["DELETE"])
|
||||||
|
def delete_client(uuid):
|
||||||
|
session = Session()
|
||||||
|
client = session.query(Client).filter_by(uuid=uuid).first()
|
||||||
|
if not client:
|
||||||
|
session.close()
|
||||||
|
return jsonify({"error": "Client nicht gefunden"}), 404
|
||||||
|
session.delete(client)
|
||||||
|
session.commit()
|
||||||
|
session.close()
|
||||||
|
return jsonify({"success": True})
|
||||||
|
|||||||
@@ -1,13 +1,45 @@
|
|||||||
|
from models.models import Client
|
||||||
|
# Neue Route: Liefert alle Gruppen mit zugehörigen Clients und deren Alive-Status
|
||||||
|
|
||||||
from database import Session
|
from database import Session
|
||||||
from models.models import ClientGroup
|
from models.models import ClientGroup
|
||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
sys.path.append('/workspace')
|
sys.path.append('/workspace')
|
||||||
|
|
||||||
groups_bp = Blueprint("groups", __name__, url_prefix="/api/groups")
|
groups_bp = Blueprint("groups", __name__, url_prefix="/api/groups")
|
||||||
|
|
||||||
|
|
||||||
|
def get_grace_period():
|
||||||
|
"""Wählt die Grace-Periode abhängig von ENV."""
|
||||||
|
env = os.environ.get("ENV", "production").lower()
|
||||||
|
if env == "development" or env == "dev":
|
||||||
|
return int(os.environ.get("HEARTBEAT_GRACE_PERIOD_DEV", "15"))
|
||||||
|
return int(os.environ.get("HEARTBEAT_GRACE_PERIOD_PROD", "180"))
|
||||||
|
|
||||||
|
|
||||||
|
def is_client_alive(last_alive, is_active):
|
||||||
|
"""Berechnet, ob ein Client als alive gilt."""
|
||||||
|
if not last_alive or not is_active:
|
||||||
|
return False
|
||||||
|
grace_period = get_grace_period()
|
||||||
|
# last_alive kann ein String oder datetime sein
|
||||||
|
if isinstance(last_alive, str):
|
||||||
|
last_alive_str = last_alive[:-
|
||||||
|
1] if last_alive.endswith('Z') else last_alive
|
||||||
|
try:
|
||||||
|
last_alive_dt = datetime.fromisoformat(last_alive_str)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
last_alive_dt = last_alive
|
||||||
|
return datetime.utcnow() - last_alive_dt <= timedelta(seconds=grace_period)
|
||||||
|
|
||||||
|
|
||||||
@groups_bp.route("", methods=["POST"])
|
@groups_bp.route("", methods=["POST"])
|
||||||
def create_group():
|
def create_group():
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
@@ -127,3 +159,31 @@ def rename_group_by_name(old_name):
|
|||||||
}
|
}
|
||||||
session.close()
|
session.close()
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
|
|
||||||
|
|
||||||
|
@groups_bp.route("/with_clients", methods=["GET"])
|
||||||
|
def get_groups_with_clients():
|
||||||
|
session = Session()
|
||||||
|
groups = session.query(ClientGroup).all()
|
||||||
|
result = []
|
||||||
|
for g in groups:
|
||||||
|
clients = session.query(Client).filter_by(group_id=g.id).all()
|
||||||
|
client_list = []
|
||||||
|
for c in clients:
|
||||||
|
client_list.append({
|
||||||
|
"uuid": c.uuid,
|
||||||
|
"description": c.description,
|
||||||
|
"ip": c.ip,
|
||||||
|
"last_alive": c.last_alive.isoformat() if c.last_alive else None,
|
||||||
|
"is_active": c.is_active,
|
||||||
|
"is_alive": is_client_alive(c.last_alive, c.is_active),
|
||||||
|
})
|
||||||
|
result.append({
|
||||||
|
"id": g.id,
|
||||||
|
"name": g.name,
|
||||||
|
"created_at": g.created_at.isoformat() if g.created_at else None,
|
||||||
|
"is_active": g.is_active,
|
||||||
|
"clients": client_list,
|
||||||
|
})
|
||||||
|
session.close()
|
||||||
|
return jsonify(result)
|
||||||
|
|||||||
Reference in New Issue
Block a user