diff --git a/src/actions/cableActions.ts b/src/actions/cableActions.ts
index 83d6eec..0b83698 100644
--- a/src/actions/cableActions.ts
+++ b/src/actions/cableActions.ts
@@ -75,6 +75,77 @@ export async function createCable(
}
}
+/**
+ * Updates an existing Cable. Expects a FormData containing `id` and the
+ * same fields as createCable. Returns success or error for client to handle.
+ */
+export async function updateCable(
+ formData: FormData
+): Promise<{ success: true } | { success: false; error: string }> {
+ const id = Number(formData.get("id"));
+ const identifier = (formData.get("identifier") as string)?.trim();
+ const description = (formData.get("description") as string)?.trim();
+ const notes = (formData.get("notes") as string)?.trim();
+ const startLocationId = Number(formData.get("startLocationId"));
+ const endLocationId = Number(formData.get("endLocationId"));
+ const intermediateLocationIds = formData
+ .getAll("intermediateLocationIds")
+ .map((value) => Number(value))
+ .filter((id) => Boolean(id));
+
+ if (!id || isNaN(id)) {
+ return { success: false, error: "Ungültige Kabel-ID." };
+ }
+ if (!identifier) {
+ return { success: false, error: "Bezeichnung ist erforderlich." };
+ }
+ if (!startLocationId || !endLocationId) {
+ return { success: false, error: "Start- und End-Ort müssen ausgewählt werden." };
+ }
+ if (intermediateLocationIds.length > 0) {
+ if (intermediateLocationIds.some((loc) => loc === startLocationId || loc === endLocationId)) {
+ return { success: false, error: "Zwischenstationen dürfen nicht der Start- oder End-Ort sein." };
+ }
+ const hasDuplicate = new Set(intermediateLocationIds).size !== intermediateLocationIds.length;
+ if (hasDuplicate) {
+ return { success: false, error: "Zwischenstationen dürfen nicht doppelt vorkommen." };
+ }
+ }
+
+ try {
+ // Update main cable fields
+ await prisma.cable.update({
+ where: { id },
+ data: {
+ identifier,
+ description: description || null,
+ notes: notes || null,
+ startLocationId,
+ endLocationId,
+ },
+ });
+
+ // Replace intermediate stops: remove existing and create new ordered stops
+ await prisma.cableStop.deleteMany({ where: { cableId: id } });
+ if (intermediateLocationIds.length) {
+ await prisma.cableStop.createMany({
+ data: intermediateLocationIds.map((locationId, index) => ({
+ cableId: id,
+ locationId,
+ order: index,
+ })),
+ });
+ }
+
+ revalidatePath("/cables");
+ revalidatePath(`/cables/${id}`);
+ return { success: true };
+ } catch (e) {
+ console.error("updateCable failed:", e);
+ return { success: false, error: "Fehler beim Aktualisieren des Kabels." };
+ }
+}
+
/**
* Deletes a Cable and all its Cores (via Prisma's onDelete: Cascade).
* Navigation back to /cables is handled by the client component.
diff --git a/src/actions/coreActions.ts b/src/actions/coreActions.ts
index 0ec5f54..ac26d0c 100644
--- a/src/actions/coreActions.ts
+++ b/src/actions/coreActions.ts
@@ -49,4 +49,37 @@ export async function createCore(
export async function deleteCore(id: number, cableId: number): Promise {
await prisma.core.delete({ where: { id } });
revalidatePath(`/cables/${cableId}`);
+}
+
+/**
+ * Updates an existing Core. Designed for use with useActionState.
+ */
+export async function updateCore(
+ prevState: FormState,
+ formData: FormData
+): Promise {
+ const id = Number(formData.get("id"));
+ const cableId = Number(formData.get("cableId"));
+ const color = (formData.get("color") as string)?.trim();
+ const notes = (formData.get("notes") as string)?.trim();
+
+ if (!id || isNaN(id)) {
+ return { error: "Ungültige Ader-ID." };
+ }
+ if (!color) {
+ return { error: "Farbe ist erforderlich." };
+ }
+
+ try {
+ await prisma.core.update({
+ where: { id },
+ data: { color, notes: notes || null },
+ });
+
+ if (cableId && !isNaN(cableId)) revalidatePath(`/cables/${cableId}`);
+ return { success: true };
+ } catch (e) {
+ console.error("updateCore failed:", e);
+ return { error: "Fehler beim Aktualisieren der Ader." };
+ }
}
\ No newline at end of file
diff --git a/src/actions/locationActions.ts b/src/actions/locationActions.ts
index dce5037..dd16e57 100644
--- a/src/actions/locationActions.ts
+++ b/src/actions/locationActions.ts
@@ -41,6 +41,42 @@ export async function createLocation(
}
}
+/**
+ * Updates an existing Location. Designed for use with useActionState.
+ */
+export async function updateLocation(
+ prevState: FormState,
+ formData: FormData
+): Promise {
+ const id = Number(formData.get("id"));
+ const name = (formData.get("name") as string)?.trim();
+ const description = (formData.get("description") as string)?.trim();
+
+ if (!id || isNaN(id)) {
+ return { error: "Ungültige Ort-ID." };
+ }
+ if (!name) {
+ return { error: "Name ist erforderlich." };
+ }
+
+ try {
+ await prisma.location.update({
+ where: { id },
+ data: {
+ name,
+ description: description || null,
+ },
+ });
+
+ revalidatePath("/locations");
+ revalidatePath("/cables");
+ return { success: true };
+ } catch (e) {
+ console.error("updateLocation failed:", e);
+ return { error: "Fehler beim Aktualisieren des Ortes." };
+ }
+}
+
/**
* Deletes a Location by ID. The UI prevents calling this if the location
* is still referenced by cables (enforced via disabled state on the button).
diff --git a/src/app/cables/[id]/cores/[coreId]/edit/page.tsx b/src/app/cables/[id]/cores/[coreId]/edit/page.tsx
new file mode 100644
index 0000000..f041c6d
--- /dev/null
+++ b/src/app/cables/[id]/cores/[coreId]/edit/page.tsx
@@ -0,0 +1,29 @@
+export const dynamic = "force-dynamic";
+
+import { notFound } from "next/navigation";
+import { prisma } from "@/lib/prisma";
+import EditCoreForm from "@/components/cores/EditCoreForm";
+
+interface EditCorePageProps {
+ params: Promise<{ id: string; coreId: string }>;
+}
+
+export default async function EditCorePage({ params }: EditCorePageProps) {
+ const { id, coreId } = await params;
+ const cableId = parseInt(id, 10);
+ const coreIdNum = parseInt(coreId, 10);
+ if (isNaN(cableId) || isNaN(coreIdNum)) notFound();
+
+ const core = await prisma.core.findUnique({ where: { id: coreIdNum } });
+ if (!core || core.cableId !== cableId) notFound();
+
+ return (
+
+
+
Ader bearbeiten
+
Bearbeiten Sie die Farbe oder Notizen dieser Ader.
+
+
+
+ );
+}
diff --git a/src/app/cables/[id]/edit/page.tsx b/src/app/cables/[id]/edit/page.tsx
new file mode 100644
index 0000000..6d11ffa
--- /dev/null
+++ b/src/app/cables/[id]/edit/page.tsx
@@ -0,0 +1,34 @@
+export const dynamic = "force-dynamic";
+
+import { notFound } from "next/navigation";
+import { prisma } from "@/lib/prisma";
+import EditCableForm from "@/components/cables/EditCableForm";
+import type { Location } from "@/generated/prisma/client";
+
+interface EditCablePageProps {
+ params: Promise<{ id: string }>;
+}
+
+export default async function EditCablePage({ params }: EditCablePageProps) {
+ const { id } = await params;
+ const cableId = parseInt(id, 10);
+ if (isNaN(cableId)) notFound();
+
+ const cable = await prisma.cable.findUnique({
+ where: { id: cableId },
+ include: { intermediateStops: { orderBy: { order: "asc" } } },
+ });
+ if (!cable) notFound();
+
+ const locations = (await prisma.location.findMany({ orderBy: { name: "asc" } })) as Location[];
+
+ return (
+
+
+
Kabel bearbeiten
+
Bearbeiten Sie die Kabel-Metadaten und Zwischenstationen.
+
+
+
+ );
+}
diff --git a/src/app/cables/[id]/page.tsx b/src/app/cables/[id]/page.tsx
index b6d8351..d87472a 100644
--- a/src/app/cables/[id]/page.tsx
+++ b/src/app/cables/[id]/page.tsx
@@ -80,7 +80,10 @@ export default async function CableDetailPage({
})}
-
+
+ Bearbeiten
+
+
{/* Route visualization */}
diff --git a/src/app/locations/[id]/edit/page.tsx b/src/app/locations/[id]/edit/page.tsx
new file mode 100644
index 0000000..02b6d6a
--- /dev/null
+++ b/src/app/locations/[id]/edit/page.tsx
@@ -0,0 +1,28 @@
+export const dynamic = "force-dynamic";
+
+import { notFound } from "next/navigation";
+import { prisma } from "@/lib/prisma";
+import EditLocationForm from "@/components/locations/EditLocationForm";
+
+interface EditLocationPageProps {
+ params: Promise<{ id: string }>;
+}
+
+export default async function EditLocationPage({ params }: EditLocationPageProps) {
+ const { id } = await params;
+ const locationId = parseInt(id, 10);
+ if (isNaN(locationId)) notFound();
+
+ const location = await prisma.location.findUnique({ where: { id: locationId } });
+ if (!location) notFound();
+
+ return (
+
+
+
Ort bearbeiten
+
Bearbeiten Sie die Details dieses Ortes.
+
+
+
+ );
+}
diff --git a/src/components/cables/EditCableForm.tsx b/src/components/cables/EditCableForm.tsx
new file mode 100644
index 0000000..a223c9e
--- /dev/null
+++ b/src/components/cables/EditCableForm.tsx
@@ -0,0 +1,123 @@
+"use client";
+
+import { useState, useTransition } from "react";
+import { useRouter } from "next/navigation";
+import type { Location } from "@/generated/prisma/client";
+import { updateCable } from "@/actions/cableActions";
+
+interface EditCableFormProps {
+ cable: {
+ id: number;
+ identifier: string;
+ description?: string | null;
+ notes?: string | null;
+ startLocationId: number;
+ endLocationId: number;
+ intermediateStops: { locationId: number }[];
+ };
+ locations: Location[];
+}
+
+export default function EditCableForm({ cable, locations }: EditCableFormProps) {
+ const [isOpen] = useState(true);
+ const [error, setError] = useState(null);
+ const [stations, setStations] = useState(
+ cable.intermediateStops.map((s) => s.locationId)
+ );
+ const [isPending, startTransition] = useTransition();
+ const router = useRouter();
+
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault();
+ const formData = new FormData(event.currentTarget);
+ setError(null);
+
+ startTransition(async () => {
+ const result = await updateCable(formData);
+ if (!result.success) {
+ setError(result.error);
+ } else {
+ router.push(`/cables/${cable.id}`);
+ }
+ });
+ }
+
+ if (!isOpen) return null;
+
+ return (
+
+
+
Kabel bearbeiten
+
+
+
+
+ );
+}
diff --git a/src/components/cores/CoresList.tsx b/src/components/cores/CoresList.tsx
index e71a7e1..bc4c135 100644
--- a/src/components/cores/CoresList.tsx
+++ b/src/components/cores/CoresList.tsx
@@ -1,5 +1,6 @@
import type { Core } from "@/generated/prisma/client";
import DeleteCoreButton from "./DeleteCoreButton";
+import Link from "next/link";
import MarkdownContent from "@/components/ui/MarkdownContent";
interface CoresListProps {
@@ -68,8 +69,11 @@ export default function CoresList({ cores, cableId }: CoresListProps) {
)}
- {/* Delete button */}
-
+ {/* Actions: edit + delete */}
+
+ Bearbeiten
+
+
))}
diff --git a/src/components/cores/EditCoreForm.tsx b/src/components/cores/EditCoreForm.tsx
new file mode 100644
index 0000000..eb58da8
--- /dev/null
+++ b/src/components/cores/EditCoreForm.tsx
@@ -0,0 +1,51 @@
+"use client";
+
+import { useActionState, useEffect, useRef } from "react";
+import { useRouter } from "next/navigation";
+import { updateCore } from "@/actions/coreActions";
+import type { FormState } from "@/actions/coreActions";
+import type { Core } from "@/generated/prisma/client";
+
+interface EditCoreFormProps {
+ core: Core;
+}
+
+export default function EditCoreForm({ core }: EditCoreFormProps) {
+ const [state, formAction] = useActionState(updateCore, {} as FormState);
+ const formRef = useRef(null);
+ const router = useRouter();
+
+ useEffect(() => {
+ if (state.success) {
+ // Navigate back to the cable detail page
+ router.push(`/cables/${core.cableId}`);
+ }
+ }, [state.success, router, core.cableId]);
+
+ return (
+
+ );
+}
diff --git a/src/components/locations/EditLocationForm.tsx b/src/components/locations/EditLocationForm.tsx
new file mode 100644
index 0000000..4ef0ada
--- /dev/null
+++ b/src/components/locations/EditLocationForm.tsx
@@ -0,0 +1,74 @@
+"use client";
+
+import { useActionState, useEffect, useRef } from "react";
+import { useRouter } from "next/navigation";
+import { updateLocation } from "@/actions/locationActions";
+import type { FormState } from "@/actions/locationActions";
+import type { Location } from "@/generated/prisma/client";
+
+interface EditLocationFormProps {
+ location: Location;
+}
+
+export default function EditLocationForm({ location }: EditLocationFormProps) {
+ const [state, formAction] = useActionState(updateLocation, {} as FormState);
+ const formRef = useRef(null);
+ const router = useRouter();
+
+ useEffect(() => {
+ if (state.success) {
+ // Navigate back to the locations list after successful update
+ router.push("/locations");
+ }
+ }, [state.success, router]);
+
+ return (
+
+
Ort bearbeiten
+
+
+ );
+}
diff --git a/src/components/locations/LocationsTabe.tsx b/src/components/locations/LocationsTabe.tsx
index de04b13..f02b7f9 100644
--- a/src/components/locations/LocationsTabe.tsx
+++ b/src/components/locations/LocationsTabe.tsx
@@ -1,5 +1,6 @@
import type { LocationWithCounts } from "@/lib/types";
import DeleteLocationButton from "./DeleteLocationButton";
+import Link from "next/link";
interface LocationsTableProps {
locations: LocationWithCounts[];
@@ -73,10 +74,13 @@ export default function LocationsTable({ locations }: LocationsTableProps) {
-
+
+ Bearbeiten
+
+
|
);