933cb17107
Plan & limit enforcement: - src/lib/plans.ts: PLAN_LIMITS (free: 15 props, 25 customers, 5 presentations, 1 member, 5 imgs/prop) - checkLimit() + limitErrorMessage() — role-aware error messages (owner: upgrade CTA, others: contact admin) - Create actions (property, customer, presentation, team invite): hard limit check before create - PropertyImageUploader: maxImages prop + isOwner-aware toast - UsageBadge component: usage counter shown only to owner (green→amber→red) Role-based access: - TenantContext + ActiveContext: add memberCount + role fields - Dashboard layout: non-owner on free plan with >1 member → /plan-limit - /plan-limit: blocked-access page with owner contact info + sign-out - AppSidebar: minRole filtering — Plan & Faturalama (owner only), Çalışma Alanı/Yatırımcılar (admin+) - settings/billing: owner-only hard redirect - settings/workspace + settings/members: member redirect, admin read-only - settings/investors: member redirect - workspace-actions + logo-actions: restricted to owner only - Workspace form: canEdit = owner only (admin sees read-only view)
131 lines
3.6 KiB
TypeScript
131 lines
3.6 KiB
TypeScript
"use server";
|
||
|
||
import { ID, Permission, Role, Query } from "node-appwrite";
|
||
import { revalidatePath } from "next/cache";
|
||
|
||
import { customerSchema } from "@/lib/validation/customers";
|
||
import { DATABASE_ID, TABLES, type CustomerStage } from "./schema";
|
||
import { createAdminClient } from "./server";
|
||
import { requireTenant } from "@/lib/appwrite/tenant-guard";
|
||
import { checkLimit, limitErrorMessage } from "@/lib/plans";
|
||
|
||
type ActionState = { ok: boolean; error?: string; fieldErrors?: Record<string, string[]> };
|
||
|
||
export async function createCustomerAction(
|
||
_prev: ActionState,
|
||
formData: FormData,
|
||
): Promise<ActionState> {
|
||
const ctx = await requireTenant();
|
||
const raw = Object.fromEntries(formData.entries());
|
||
const parsed = customerSchema.safeParse(raw);
|
||
if (!parsed.success) {
|
||
return { ok: false, fieldErrors: parsed.error.flatten().fieldErrors };
|
||
}
|
||
|
||
const limitCheck = await checkLimit(ctx.tenantId, ctx.settings?.plan, "customers");
|
||
if (!limitCheck.allowed) {
|
||
return { ok: false, error: limitErrorMessage("customers", limitCheck.limit, ctx.role) };
|
||
}
|
||
|
||
const { tablesDB } = createAdminClient();
|
||
const data = parsed.data;
|
||
|
||
try {
|
||
await tablesDB.createRow(
|
||
DATABASE_ID,
|
||
TABLES.customers,
|
||
ID.unique(),
|
||
{
|
||
tenantId: ctx.tenantId,
|
||
name: data.name,
|
||
email: data.email,
|
||
phone: data.phone,
|
||
type: data.type,
|
||
stage: data.stage ?? "ilk_temas",
|
||
source: data.source,
|
||
nextFollowUpDate: data.nextFollowUpDate,
|
||
assigneeId: data.assigneeId,
|
||
notes: data.notes,
|
||
createdBy: ctx.user.id,
|
||
},
|
||
[
|
||
Permission.read(Role.team(ctx.tenantId)),
|
||
Permission.update(Role.team(ctx.tenantId)),
|
||
Permission.delete(Role.team(ctx.tenantId, "owner")),
|
||
Permission.delete(Role.team(ctx.tenantId, "admin")),
|
||
],
|
||
);
|
||
} catch {
|
||
return { ok: false, error: "Müşteri oluşturulamadı." };
|
||
}
|
||
|
||
revalidatePath("/customers");
|
||
return { ok: true };
|
||
}
|
||
|
||
export async function updateCustomerAction(
|
||
id: string,
|
||
_prev: ActionState,
|
||
formData: FormData,
|
||
): Promise<ActionState> {
|
||
await requireTenant();
|
||
const raw = Object.fromEntries(formData.entries());
|
||
const parsed = customerSchema.safeParse(raw);
|
||
if (!parsed.success) {
|
||
return { ok: false, fieldErrors: parsed.error.flatten().fieldErrors };
|
||
}
|
||
|
||
const { tablesDB } = createAdminClient();
|
||
const data = parsed.data;
|
||
|
||
try {
|
||
await tablesDB.updateRow(DATABASE_ID, TABLES.customers, id, {
|
||
name: data.name,
|
||
email: data.email,
|
||
phone: data.phone,
|
||
type: data.type,
|
||
stage: data.stage,
|
||
source: data.source,
|
||
nextFollowUpDate: data.nextFollowUpDate,
|
||
assigneeId: data.assigneeId,
|
||
notes: data.notes,
|
||
});
|
||
} catch {
|
||
return { ok: false, error: "Müşteri güncellenemedi." };
|
||
}
|
||
|
||
revalidatePath("/customers");
|
||
return { ok: true };
|
||
}
|
||
|
||
export async function updateCustomerStageAction(
|
||
id: string,
|
||
stage: CustomerStage,
|
||
): Promise<ActionState> {
|
||
await requireTenant();
|
||
const { tablesDB } = createAdminClient();
|
||
|
||
try {
|
||
await tablesDB.updateRow(DATABASE_ID, TABLES.customers, id, { stage });
|
||
} catch {
|
||
return { ok: false, error: "Aşama güncellenemedi." };
|
||
}
|
||
|
||
revalidatePath("/customers");
|
||
return { ok: true };
|
||
}
|
||
|
||
export async function deleteCustomerAction(id: string): Promise<ActionState> {
|
||
await requireTenant();
|
||
const { tablesDB } = createAdminClient();
|
||
|
||
try {
|
||
await tablesDB.deleteRow(DATABASE_ID, TABLES.customers, id);
|
||
} catch {
|
||
return { ok: false, error: "Müşteri silinemedi." };
|
||
}
|
||
|
||
revalidatePath("/customers");
|
||
return { ok: true };
|
||
}
|