81 lines
3.9 KiB
TypeScript
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";
|
|
}
|