252 lines
6.7 KiB
TypeScript
252 lines
6.7 KiB
TypeScript
// app/(app)/devices/page.tsx
|
|
'use client';
|
|
|
|
import Button from '@/components/ui/Button';
|
|
import Table, { TableColumn } from '@/components/ui/Table';
|
|
import { Dropdown } from '@/components/ui/Dropdown';
|
|
import {
|
|
BookOpenIcon,
|
|
PencilIcon,
|
|
TrashIcon,
|
|
} from '@heroicons/react/24/outline';
|
|
|
|
type DeviceRow = {
|
|
id: string;
|
|
|
|
// Fachliche Felder (entsprechend deinem Prisma-Model)
|
|
name: string;
|
|
manufacturer: string;
|
|
model: string;
|
|
inventoryNumber: string;
|
|
serialNumber?: string | null;
|
|
productNumber?: string | null;
|
|
comment?: string | null;
|
|
|
|
// optionale Netzwerk-/Zugangs-Felder
|
|
ipv4Address?: string | null;
|
|
ipv6Address?: string | null;
|
|
macAddress?: string | null;
|
|
username?: string | null;
|
|
|
|
// Beziehungen (als einfache Strings für die Tabelle)
|
|
group?: string | null;
|
|
location?: string | null;
|
|
|
|
// Audit
|
|
updatedAt: string;
|
|
};
|
|
|
|
// TODO: später per Prisma laden
|
|
const mockDevices: DeviceRow[] = [
|
|
{
|
|
id: '1',
|
|
name: 'Dienstrechner Sachbearbeitung 1',
|
|
manufacturer: 'Dell',
|
|
model: 'OptiPlex 7010',
|
|
inventoryNumber: 'INV-00123',
|
|
serialNumber: 'SN-ABC-123',
|
|
productNumber: 'PN-4711',
|
|
group: 'Dienstrechner',
|
|
location: 'Raum 1.12',
|
|
comment: 'Steht am Fensterplatz',
|
|
ipv4Address: '10.0.0.12',
|
|
ipv6Address: null,
|
|
macAddress: '00-11-22-33-44-55',
|
|
username: 'sachb1',
|
|
updatedAt: '2025-01-10T09:15:00Z',
|
|
},
|
|
{
|
|
id: '2',
|
|
name: 'Monitor Lager 27"',
|
|
manufacturer: 'Samsung',
|
|
model: 'S27F350',
|
|
inventoryNumber: 'INV-00124',
|
|
serialNumber: 'SN-DEF-456',
|
|
productNumber: 'PN-0815',
|
|
group: 'Monitore',
|
|
location: 'Lager Keller',
|
|
comment: null,
|
|
ipv4Address: null,
|
|
ipv6Address: null,
|
|
macAddress: null,
|
|
username: null,
|
|
updatedAt: '2025-01-08T14:30:00Z',
|
|
},
|
|
];
|
|
|
|
function formatDate(iso: string) {
|
|
return new Intl.DateTimeFormat('de-DE', {
|
|
dateStyle: 'short',
|
|
timeStyle: 'short',
|
|
}).format(new Date(iso));
|
|
}
|
|
|
|
const columns: TableColumn<DeviceRow>[] = [
|
|
{
|
|
key: 'name',
|
|
header: 'Bezeichnung',
|
|
sortable: true,
|
|
canHide: true,
|
|
headerClassName: 'min-w-48',
|
|
cellClassName: 'font-medium text-gray-900 dark:text-white',
|
|
},
|
|
{
|
|
key: 'inventoryNumber',
|
|
header: 'Inventar-Nr.',
|
|
sortable: true,
|
|
canHide: false,
|
|
headerClassName: 'min-w-32',
|
|
},
|
|
{
|
|
key: 'manufacturer',
|
|
header: 'Hersteller',
|
|
sortable: true,
|
|
canHide: false,
|
|
},
|
|
{
|
|
key: 'model',
|
|
header: 'Modell',
|
|
sortable: true,
|
|
canHide: false,
|
|
},
|
|
{
|
|
key: 'serialNumber',
|
|
header: 'Seriennummer',
|
|
sortable: true,
|
|
canHide: true,
|
|
},
|
|
{
|
|
key: 'productNumber',
|
|
header: 'Produktnummer',
|
|
sortable: true,
|
|
canHide: true,
|
|
},
|
|
{
|
|
key: 'group',
|
|
header: 'Gruppe',
|
|
sortable: true,
|
|
canHide: true,
|
|
},
|
|
{
|
|
key: 'location',
|
|
header: 'Standort / Raum',
|
|
sortable: true,
|
|
canHide: false,
|
|
},
|
|
{
|
|
key: 'comment',
|
|
header: 'Kommentar',
|
|
sortable: false,
|
|
canHide: true,
|
|
cellClassName: 'whitespace-normal max-w-xs',
|
|
},
|
|
{
|
|
key: 'updatedAt',
|
|
header: 'Geändert am',
|
|
sortable: true,
|
|
canHide: true,
|
|
render: (row) => formatDate(row.updatedAt),
|
|
},
|
|
];
|
|
|
|
export default function DevicesPage() {
|
|
const devices = mockDevices;
|
|
|
|
return (
|
|
<>
|
|
{/* Header über der Tabelle */}
|
|
<div className="flex items-center justify-between gap-4">
|
|
<div>
|
|
<h1 className="text-2xl font-semibold text-gray-900 dark:text-white">
|
|
Geräte
|
|
</h1>
|
|
<p className="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
|
Übersicht aller erfassten Geräte im Inventar.
|
|
</p>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
className="inline-flex items-center rounded-md bg-indigo-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:hover:bg-indigo-400 dark:focus-visible:outline-indigo-500"
|
|
>
|
|
Neues Gerät anlegen
|
|
</button>
|
|
</div>
|
|
|
|
{/* Tabelle */}
|
|
<div className="mt-8">
|
|
<Table<DeviceRow>
|
|
data={devices}
|
|
columns={columns}
|
|
getRowId={(row) => row.id}
|
|
selectable
|
|
actionsHeader=""
|
|
renderActions={(row) => (
|
|
<div className="flex justify-end">
|
|
{/* Desktop: drei Icon-Buttons nebeneinander */}
|
|
<div className="hidden gap-2 lg:flex">
|
|
<Button
|
|
variant="soft"
|
|
tone="indigo"
|
|
size="md"
|
|
icon={<BookOpenIcon className="size-5" />}
|
|
aria-label={`Details zu ${row.inventoryNumber}`}
|
|
onClick={() => console.log('Details', row.id)}
|
|
/>
|
|
|
|
<Button
|
|
variant="soft"
|
|
tone="gray"
|
|
size="md"
|
|
icon={<PencilIcon className="size-5" />}
|
|
aria-label={`Gerät ${row.inventoryNumber} bearbeiten`}
|
|
onClick={() => console.log('Bearbeiten', row.id)}
|
|
/>
|
|
|
|
<Button
|
|
variant="soft"
|
|
tone="rose"
|
|
size="md"
|
|
icon={<TrashIcon className="size-5" />}
|
|
aria-label={`Gerät ${row.inventoryNumber} löschen`}
|
|
onClick={() => console.log('Löschen', row.id)}
|
|
/>
|
|
</div>
|
|
|
|
{/* Mobile / kleine Screens: kompaktes Dropdown mit Ellipsis-Trigger */}
|
|
<div className="lg:hidden">
|
|
<Dropdown
|
|
triggerVariant="icon"
|
|
ariaLabel={`Aktionen für ${row.inventoryNumber}`}
|
|
sections={[
|
|
{
|
|
items: [
|
|
{
|
|
label: 'Details',
|
|
icon: <BookOpenIcon className="size-4" />,
|
|
onClick: () => console.log('Details', row.id),
|
|
},
|
|
{
|
|
label: 'Bearbeiten',
|
|
icon: <PencilIcon className="size-4" />,
|
|
onClick: () => console.log('Bearbeiten', row.id),
|
|
},
|
|
{
|
|
label: 'Löschen',
|
|
icon: <TrashIcon className="size-4" />,
|
|
tone: 'danger',
|
|
onClick: () => console.log('Löschen', row.id),
|
|
},
|
|
],
|
|
},
|
|
]}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
/>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|