267 lines
9.1 KiB
TypeScript
267 lines
9.1 KiB
TypeScript
import SetupModeButton from './components/SetupModeButton';
|
|
import React, { useEffect, useState } from 'react';
|
|
import { useClientDelete } from './hooks/useClientDelete';
|
|
import { fetchClients, updateClient } from './apiClients';
|
|
import type { Client } from './apiClients';
|
|
import {
|
|
GridComponent,
|
|
ColumnsDirective,
|
|
ColumnDirective,
|
|
Page,
|
|
Inject,
|
|
Toolbar,
|
|
Search,
|
|
Sort,
|
|
Edit,
|
|
} from '@syncfusion/ej2-react-grids';
|
|
import { DialogComponent } from '@syncfusion/ej2-react-popups';
|
|
import { ButtonComponent } from '@syncfusion/ej2-react-buttons';
|
|
|
|
// Raumgruppen werden dynamisch aus der API geladen
|
|
|
|
// Details dialog renders via Syncfusion Dialog for consistent look & feel
|
|
function DetailsContent({ client, groupIdToName }: { client: Client; groupIdToName: Record<string | number, string> }) {
|
|
return (
|
|
<div className="e-card-content">
|
|
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
|
<tbody>
|
|
{Object.entries(client)
|
|
.filter(
|
|
([key]) =>
|
|
![
|
|
'index',
|
|
'is_active',
|
|
'type',
|
|
'column',
|
|
'group_name',
|
|
'foreignKeyData',
|
|
'hardware_token',
|
|
].includes(key)
|
|
)
|
|
.map(([key, value]) => (
|
|
<tr key={key}>
|
|
<td style={{ fontWeight: 600, padding: '6px 8px', width: '40%' }}>
|
|
{key === 'group_id'
|
|
? 'Raumgruppe'
|
|
: key === 'ip'
|
|
? 'IP-Adresse'
|
|
: key === 'registration_time'
|
|
? 'Registriert am'
|
|
: key === 'description'
|
|
? 'Beschreibung'
|
|
: key === 'last_alive'
|
|
? 'Letzter Kontakt'
|
|
: key === 'model'
|
|
? 'Modell'
|
|
: key === 'uuid'
|
|
? 'Client-Code'
|
|
: key === 'os_version'
|
|
? 'Betriebssystem'
|
|
: key === 'software_version'
|
|
? 'Clientsoftware'
|
|
: key === 'macs'
|
|
? 'MAC-Adressen'
|
|
: key.charAt(0).toUpperCase() + key.slice(1)}
|
|
</td>
|
|
<td style={{ padding: '6px 8px' }}>
|
|
{key === 'group_id'
|
|
? value !== undefined
|
|
? groupIdToName[value as string | number] || String(value)
|
|
: ''
|
|
: key === 'registration_time' && value
|
|
? new Date(
|
|
(value as string).endsWith('Z') ? (value as string) : String(value) + 'Z'
|
|
).toLocaleString()
|
|
: key === 'last_alive' && value
|
|
? String(value)
|
|
: key === 'macs' && typeof value === 'string'
|
|
? value.replace(/,\s*/g, ', ')
|
|
: String(value)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const Clients: React.FC = () => {
|
|
const [clients, setClients] = useState<Client[]>([]);
|
|
const [groups, setGroups] = useState<{ id: number; name: string }[]>([]);
|
|
const [detailsClient, setDetailsClient] = useState<Client | null>(null);
|
|
|
|
const { showDialog, deleteClientId, handleDelete, confirmDelete, cancelDelete } = useClientDelete(
|
|
uuid => setClients(prev => prev.filter(c => c.uuid !== uuid))
|
|
);
|
|
|
|
useEffect(() => {
|
|
fetchClients().then(setClients);
|
|
// Gruppen auslesen
|
|
import('./apiGroups').then(mod => mod.fetchGroups()).then(setGroups);
|
|
}, []);
|
|
|
|
// Map group_id zu group_name
|
|
const groupIdToName: Record<string | number, string> = {};
|
|
groups.forEach(g => {
|
|
groupIdToName[g.id] = g.name;
|
|
});
|
|
|
|
// DataGrid data mit korrektem Gruppennamen und formatierten Zeitangaben
|
|
const gridData = clients.map(c => ({
|
|
...c,
|
|
group_name: c.group_id !== undefined ? groupIdToName[c.group_id] || String(c.group_id) : '',
|
|
last_alive: c.last_alive
|
|
? new Date(
|
|
(c.last_alive as string).endsWith('Z') ? (c.last_alive as string) : c.last_alive + 'Z'
|
|
).toLocaleString()
|
|
: '',
|
|
}));
|
|
|
|
// DataGrid row template für Details- und Entfernen-Button
|
|
const detailsButtonTemplate = (props: Client) => (
|
|
<div style={{ display: 'flex', gap: 8, justifyContent: 'center' }}>
|
|
<ButtonComponent cssClass="e-primary" onClick={() => setDetailsClient(props)}>
|
|
Details
|
|
</ButtonComponent>
|
|
<ButtonComponent
|
|
cssClass="e-danger"
|
|
onClick={e => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
handleDelete(props.uuid);
|
|
}}
|
|
>
|
|
Entfernen
|
|
</ButtonComponent>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div id="dialog-target">
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
marginBottom: 16,
|
|
gap: 12,
|
|
flexWrap: 'wrap',
|
|
}}
|
|
>
|
|
<h2 style={{ margin: 0, fontSize: '1.25rem', fontWeight: 700 }}>
|
|
Client-Übersicht
|
|
</h2>
|
|
<SetupModeButton />
|
|
</div>
|
|
{groups.length > 0 ? (
|
|
<>
|
|
<GridComponent
|
|
dataSource={gridData}
|
|
allowPaging={true}
|
|
pageSettings={{ pageSize: 10 }}
|
|
toolbar={['Search', 'Edit', 'Update', 'Cancel']}
|
|
allowSorting={true}
|
|
allowFiltering={true}
|
|
height={420}
|
|
editSettings={{
|
|
allowEditing: true,
|
|
allowAdding: false,
|
|
allowDeleting: false,
|
|
mode: 'Normal',
|
|
}}
|
|
actionComplete={async (args: {
|
|
requestType: string;
|
|
data: Record<string, unknown>;
|
|
}) => {
|
|
if (args.requestType === 'save') {
|
|
const { uuid, description, model } = args.data as {
|
|
uuid: string;
|
|
description: string;
|
|
model: string;
|
|
};
|
|
// API-Aufruf zum Speichern
|
|
await updateClient(uuid, { description, model });
|
|
// Nach dem Speichern neu laden
|
|
fetchClients().then(setClients);
|
|
}
|
|
}}
|
|
>
|
|
<ColumnsDirective>
|
|
<ColumnDirective
|
|
field="description"
|
|
headerText="Beschreibung"
|
|
allowEditing={true}
|
|
width="180"
|
|
/>
|
|
<ColumnDirective
|
|
field="group_name"
|
|
headerText="Raumgruppe"
|
|
allowEditing={false}
|
|
width="140"
|
|
/>
|
|
<ColumnDirective field="uuid" headerText="UUID" allowEditing={false} width="160" />
|
|
<ColumnDirective field="ip" headerText="IP-Adresse" allowEditing={false} width="100" />
|
|
<ColumnDirective
|
|
field="last_alive"
|
|
headerText="Last Alive"
|
|
allowEditing={false}
|
|
width="150"
|
|
/>
|
|
<ColumnDirective field="model" headerText="Model" allowEditing={true} width="140" />
|
|
<ColumnDirective
|
|
headerText="Aktion"
|
|
width="210"
|
|
template={detailsButtonTemplate}
|
|
textAlign="Center"
|
|
allowEditing={false}
|
|
/>
|
|
</ColumnsDirective>
|
|
<Inject services={[Page, Toolbar, Search, Sort, Edit]} />
|
|
</GridComponent>
|
|
</>
|
|
) : (
|
|
<span style={{ color: '#6b7280' }}>Raumgruppen werden geladen ...</span>
|
|
)}
|
|
|
|
{/* Details-Dialog */}
|
|
{detailsClient && (
|
|
<DialogComponent
|
|
visible={!!detailsClient}
|
|
header="Client-Details"
|
|
showCloseIcon={true}
|
|
target="#dialog-target"
|
|
width="560px"
|
|
close={() => setDetailsClient(null)}
|
|
footerTemplate={() => (
|
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
|
<ButtonComponent onClick={() => setDetailsClient(null)}>{'Schließen'}</ButtonComponent>
|
|
</div>
|
|
)}
|
|
>
|
|
<DetailsContent client={detailsClient} groupIdToName={groupIdToName} />
|
|
</DialogComponent>
|
|
)}
|
|
|
|
{/* Bestätigungs-Dialog für Löschen */}
|
|
{showDialog && deleteClientId && (
|
|
<DialogComponent
|
|
visible={showDialog}
|
|
header="Bestätigung"
|
|
content="Möchten Sie diesen Client wirklich entfernen?"
|
|
showCloseIcon={true}
|
|
width="400px"
|
|
target="#dialog-target"
|
|
buttons={[
|
|
{ click: confirmDelete, buttonModel: { content: 'Ja', isPrimary: true } },
|
|
{ click: cancelDelete, buttonModel: { content: 'Abbrechen' } },
|
|
]}
|
|
close={cancelDelete}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Clients;
|