Merge lane/stammdaten in feature/craftvia-mvp

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 12:38:34 +02:00
co-authored by Claude Opus 5
64 changed files with 7249 additions and 73 deletions
+22
View File
@@ -0,0 +1,22 @@
import { assertSameOrigin, requireApiContext } from "@/server/api/context";
import { json, readJson, withApi } from "@/server/api/respond";
import { getCustomer, updateCustomer } from "@/server/services/customers/customers";
import type { CustomerPatchInput } from "@/server/services/customers/schemas";
type Ctx = { params: Promise<{ id: string }> };
/** GET /api/v1/customers/:id — customer incl. contacts (scope applies, otherwise 404). */
export const GET = withApi(async (_req: Request, { params }: Ctx) => {
const ctx = await requireApiContext("customers", "customer:read");
const { id } = await params;
return json({ data: await getCustomer(ctx, id) });
});
/** PATCH /api/v1/customers/:id — absent fields unchanged, null clears. */
export const PATCH = withApi(async (req: Request, { params }: Ctx) => {
assertSameOrigin(req);
const ctx = await requireApiContext("customers", "customer:write");
const { id } = await params;
const body = (await readJson(req)) as CustomerPatchInput | null;
return json({ data: await updateCustomer(ctx, id, body ?? {}) });
});
+31
View File
@@ -0,0 +1,31 @@
import { z } from "zod";
import { assertSameOrigin, requireApiContext } from "@/server/api/context";
import { json, paginated, parsePagination, readJson, withApi } from "@/server/api/respond";
import { createCustomer, CUSTOMER_LIST_STATUSES, listCustomers } from "@/server/services/customers/customers";
import type { CustomerCreateInput } from "@/server/services/customers/schemas";
const statusParam = z.enum([...CUSTOMER_LIST_STATUSES, "all"]).optional();
/** GET /api/v1/customers?q&status&page&pageSize */
export const GET = withApi(async (req: Request) => {
const ctx = await requireApiContext("customers", "customer:read");
const url = new URL(req.url);
const { page, pageSize } = parsePagination(url);
const status = statusParam.parse(url.searchParams.get("status") ?? undefined);
const result = await listCustomers(ctx, { q: url.searchParams.get("q") ?? undefined, status, page, pageSize });
return paginated(result.items, result.total, result.page, result.pageSize);
});
/**
* POST /api/v1/customers — body: customer fields + optional `acknowledgeDuplicates: true`.
* Possible duplicates without acknowledgement → 409 `{ error: { code: "conflict", details: { reason: "possible_duplicates", candidates } } }`.
*/
export const POST = withApi(async (req: Request) => {
assertSameOrigin(req);
const ctx = await requireApiContext("customers", "customer:write");
const body = (await readJson(req)) as Record<string, unknown> | null;
const customer = await createCustomer(ctx, (body ?? {}) as CustomerCreateInput, {
acknowledgeDuplicates: body?.acknowledgeDuplicates === true,
});
return json({ data: customer }, { status: 201 });
});
@@ -0,0 +1,21 @@
import { requireApiContext } from "@/server/api/context";
import { json, parsePagination, withApi } from "@/server/api/respond";
import { getSiteHistory } from "@/server/services/sites/history";
/**
* GET /api/v1/sites/:id/history?onlyApproved=true&page&pageSize
* Field roles always receive released deployments only (see getSiteHistory).
*/
export const GET = withApi(async (req: Request, { params }: { params: Promise<{ id: string }> }) => {
const ctx = await requireApiContext("sites", "site:read");
const { id } = await params;
const url = new URL(req.url);
const { page, pageSize } = parsePagination(url, { pageSize: 50 });
const flag = url.searchParams.get("onlyApproved");
const result = await getSiteHistory(ctx, id, { onlyApproved: flag === "true" || flag === "1", page, pageSize });
return json({
data: result.items,
pagination: { page: result.page, pageSize: result.pageSize, total: result.total },
meta: { onlyApproved: result.onlyApproved },
});
});
+29
View File
@@ -0,0 +1,29 @@
import { z } from "zod";
import { assertSameOrigin, requireApiContext } from "@/server/api/context";
import { json, paginated, parsePagination, readJson, withApi } from "@/server/api/respond";
import { createSite, listSites, SITE_STATUSES, type SiteCreateInput } from "@/server/services/sites/sites";
const statusParam = z.enum([...SITE_STATUSES, "all"]).optional();
/** GET /api/v1/sites?q&customerId&status&page&pageSize */
export const GET = withApi(async (req: Request) => {
const ctx = await requireApiContext("sites", "site:read");
const url = new URL(req.url);
const { page, pageSize } = parsePagination(url);
const result = await listSites(ctx, {
q: url.searchParams.get("q") ?? undefined,
customerId: url.searchParams.get("customerId") ?? undefined,
status: statusParam.parse(url.searchParams.get("status") ?? undefined),
page,
pageSize,
});
return paginated(result.items, result.total, result.page, result.pageSize);
});
/** POST /api/v1/sites */
export const POST = withApi(async (req: Request) => {
assertSameOrigin(req);
const ctx = await requireApiContext("sites", "site:write");
const body = (await readJson(req)) as SiteCreateInput | null;
return json({ data: await createSite(ctx, body ?? ({} as SiteCreateInput)) }, { status: 201 });
});