Planstellen klonen — ausser den leitenden

Anforderung 11 aus dem Workshop. Migration 20260915160000.

In der Fertigung sind Planstellen reihenweise gleich: zwoelf
"Maschinenbediener:in" in derselben Abteilung auf derselben Kostenstelle.
Von Hand angelegt sind das zwoelf Gelegenheiten, die Taetigkeit
unterschiedlich zu schreiben — und ab der zweiten Schreibweise steht sie
zweimal im Katalog und jede Auswertung nach Taetigkeit ist falsch.

Der Klon nimmt Einheit, Taetigkeit (denselben Katalogeintrag) und die zum
Stichtag geltende Kontierung. Nicht mit kommt die Besetzung: eine
Planstelle ist ein Platz, keine Person, der Klon ist frei.

Leitungsplanstellen sind ausgenommen, und zwar mit einer eigenen Meldung.
Je Einheit gibt es genau eine, und ein Unique-Index sichert das ab — ohne
die Pruefung waere ein Klonversuch entweder "duplicate key value violates
unique constraint" oder, mit stillschweigend fallengelassenem is_chief,
eine Planstelle, die anders ist als ihre Vorlage, ohne dass es jemand
angefordert hat. In der Liste fehlt der Knopf dort; das ist Bequemlichkeit,
die Regel steht in der Funktion.
This commit is contained in:
2026-09-15 22:55:33 +02:00
parent cf0ea51f47
commit b9da3f3411
4 changed files with 204 additions and 12 deletions

View File

@@ -1,9 +1,9 @@
"use client";
import { Plus, Trash2 } from "lucide-react";
import { Copy, Plus, Trash2 } from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { deletePosition } from "@/actions/positions";
import { clonePosition, deletePosition } from "@/actions/positions";
import { Button } from "@/components/ui/Button";
import { useToast } from "@/components/ui/Toast";
import { fmtDate, todayIso } from "@/lib/format";
@@ -25,6 +25,7 @@ export function PositionsPageClient({ openPositions, units, kostenstellen }: Pos
const [createOpen, setCreateOpen] = useState(false);
const [editing, setEditing] = useState<OpenPositionWithDays | null>(null);
const [deletingId, setDeletingId] = useState<string | null>(null);
const [cloningId, setCloningId] = useState<string | null>(null);
// Getrennt, weil es zwei verschiedene Aussagen sind: „hier fehlt jemand"
// und „das entsteht erst". In einer Liste vermischt liest sich eine
@@ -32,6 +33,18 @@ export function PositionsPageClient({ openPositions, units, kostenstellen }: Pos
const offen = openPositions.filter((p) => !p.future);
const kuenftig = openPositions.filter((p) => p.future);
async function handleClone(p: OpenPositionWithDays) {
setCloningId(p.id);
const result = await clonePosition({ position_id: p.id });
setCloningId(null);
if (result.success) {
showToast(`Planstelle ${p.position_number} kopiert.`);
router.refresh();
} else {
showToast(result.error ?? "Fehler beim Klonen.", "error");
}
}
async function handleDelete(id: string) {
setDeletingId(id);
const result = await deletePosition(id);
@@ -77,15 +90,33 @@ export function PositionsPageClient({ openPositions, units, kostenstellen }: Pos
)}
</button>
<Button
variant="icon"
onClick={() => handleDelete(p.id)}
pending={deletingId === p.id}
aria-label={`Planstelle ${p.position_number} (${p.title}) entfernen`}
className="absolute right-2 top-2 hover:!text-danger-solid"
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
<span className="absolute right-2 top-2 flex items-center gap-0.5">
{/* Nicht bei einer Leitung: je Einheit gibt es genau eine, ein Klon
wäre also entweder ein Fehler aus dem Maschinenraum oder eine
Planstelle, die stillschweigend anders ist als ihre Vorlage. Die
SQL-Funktion weist es ohnehin ab — der fehlende Knopf erspart
den Weg dorthin. */}
{!p.is_chief && (
<Button
variant="icon"
onClick={() => handleClone(p)}
pending={cloningId === p.id}
aria-label={`Planstelle ${p.position_number} (${p.title}) klonen`}
title="Klonen — gleiche Einheit, Tätigkeit und Kostenstelle, neue Nummer"
>
<Copy className="h-3.5 w-3.5" />
</Button>
)}
<Button
variant="icon"
onClick={() => handleDelete(p.id)}
pending={deletingId === p.id}
aria-label={`Planstelle ${p.position_number} (${p.title}) entfernen`}
className="hover:!text-danger-solid"
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</span>
</div>
);
}