Files
LumaOps-Public/lumaops/frontend/src/pages/DeviceGroupPage.tsx
T
2026-09-30 00:26:45 +00:00

81 lines
3.9 KiB
TypeScript

import { ArrowLeft, Layers3, MemoryStick, Wifi } from "lucide-react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Link, useParams } from "react-router-dom";
import { api } from "../api/client";
import type { DeviceGroup, DeviceState } from "../api/types";
import { DeviceCard } from "../components/DeviceCard";
import { RgbControlPanel } from "../components/RgbControlPanel";
import { useToast } from "../components/Toast";
import { Badge, ErrorPanel, LoadingGrid, PageHeader } from "../components/ui";
export function DeviceGroupPage() {
const { groupId = "" } = useParams();
const queryClient = useQueryClient();
const { notify } = useToast();
const query = useQuery({
queryKey: ["device-group", groupId],
queryFn: () => api<DeviceGroup>(`/api/v1/device-groups/${groupId}`),
});
const command = useMutation({
mutationFn: (state: DeviceState) => api<{ status: string; results: Array<{ status: string }> }>(`/api/v1/device-groups/${groupId}/state`, {
method: "POST",
body: JSON.stringify({ state }),
}),
onSuccess: (result) => {
const failed = result.results.filter((item) => item.status === "failed").length;
notify(failed ? `${failed} RAM-modules konden niet worden bijgewerkt.` : "RAM-groep bijgewerkt.", failed ? "danger" : "success");
void queryClient.invalidateQueries({ queryKey: ["device-group", groupId] });
void queryClient.invalidateQueries({ queryKey: ["device-groups"] });
void queryClient.invalidateQueries({ queryKey: ["devices"] });
void queryClient.invalidateQueries({ queryKey: ["system"] });
},
onError: (error) => notify(error instanceof Error ? error.message : "RAM-groepsopdracht mislukt.", "danger"),
});
if (query.isLoading) return <LoadingGrid count={3} />;
if (query.error || !query.data) return <ErrorPanel error={query.error} retry={() => void query.refetch()} />;
const group = query.data;
const disabled = group.online_count === 0 || group.devices.every((device) => device.read_only || device.blocked);
return <>
<Link className="back-link" to="/devices"><ArrowLeft size={16} /> Terug naar apparaten</Link>
<PageHeader
eyebrow="Gegroepeerd apparaat"
title={group.name}
description={`${group.vendor ?? "RAM"} · ${group.module_count} fysieke modules · ${group.led_count} leds`}
/>
<div className="detail-status">
<Badge tone={group.online ? "success" : "warning"}><Wifi size={13} /> {group.online_count}/{group.module_count} online</Badge>
<Badge tone="accent"><Layers3 size={13} /> Als groep bedienbaar</Badge>
{group.mixed ? <Badge tone="warning">Gemengde toestand</Badge> : null}
</div>
<div className="detail-grid device-group-overview">
<RgbControlPanel
targetKey={group.id}
capabilities={group.capabilities}
state={group.state}
modes={group.modes}
pending={command.isPending}
disabled={disabled}
onApply={(state) => command.mutate(state)}
/>
<div className="group-summary">
<MemoryStick size={34} />
<strong>{group.module_count} modules</strong>
<span>Groepsopdrachten worden afzonderlijk bevestigd en blijvend opgeslagen per fysieke module.</span>
</div>
</div>
<section className="section-block">
<div className="section-heading"><div><h2>Individuele RAM-modules</h2><p>Open een module om kleur, effect en overige mogelijkheden uitsluitend daarop toe te passen.</p></div></div>
<div className="grid grid--devices">
{group.devices.map((device, index) => <DeviceCard device={device} moduleLabel={`RAM-module ${index + 1} · ${moduleAddress(device.location)}`} key={device.id} />)}
</div>
</section>
</>;
}
function moduleAddress(location: string | null) {
const address = location?.match(/address (0x[0-9a-f]+)/i)?.[1];
return address?.toUpperCase() ?? "onbekend adres";
}