Registo de chaves

This commit is contained in:
Luis Lourenço 2026-08-24 16:35:20 +01:00
parent ef83aa153f
commit 94961cf98a
4 changed files with 25 additions and 9 deletions

View file

@ -163,10 +163,10 @@ One row per **physical piece of metal**, not per door.
| Column | Required | What it is |
|---|---|---|
| `tag` | **yes** | The number stamped on that copy, e.g. `12-A`. Must be unique — this is what makes the register able to say *which* copy is out. |
| `door` | **yes** | The door **name**, matched against the doors you already imported. If no door matches, one is created. |
| `door` | **yes** | The door **number** or its **name** — whichever your list uses. Matched against doors already imported, number first. A number that matches nothing is refused (not turned into a door called `12`); a name that matches nothing creates the door. |
| `building` | **yes** | Used together with `door` to find the right door, since two buildings may both have a `Cozinha`. |
| `lock_group` | no | **Only used if this row has to create a new door.** For doors that already exist it is ignored — edit the door instead. |
| `cabinet` | no | The cabinet `id`, e.g. `SAC`. Created if it does not exist, but then it gets no proper name, so import cabinets first. |
| `cabinet` | no | The cabinet **code**, e.g. `SAC`. Case does not matter. A code that does not exist is **refused** and the row is listed as skipped — import cabinets first (step 1). |
| `hook` | no | Position in the cabinet, e.g. `SAC·R2·05`. Sets the order keys appear on the board and on the audit sheet. Blank falls back to tag order. |
| `master` | no | yes/no. Marks it as a master key on the board. |
| `status` | no | `in` (default), `out`, `reserve`, `lost`, `retired`. Use `reserve` for copies in the off-site box. Do **not** import `out` — issue the key in the app instead, so there is a handover record. |

View file

@ -322,7 +322,7 @@ function sheetKind(id) {
}
/* --------------------------------------------------------- import / backup */
let impKind = "items";
let impKind = "cabinets"; // the first step, not the last
const IMP_SPEC = {
items: { url: "/api/admin/import", req: "tag, door, building",
@ -344,9 +344,10 @@ function pageData() {
$body.innerHTML = `
<div class="card"><h3>${T.impTitle}</h3>
<span class="lab">${T.impWhat}</span>
<p style="color:var(--slate);font-size:12.5px;margin:0 0 8px">${T.impOrder}</p>
<div class="adm-nav" style="padding:0 0 12px">
${[["items", T.impItems], ["doors", T.impDoors], ["people", T.impPeople], ["cabinets", T.impCabinets]]
.map(([k, l]) => `<button data-imp="${k}" aria-pressed="${impKind === k}">${l}</button>`).join("")}
${[["cabinets", T.impCabinets], ["doors", T.impDoors], ["people", T.impPeople], ["items", T.impItems]]
.map(([k, l], n) => `<button data-imp="${k}" aria-pressed="${impKind === k}">${n + 1}. ${l}</button>`).join("")}
</div>
<p style="color:var(--slate);font-size:13px">
<b>${esc(spec.req)}</b>${spec.opt ? " · " + esc(spec.opt) : ""}</p>

View file

@ -232,6 +232,7 @@ var I18N = {
holdsWarn: (n, tags) => `Tem ${n} ${n === 1 ? "item" : "itens"}: ${tags}. Receba-os de volta antes de desativar.`,
impWhat: "O que quer importar?",
impOrder: "Importe por esta ordem — as chaves apontam para portas e armários, que têm de existir primeiro.",
impItems: "Chaves e etiquetas", impDoors: "Portas", impPeople: "Pessoas", impCabinets: "Armários",
impResult2: (c, u) => `Criados <b>${c}</b>, atualizados <b>${u}</b>.`,
impTitle: "Importar de uma folha de cálculo",
@ -425,6 +426,7 @@ var I18N = {
offSiteNote: "Keys returned to an off-site store go back to <em>reserve</em> rather than <em>in cabinet</em>, so they stay out of everyday circulation.",
holdsWarn: (n, tags) => `Currently holds ${n} item${n === 1 ? "" : "s"}: ${tags}. Take those back before deactivating.`,
impWhat: "What do you want to import?",
impOrder: "Import in this order — keys point at doors and cabinets, which must exist first.",
impItems: "Keys & tags", impDoors: "Doors", impPeople: "People", impCabinets: "Cabinets",
impResult2: (c, u) => `Created <b>${c}</b>, updated <b>${u}</b>.`,
impTitle: "Import from a spreadsheet",

View file

@ -666,16 +666,29 @@ app.post(A + "/import", (req, res) => {
out.skipped.push(tag + " (already exists)"); continue;
}
const group = col("lock_group") >= 0 ? (r[col("lock_group")] || "").trim() : "";
let door = findDoor.get(doorName, building);
/* The `door` column takes either the door number or its name. Numbering
is how a paper key list works, so `12` is usually what people have to
hand; the name still works for lists that have no numbers. */
let door = db.prepare("SELECT id FROM doors WHERE num=? AND building=?").get(doorName, building)
|| findDoor.get(doorName, building);
if (!door) {
/* A bare number that matches no door is a mistake, not a new door
called "12" — refuse it rather than quietly creating junk. */
if (/^\d+$/.test(doorName)) {
out.skipped.push(`${tag} (unknown door: ${doorName})`);
continue;
}
const id = db.prepare("INSERT INTO doors (num,building,name,lock_group) VALUES (?,?,?,?)")
.run(nextNum(), building, doorName, group).lastInsertRowid;
door = { id }; out.doors++;
}
let cabId = col("cabinet") >= 0 ? (r[col("cabinet")] || "").trim() : "";
/* An unknown cabinet code is refused rather than created. A typo would
otherwise produce a nameless cabinet and quietly scatter keys into it —
and cabinets have their own importer, one step earlier. */
let cabId = col("cabinet") >= 0 ? (r[col("cabinet")] || "").trim().toUpperCase() : "";
if (cabId && !db.prepare("SELECT 1 FROM cabinets WHERE id=?").get(cabId)) {
db.prepare("INSERT INTO cabinets (id,name,full_name) VALUES (?,?,?)").run(cabId, cabId, cabId);
out.cabinets++;
out.skipped.push(`${tag} (unknown cabinet: ${cabId})`);
continue;
}
/* An unknown type is refused rather than created. A malformed row or a
typo would otherwise litter the type list with junk, and types are now