mirror of
https://github.com/Since-3/since3-portal.git
synced 2026-10-12 20:51:17 +00:00
163 lines
5.6 KiB
TypeScript
163 lines
5.6 KiB
TypeScript
"use server";
|
|
|
|
import * as z from "zod";
|
|
import { revalidatePath } from "next/cache";
|
|
import { createClient } from "@/lib/supabase/server";
|
|
import { verifySession } from "@/lib/supabase/dal";
|
|
import { getClientId } from "@/modules/shared/queries/profile";
|
|
import { NoteSchema, ProductSchema, TaskSchema } from "@/modules/shared/schemas/info";
|
|
|
|
export type InfoActionState = { error: string } | { success: true } | undefined;
|
|
|
|
const IdSchema = z.string().uuid();
|
|
|
|
type Table = "client_products" | "product_tasks" | "internal_notes";
|
|
|
|
const DELETE_TABLES: Record<string, Table | undefined> = {
|
|
product: "client_products",
|
|
task: "product_tasks",
|
|
note: "internal_notes",
|
|
};
|
|
|
|
function firstError(error: z.ZodError) {
|
|
return error.issues[0]?.message ?? "Bitte alle Pflichtfelder ausfüllen.";
|
|
}
|
|
|
|
function revalidateInfo(profileId: string) {
|
|
revalidatePath("/client/dashboard");
|
|
revalidatePath(`/admin/clients/${profileId}`);
|
|
}
|
|
|
|
// Admin-Check + Client auflösen. Alle Schreibzugriffe laufen danach zusätzlich über RLS (admin_all).
|
|
async function resolveAdminTarget(profileId: string): Promise<{ error: string } | { clientId: string }> {
|
|
const session = await verifySession();
|
|
if (!session.isAdmin) return { error: "Keine Berechtigung." };
|
|
if (!IdSchema.safeParse(profileId).success) return { error: "Ungültiger Client." };
|
|
|
|
const clientId = await getClientId(profileId);
|
|
if (!clientId) return { error: "Client hat noch keine Stammdaten hinterlegt." };
|
|
|
|
return { clientId };
|
|
}
|
|
|
|
async function upsertRow(
|
|
table: Table,
|
|
clientId: string,
|
|
id: string | null,
|
|
values: Record<string, unknown>
|
|
): Promise<boolean> {
|
|
const supabase = await createClient();
|
|
|
|
if (!id) {
|
|
const { error } = await supabase.from(table).insert({ ...values, client_id: clientId });
|
|
return !error;
|
|
}
|
|
|
|
if (!IdSchema.safeParse(id).success) return false;
|
|
|
|
const { data, error } = await supabase
|
|
.from(table)
|
|
.update(values)
|
|
.eq("id", id)
|
|
.eq("client_id", clientId)
|
|
.select("id");
|
|
|
|
return !error && Boolean(data?.length);
|
|
}
|
|
|
|
function formString(formData: FormData, key: string) {
|
|
return String(formData.get(key) ?? "");
|
|
}
|
|
|
|
function formId(formData: FormData) {
|
|
const id = formString(formData, "id");
|
|
return id === "" ? null : id;
|
|
}
|
|
|
|
export async function saveProduct(_prev: InfoActionState, formData: FormData): Promise<InfoActionState> {
|
|
const profileId = formString(formData, "profileId");
|
|
const target = await resolveAdminTarget(profileId);
|
|
if ("error" in target) return target;
|
|
|
|
const parsed = ProductSchema.safeParse({
|
|
name: formString(formData, "name"),
|
|
category: formString(formData, "category"),
|
|
url: formString(formData, "url"),
|
|
status: formString(formData, "status"),
|
|
statusMessage: formString(formData, "statusMessage"),
|
|
uptime30d: formString(formData, "uptime30d"),
|
|
});
|
|
if (!parsed.success) return { error: firstError(parsed.error) };
|
|
|
|
const ok = await upsertRow("client_products", target.clientId, formId(formData), {
|
|
name: parsed.data.name,
|
|
category: parsed.data.category,
|
|
url: parsed.data.url,
|
|
status: parsed.data.status,
|
|
status_message: parsed.data.statusMessage,
|
|
uptime_30d: parsed.data.uptime30d,
|
|
});
|
|
if (!ok) return { error: "Produkt konnte nicht gespeichert werden." };
|
|
|
|
revalidateInfo(profileId);
|
|
return { success: true };
|
|
}
|
|
|
|
export async function saveTask(_prev: InfoActionState, formData: FormData): Promise<InfoActionState> {
|
|
const profileId = formString(formData, "profileId");
|
|
const target = await resolveAdminTarget(profileId);
|
|
if ("error" in target) return target;
|
|
|
|
const parsed = TaskSchema.safeParse({
|
|
name: formString(formData, "name"),
|
|
description: formString(formData, "description"),
|
|
status: formString(formData, "status"),
|
|
since: formString(formData, "since"),
|
|
});
|
|
if (!parsed.success) return { error: firstError(parsed.error) };
|
|
|
|
const ok = await upsertRow("product_tasks", target.clientId, formId(formData), parsed.data);
|
|
if (!ok) return { error: "Vorgang konnte nicht gespeichert werden." };
|
|
|
|
revalidateInfo(profileId);
|
|
return { success: true };
|
|
}
|
|
|
|
export async function saveNote(_prev: InfoActionState, formData: FormData): Promise<InfoActionState> {
|
|
const profileId = formString(formData, "profileId");
|
|
const target = await resolveAdminTarget(profileId);
|
|
if ("error" in target) return target;
|
|
|
|
const parsed = NoteSchema.safeParse({ body: formString(formData, "body") });
|
|
if (!parsed.success) return { error: firstError(parsed.error) };
|
|
|
|
const ok = await upsertRow("internal_notes", target.clientId, formId(formData), parsed.data);
|
|
if (!ok) return { error: "Notiz konnte nicht gespeichert werden." };
|
|
|
|
revalidateInfo(profileId);
|
|
return { success: true };
|
|
}
|
|
|
|
export async function deleteInfoEntry(
|
|
kind: "product" | "task" | "note",
|
|
profileId: string,
|
|
id: string
|
|
): Promise<InfoActionState> {
|
|
const target = await resolveAdminTarget(profileId);
|
|
if ("error" in target) return target;
|
|
const table = Object.hasOwn(DELETE_TABLES, kind) ? DELETE_TABLES[kind] : undefined;
|
|
if (!table || !IdSchema.safeParse(id).success) return { error: "Ungültiger Eintrag." };
|
|
|
|
const supabase = await createClient();
|
|
const { data, error } = await supabase
|
|
.from(table)
|
|
.delete()
|
|
.eq("id", id)
|
|
.eq("client_id", target.clientId)
|
|
.select("id");
|
|
|
|
if (error || !data?.length) return { error: "Eintrag konnte nicht gelöscht werden." };
|
|
|
|
revalidateInfo(profileId);
|
|
return { success: true };
|
|
}
|