Web: Unterkategorien umsortieren (uebergeordnete Kategorie aenderbar)

In der Kategorien-Verwaltung gibt es je Zeile eine Spalte "Uebergeordnet" mit
Auswahl - so laesst sich eine (Unter-)Kategorie unter eine andere haengen oder
auf die oberste Ebene holen. Zielauswahl auf denselben Typ beschraenkt und um die
eigenen Unterkategorien bereinigt; das Backend verhindert Ringe zusaetzlich.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-26 00:01:32 +02:00
parent 5edb830afd
commit 928342d352

View File

@@ -95,6 +95,19 @@ export default function Categories() {
const nameById = Object.fromEntries(categories.map((c) => [c.id, c.name]));
const parentOf = Object.fromEntries(categories.map((c) => [c.id, c.parent_id]));
const childCount = (id) => categories.filter((c) => c.parent_id === id).length;
// Eigene Unterkategorien (rekursiv) als Ziel beim Verschieben ausgeschlossen,
// sonst entstünde ein Ring.
function descendantIds(id) {
const result = new Set();
const stack = [id];
while (stack.length) {
const cur = stack.pop();
for (const kid of categories.filter((c) => c.parent_id === cur)) {
if (!result.has(kid.id)) { result.add(kid.id); stack.push(kid.id); }
}
}
return result;
}
function versteckt(id) {
let p = parentOf[id];
while (p != null) {
@@ -135,6 +148,7 @@ export default function Categories() {
<tr>
<th>Kategorie</th>
<th>Art</th>
<th>Übergeordnet</th>
<th className="num">Artikel</th>
<th></th>
</tr>
@@ -143,6 +157,12 @@ export default function Categories() {
{sichtbar.map((c) => {
const kinder = childCount(c.id);
const zu = collapsed.has(c.id);
// Mögliche neue Elternkategorien: gleicher Typ, nicht sie selbst
// und keine ihrer Unterkategorien.
const desc = descendantIds(c.id);
const parentOptions = tree.filter(
(o) => o.tracking === c.tracking && o.id !== c.id && !desc.has(o.id),
);
return (
<tr key={c.id} className={feldKat && feldKat.id === c.id ? "row-active" : ""}>
<td data-label="Kategorie">
@@ -164,9 +184,6 @@ export default function Categories() {
{zu && kinder > 0 && (
<span className="badge nowrap">{kinder} ausgeblendet</span>
)}
{c.depth > 0 && c.parent_id && nameById[c.parent_id] && (
<span className="badge nowrap">in {nameById[c.parent_id]}</span>
)}
</span>
</td>
<td data-label="Art">
@@ -180,6 +197,21 @@ export default function Categories() {
<span className="badge">{MODUS[c.tracking] || c.tracking}</span>
)}
</td>
<td data-label="Übergeordnet">
{isAdmin ? (
<select value={c.parent_id ?? ""} style={{ marginTop: 0, minWidth: 150 }}
onChange={(e) => patch(c, {
parent_id: e.target.value === "" ? null : Number(e.target.value),
})}>
<option value=""> oberste Ebene </option>
{parentOptions.map((o) => (
<option key={o.id} value={o.id}>{"— ".repeat(o.depth)}{o.name}</option>
))}
</select>
) : (
<span className="muted">{c.parent_id ? nameById[c.parent_id] : ""}</span>
)}
</td>
<td data-label="Artikel" className="num muted">{c.product_count}</td>
<td className="num">
<button className="btn-icon" title="Felder verwalten"
@@ -196,7 +228,7 @@ export default function Categories() {
);
})}
{categories.length === 0 && (
<tr><td colSpan={4} className="empty">Noch keine Kategorien.</td></tr>
<tr><td colSpan={5} className="empty">Noch keine Kategorien.</td></tr>
)}
</tbody>
</table>