206 lines
5.3 KiB
TypeScript
206 lines
5.3 KiB
TypeScript
"use server";
|
||
|
||
import { revalidatePath } from "next/cache";
|
||
import { AppwriteException, ID, Permission, Role } from "node-appwrite";
|
||
import { z } from "zod";
|
||
|
||
import { logAudit } from "./audit";
|
||
import {
|
||
isPlanLimitError,
|
||
planLimitMessage,
|
||
requirePlanCapacity,
|
||
} from "./plan-limits";
|
||
import { DATABASE_ID, TABLES, type Customer } from "./schema";
|
||
import { createAdminClient } from "./server";
|
||
import { requireTenant } from "./tenant-guard";
|
||
import type { CustomerActionState } from "./customer-types";
|
||
import { customerSchema } from "@/lib/validation/customers";
|
||
|
||
function appwriteError(e: unknown): string {
|
||
if (e instanceof AppwriteException) {
|
||
return e.message || "Beklenmeyen bir hata oluştu.";
|
||
}
|
||
return "Bağlantı hatası. Tekrar deneyin.";
|
||
}
|
||
|
||
function pickFormFields(formData: FormData) {
|
||
return {
|
||
name: String(formData.get("name") ?? "").trim(),
|
||
email: String(formData.get("email") ?? "").trim(),
|
||
phone: String(formData.get("phone") ?? "").trim(),
|
||
taxId: String(formData.get("taxId") ?? "").trim(),
|
||
taxOffice: String(formData.get("taxOffice") ?? "").trim(),
|
||
website: String(formData.get("website") ?? "").trim(),
|
||
address: String(formData.get("address") ?? "").trim(),
|
||
notes: String(formData.get("notes") ?? "").trim(),
|
||
status: (formData.get("status") as "active" | "passive" | null) ?? "active",
|
||
};
|
||
}
|
||
|
||
function flattenErrors(err: z.ZodError): Record<string, string> {
|
||
const out: Record<string, string> = {};
|
||
for (const issue of err.issues) {
|
||
const key = issue.path.join(".");
|
||
if (key && !out[key]) out[key] = issue.message;
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function teamRowPermissions(tenantId: string) {
|
||
return [
|
||
Permission.read(Role.team(tenantId)),
|
||
Permission.update(Role.team(tenantId)),
|
||
Permission.delete(Role.team(tenantId, "owner")),
|
||
Permission.delete(Role.team(tenantId, "admin")),
|
||
];
|
||
}
|
||
|
||
export async function createCustomerAction(
|
||
_prev: CustomerActionState,
|
||
formData: FormData,
|
||
): Promise<CustomerActionState> {
|
||
let ctx;
|
||
try {
|
||
ctx = await requireTenant();
|
||
} catch {
|
||
return { ok: false, error: "Yetkiniz yok." };
|
||
}
|
||
|
||
const parsed = customerSchema.safeParse(pickFormFields(formData));
|
||
if (!parsed.success) {
|
||
return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) };
|
||
}
|
||
|
||
try {
|
||
await requirePlanCapacity(ctx, "customers");
|
||
} catch (e) {
|
||
if (isPlanLimitError(e)) {
|
||
return {
|
||
ok: false,
|
||
error: planLimitMessage(e.resource, e.limit),
|
||
code: "PLAN_LIMIT_EXCEEDED",
|
||
};
|
||
}
|
||
throw e;
|
||
}
|
||
|
||
try {
|
||
const { tablesDB } = createAdminClient();
|
||
const row = await tablesDB.createRow(
|
||
DATABASE_ID,
|
||
TABLES.customers,
|
||
ID.unique(),
|
||
{
|
||
tenantId: ctx.tenantId,
|
||
createdBy: ctx.user.id,
|
||
...parsed.data,
|
||
},
|
||
teamRowPermissions(ctx.tenantId),
|
||
);
|
||
|
||
await logAudit({
|
||
tenantId: ctx.tenantId,
|
||
userId: ctx.user.id,
|
||
action: "create",
|
||
entityType: "customer",
|
||
entityId: row.$id,
|
||
changes: parsed.data,
|
||
});
|
||
} catch (e) {
|
||
return { ok: false, error: appwriteError(e) };
|
||
}
|
||
|
||
revalidatePath("/customers");
|
||
return { ok: true };
|
||
}
|
||
|
||
export async function updateCustomerAction(
|
||
_prev: CustomerActionState,
|
||
formData: FormData,
|
||
): Promise<CustomerActionState> {
|
||
const id = String(formData.get("id") ?? "");
|
||
if (!id) return { ok: false, error: "ID eksik." };
|
||
|
||
let ctx;
|
||
try {
|
||
ctx = await requireTenant();
|
||
} catch {
|
||
return { ok: false, error: "Yetkiniz yok." };
|
||
}
|
||
|
||
const parsed = customerSchema.safeParse(pickFormFields(formData));
|
||
if (!parsed.success) {
|
||
return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) };
|
||
}
|
||
|
||
try {
|
||
const { tablesDB } = createAdminClient();
|
||
const existing = (await tablesDB.getRow(
|
||
DATABASE_ID,
|
||
TABLES.customers,
|
||
id,
|
||
)) as unknown as Customer;
|
||
|
||
if (existing.tenantId !== ctx.tenantId) {
|
||
return { ok: false, error: "Erişim engellendi." };
|
||
}
|
||
|
||
await tablesDB.updateRow(DATABASE_ID, TABLES.customers, id, parsed.data);
|
||
|
||
await logAudit({
|
||
tenantId: ctx.tenantId,
|
||
userId: ctx.user.id,
|
||
action: "update",
|
||
entityType: "customer",
|
||
entityId: id,
|
||
changes: parsed.data,
|
||
});
|
||
} catch (e) {
|
||
return { ok: false, error: appwriteError(e) };
|
||
}
|
||
|
||
revalidatePath("/customers");
|
||
return { ok: true };
|
||
}
|
||
|
||
export async function deleteCustomerAction(formData: FormData): Promise<CustomerActionState> {
|
||
const id = String(formData.get("id") ?? "");
|
||
if (!id) return { ok: false, error: "ID eksik." };
|
||
|
||
let ctx;
|
||
try {
|
||
ctx = await requireTenant();
|
||
} catch {
|
||
return { ok: false, error: "Yetkiniz yok." };
|
||
}
|
||
|
||
try {
|
||
const { tablesDB } = createAdminClient();
|
||
const existing = (await tablesDB.getRow(
|
||
DATABASE_ID,
|
||
TABLES.customers,
|
||
id,
|
||
)) as unknown as Customer;
|
||
|
||
if (existing.tenantId !== ctx.tenantId) {
|
||
return { ok: false, error: "Erişim engellendi." };
|
||
}
|
||
|
||
await tablesDB.deleteRow(DATABASE_ID, TABLES.customers, id);
|
||
|
||
await logAudit({
|
||
tenantId: ctx.tenantId,
|
||
userId: ctx.user.id,
|
||
action: "delete",
|
||
entityType: "customer",
|
||
entityId: id,
|
||
changes: { name: existing.name },
|
||
});
|
||
} catch (e) {
|
||
return { ok: false, error: appwriteError(e) };
|
||
}
|
||
|
||
revalidatePath("/customers");
|
||
return { ok: true };
|
||
}
|