48361792f0
The /connections page was firing one listClinicPricing call per approved
counterpart inside Promise.all. That meant a lab with 10 clinics paid 10
sequential Appwrite roundtrips on every load, and worse, every time the
ClinicPricingDialog saved a row revalidatePath('/connections') ran the
whole fan-out again — saving a single discount felt like the request had
hung.
Replaced the per-peer query with listAllPricingForLab /
listAllPricingForClinic (single Query.equal on the side-specific column,
limit 500) and group the result into a Map client-side. One roundtrip
regardless of how many connections you have.
Also flipped the audit-log calls in setClinicPricingAction /
clearClinicPricingAction from 'await logAudit(...)' to 'void logAudit(...)'
— audit is best-effort by design and never blocks the user-facing
mutation; awaiting it doubled the perceived latency for nothing.
201 lines
5.9 KiB
TypeScript
201 lines
5.9 KiB
TypeScript
"use server";
|
||
|
||
import { revalidatePath } from "next/cache";
|
||
import { AppwriteException, ID, Permission, Query, Role } from "node-appwrite";
|
||
import { z } from "zod";
|
||
|
||
import { logAudit } from "./audit";
|
||
import {
|
||
DATABASE_ID,
|
||
TABLES,
|
||
type ClinicPricing,
|
||
type Connection,
|
||
} from "./schema";
|
||
import { createAdminClient } from "./server";
|
||
import {
|
||
requireRole,
|
||
requireTenant,
|
||
requireTenantKind,
|
||
} from "./tenant-guard";
|
||
import type {
|
||
ClinicPricingActionState,
|
||
ClinicPricingFormState,
|
||
} from "./clinic-pricing-types";
|
||
import { clinicPricingSchema } from "@/lib/validation/clinic-pricing";
|
||
|
||
function appwriteError(e: unknown, fallback = "Beklenmeyen bir hata oluştu."): string {
|
||
if (e instanceof AppwriteException) return e.message || fallback;
|
||
return process.env.NODE_ENV !== "production" && e instanceof Error
|
||
? `${fallback} (${e.message})`
|
||
: fallback;
|
||
}
|
||
|
||
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 pricingPermissions(labTenantId: string, clinicTenantId: string): string[] {
|
||
return [
|
||
Permission.read(Role.team(labTenantId)),
|
||
Permission.read(Role.team(clinicTenantId)),
|
||
Permission.update(Role.team(labTenantId, "owner")),
|
||
Permission.update(Role.team(labTenantId, "admin")),
|
||
Permission.delete(Role.team(labTenantId, "owner")),
|
||
Permission.delete(Role.team(labTenantId, "admin")),
|
||
];
|
||
}
|
||
|
||
function pickFields(formData: FormData) {
|
||
return {
|
||
clinicTenantId: String(formData.get("clinicTenantId") ?? "").trim(),
|
||
prostheticType: String(formData.get("prostheticType") ?? "").trim(),
|
||
customUnitPrice: String(formData.get("customUnitPrice") ?? "").trim(),
|
||
discountPercent: String(formData.get("discountPercent") ?? "").trim(),
|
||
currency: String(formData.get("currency") ?? "").trim(),
|
||
};
|
||
}
|
||
|
||
export async function setClinicPricingAction(
|
||
_prev: ClinicPricingFormState,
|
||
formData: FormData,
|
||
): Promise<ClinicPricingFormState> {
|
||
let ctx;
|
||
try {
|
||
ctx = await requireTenant();
|
||
requireRole(ctx, ["owner", "admin"]);
|
||
requireTenantKind(ctx, ["lab"]);
|
||
} catch {
|
||
return { ok: false, error: "Fiyatlandırma yalnızca laboratuvar hesapları için." };
|
||
}
|
||
|
||
const parsed = clinicPricingSchema.safeParse(pickFields(formData));
|
||
if (!parsed.success) {
|
||
return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) };
|
||
}
|
||
|
||
const { tablesDB } = createAdminClient();
|
||
|
||
// Verify the lab actually has an approved connection with this clinic
|
||
const connRes = await tablesDB.listRows({
|
||
databaseId: DATABASE_ID,
|
||
tableId: TABLES.connections,
|
||
queries: [
|
||
Query.equal("labTenantId", ctx.tenantId),
|
||
Query.equal("clinicTenantId", parsed.data.clinicTenantId),
|
||
Query.equal("status", "approved"),
|
||
Query.limit(1),
|
||
],
|
||
});
|
||
if (!(connRes.rows[0] as unknown as Connection | undefined)) {
|
||
return {
|
||
ok: false,
|
||
error: "Onaylı bir bağlantınız yok.",
|
||
fieldErrors: { clinicTenantId: "Bağlantı yok." },
|
||
};
|
||
}
|
||
|
||
try {
|
||
const existing = await tablesDB.listRows({
|
||
databaseId: DATABASE_ID,
|
||
tableId: TABLES.clinicPricing,
|
||
queries: [
|
||
Query.equal("labTenantId", ctx.tenantId),
|
||
Query.equal("clinicTenantId", parsed.data.clinicTenantId),
|
||
Query.equal("prostheticType", parsed.data.prostheticType),
|
||
Query.limit(1),
|
||
],
|
||
});
|
||
const row = existing.rows[0] as unknown as ClinicPricing | undefined;
|
||
|
||
const payload = {
|
||
labTenantId: ctx.tenantId,
|
||
clinicTenantId: parsed.data.clinicTenantId,
|
||
prostheticType: parsed.data.prostheticType,
|
||
customUnitPrice: parsed.data.customUnitPrice,
|
||
discountPercent: parsed.data.discountPercent,
|
||
currency: parsed.data.currency,
|
||
createdBy: ctx.user.id,
|
||
};
|
||
|
||
if (row) {
|
||
await tablesDB.updateRow(DATABASE_ID, TABLES.clinicPricing, row.$id, payload);
|
||
void logAudit({
|
||
tenantId: ctx.tenantId,
|
||
userId: ctx.user.id,
|
||
action: "update",
|
||
entityType: "clinic_pricing",
|
||
entityId: row.$id,
|
||
changes: payload,
|
||
});
|
||
} else {
|
||
const created = await tablesDB.createRow(
|
||
DATABASE_ID,
|
||
TABLES.clinicPricing,
|
||
ID.unique(),
|
||
payload,
|
||
pricingPermissions(ctx.tenantId, parsed.data.clinicTenantId),
|
||
);
|
||
void logAudit({
|
||
tenantId: ctx.tenantId,
|
||
userId: ctx.user.id,
|
||
action: "create",
|
||
entityType: "clinic_pricing",
|
||
entityId: created.$id,
|
||
changes: payload,
|
||
});
|
||
}
|
||
} catch (e) {
|
||
return { ok: false, error: appwriteError(e, "Fiyatlandırma kaydedilemedi.") };
|
||
}
|
||
|
||
revalidatePath("/connections");
|
||
return { ok: true };
|
||
}
|
||
|
||
export async function clearClinicPricingAction(
|
||
_prev: ClinicPricingActionState,
|
||
formData: FormData,
|
||
): Promise<ClinicPricingActionState> {
|
||
const id = String(formData.get("id") ?? "").trim();
|
||
if (!id) return { ok: false, error: "Kayıt bulunamadı." };
|
||
|
||
let ctx;
|
||
try {
|
||
ctx = await requireTenant();
|
||
requireRole(ctx, ["owner", "admin"]);
|
||
requireTenantKind(ctx, ["lab"]);
|
||
} catch {
|
||
return { ok: false, error: "Yetkiniz yok." };
|
||
}
|
||
|
||
try {
|
||
const { tablesDB } = createAdminClient();
|
||
const row = (await tablesDB.getRow(
|
||
DATABASE_ID,
|
||
TABLES.clinicPricing,
|
||
id,
|
||
)) as unknown as ClinicPricing;
|
||
if (row.labTenantId !== ctx.tenantId) {
|
||
return { ok: false, error: "Yetkiniz yok." };
|
||
}
|
||
await tablesDB.deleteRow(DATABASE_ID, TABLES.clinicPricing, id);
|
||
void logAudit({
|
||
tenantId: ctx.tenantId,
|
||
userId: ctx.user.id,
|
||
action: "delete",
|
||
entityType: "clinic_pricing",
|
||
entityId: id,
|
||
});
|
||
} catch (e) {
|
||
return { ok: false, error: appwriteError(e, "Silinemedi.") };
|
||
}
|
||
|
||
revalidatePath("/connections");
|
||
return { ok: true };
|
||
}
|