diff --git a/apps/web/src/RolesManager.tsx b/apps/web/src/RolesManager.tsx
index 12b76e6..63fda3f 100644
--- a/apps/web/src/RolesManager.tsx
+++ b/apps/web/src/RolesManager.tsx
@@ -12,6 +12,7 @@ import {
type Permission,
type SessionUser,
} from "./api.js";
+import { Modal } from "./ui/Modal.js";
// Role management (admin). Compose a role from the permission grid (a checkbox
// matrix of resource × action) and name it; users are then assigned a role. The
@@ -56,8 +57,7 @@ export function RolesManager({ user }: { user: SessionUser | null }) {
);
diff --git a/apps/web/src/SetupWizard.tsx b/apps/web/src/SetupWizard.tsx
index b4d8513..310eb48 100644
--- a/apps/web/src/SetupWizard.tsx
+++ b/apps/web/src/SetupWizard.tsx
@@ -1,4 +1,5 @@
import { useState, useEffect, useCallback } from "react";
+import { useTranslation } from "react-i18next";
import {
assignDevice,
editDevice,
@@ -19,6 +20,7 @@ import {
type RelaySpec,
type TestResult,
} from "./api.js";
+import { Modal } from "./ui/Modal.js";
// First-run setup wizard. The pool-of-spaces model: a parking lot is one pool with
// a flexible set of entry/exit points — NO lane. The admin adds CONTROLLERS (each
@@ -27,25 +29,30 @@ import {
// Direction is a property of the relay, inherited by bound devices. The data model
// is multi-instance — one `devices` row per instance. See entry-exit-points.md.
-const CONTROLLER: { key: DeviceCategory; title: string; noun: string } = {
+// Categories carry i18n KEYS (resolved at render via t()), not literal copy.
+// `titleKey` is the section heading; `nounKey` resolves to the singular noun used in
+// the add/edit buttons, modal titles and confirm prompts.
+const CONTROLLER: { key: DeviceCategory; titleKey: string; nounKey: string } = {
key: "access",
- title: "Controllers (barriers + entry button)",
- noun: "controller",
+ titleKey: "setup.catControllers",
+ nounKey: "setup.nounController",
};
// Categories that BIND to a controller relay (direction inherited from the relay).
-const BOUND: { key: DeviceCategory; title: string; noun: string }[] = [
- { key: "reader", title: "Readers (QR / RFID)", noun: "reader" },
- { key: "camera", title: "Cameras (snapshot + plate)", noun: "camera" },
- { key: "printer", title: "Printers (tickets / vouchers)", noun: "printer" },
+const BOUND: { key: DeviceCategory; titleKey: string; nounKey: string }[] = [
+ { key: "reader", titleKey: "setup.catReaders", nounKey: "setup.nounReader" },
+ { key: "camera", titleKey: "setup.catCameras", nounKey: "setup.nounCamera" },
+ { key: "printer", titleKey: "setup.catPrinters", nounKey: "setup.nounPrinter" },
];
-const DIRECTION_LABELS: Record = {
- entry: "Entry",
- exit: "Exit",
- both: "Both (entry + exit)",
+// Translated direction label (relay direction / inherited binding).
+const DIRECTION_KEYS: Record = {
+ entry: "setup.dirEntry",
+ exit: "setup.dirExit",
+ both: "setup.dirBoth",
};
export function SetupWizard() {
+ const { t } = useTranslation();
const [catalog, setCatalog] = useState(null);
const [assignments, setAssignments] = useState(null);
const [error, setError] = useState(null);
@@ -61,25 +68,21 @@ export function SetupWizard() {
reloadState();
}, [reloadState]);
- if (error) return
Failed to load setup: {error}
;
- if (!catalog || !assignments) return
Loading device catalog…
;
+ if (error) return
{t("setup.failedToLoad", { error })}
;
+ if (!catalog || !assignments) return
{t("setup.loadingCatalog")}
;
// Controllers are needed before binding readers/cameras (they pick a controller relay).
const controllers = assignments.filter((a) => a.category === "access");
return (
-
-
First-run setup
-
- Add your barrier controllers first — set which relay is entry/exit and which
- terminal the entry button is wired to. Then add readers, cameras and printers
- and point each at the barrier it serves.
-
+
+
{t("setup.title")}
+
{t("setup.intro")}
- {BOUND.map(({ key, title, noun }) => (
+ {BOUND.map(({ key, titleKey, nounKey }) => (
Promise | void;
}) {
- const [adding, setAdding] = useState(false);
- const [editingId, setEditingId] = useState(null);
+ const { t } = useTranslation();
+ // The form is popped out in a Modal. `formFor` selects what it edits:
+ // - "new" → the add form
+ // - an Assignment → edit that device in place
+ // - null → closed.
+ const [formFor, setFormFor] = useState(null);
const [warnings, setWarnings] = useState([]);
- const editing = editingId ? assignments.find((a) => a.id === editingId) : undefined;
- // Show the add form for an empty category or an explicit "+ Add", but not while
- // editing an existing row (that row renders its own inline form).
- const showForm = !editing && (adding || assignments.length === 0);
// Binding categories need a controller to point at first.
const isBound = category !== "access";
const blockedNoController = isBound && controllers.length === 0;
+ const editing = formFor && formFor !== "new" ? formFor : undefined;
return (
-