feat: all core modules — properties, customers, searches, matches, presentations, activities, investors + public sunum page
- Server actions: property/customer/search/presentation/activity/investor CRUD - Matching engine: matchPropertyToSearches + syncMatchesForSearch on search save - UI: form sheets + table clients for all modules - Public /sunum/[token] page (no auth) with property card grid + expiry check - All pages force-dynamic for auth guard compatibility
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
"use server";
|
||||
|
||||
import { ID, Permission, Query, Role } from "node-appwrite";
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
import { customerSearchSchema } from "@/lib/validation/customer-searches";
|
||||
import { DATABASE_ID, TABLES, type Property } from "./schema";
|
||||
import { createAdminClient } from "./server";
|
||||
import { requireTenant } from "@/lib/appwrite/tenant-guard";
|
||||
import { matchPropertyToSearches } from "./matching";
|
||||
|
||||
type ActionState = { ok: boolean; error?: string; fieldErrors?: Record<string, string[]> };
|
||||
|
||||
function toJsonList(csv?: string | null): string | undefined {
|
||||
if (!csv || !csv.trim()) return undefined;
|
||||
const items = csv
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
return items.length ? JSON.stringify(items) : undefined;
|
||||
}
|
||||
|
||||
async function syncMatchesForSearch(tenantId: string, userId: string): Promise<void> {
|
||||
const { tablesDB } = createAdminClient();
|
||||
const result = await tablesDB.listRows({
|
||||
databaseId: DATABASE_ID,
|
||||
tableId: TABLES.properties,
|
||||
queries: [
|
||||
Query.equal("tenantId", tenantId),
|
||||
Query.equal("status", "aktif"),
|
||||
Query.limit(200),
|
||||
],
|
||||
});
|
||||
const properties = result.rows as unknown as Property[];
|
||||
for (const property of properties) {
|
||||
await matchPropertyToSearches(property, tenantId, userId).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
export async function createCustomerSearchAction(
|
||||
_prev: ActionState,
|
||||
formData: FormData,
|
||||
): Promise<ActionState> {
|
||||
const ctx = await requireTenant();
|
||||
const raw = Object.fromEntries(formData.entries());
|
||||
const parsed = customerSearchSchema.safeParse(raw);
|
||||
if (!parsed.success) {
|
||||
return { ok: false, fieldErrors: parsed.error.flatten().fieldErrors };
|
||||
}
|
||||
|
||||
const { tablesDB } = createAdminClient();
|
||||
const data = parsed.data;
|
||||
|
||||
try {
|
||||
await tablesDB.createRow(
|
||||
DATABASE_ID,
|
||||
TABLES.customerSearches,
|
||||
ID.unique(),
|
||||
{
|
||||
tenantId: ctx.tenantId,
|
||||
customerId: data.customerId,
|
||||
listingType: data.listingType || undefined,
|
||||
propertyTypes: toJsonList(data.propertyTypes),
|
||||
roomCounts: toJsonList(data.roomCounts),
|
||||
minPrice: data.minPrice,
|
||||
maxPrice: data.maxPrice,
|
||||
minM2: data.minM2,
|
||||
maxM2: data.maxM2,
|
||||
cities: toJsonList(data.cities),
|
||||
districts: toJsonList(data.districts),
|
||||
isActive: true,
|
||||
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")),
|
||||
],
|
||||
);
|
||||
|
||||
await syncMatchesForSearch(ctx.tenantId, ctx.user.id);
|
||||
} catch {
|
||||
return { ok: false, error: "Arama kriteri oluşturulamadı." };
|
||||
}
|
||||
|
||||
revalidatePath("/customers/searches");
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
export async function updateCustomerSearchAction(
|
||||
id: string,
|
||||
_prev: ActionState,
|
||||
formData: FormData,
|
||||
): Promise<ActionState> {
|
||||
const ctx = await requireTenant();
|
||||
const raw = Object.fromEntries(formData.entries());
|
||||
const parsed = customerSearchSchema.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.customerSearches, id, {
|
||||
customerId: data.customerId,
|
||||
listingType: data.listingType || undefined,
|
||||
propertyTypes: toJsonList(data.propertyTypes),
|
||||
roomCounts: toJsonList(data.roomCounts),
|
||||
minPrice: data.minPrice,
|
||||
maxPrice: data.maxPrice,
|
||||
minM2: data.minM2,
|
||||
maxM2: data.maxM2,
|
||||
cities: toJsonList(data.cities),
|
||||
districts: toJsonList(data.districts),
|
||||
notes: data.notes,
|
||||
});
|
||||
|
||||
await syncMatchesForSearch(ctx.tenantId, ctx.user.id);
|
||||
} catch {
|
||||
return { ok: false, error: "Arama kriteri güncellenemedi." };
|
||||
}
|
||||
|
||||
revalidatePath("/customers/searches");
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
export async function toggleCustomerSearchActiveAction(
|
||||
id: string,
|
||||
isActive: boolean,
|
||||
): Promise<ActionState> {
|
||||
await requireTenant();
|
||||
const { tablesDB } = createAdminClient();
|
||||
|
||||
try {
|
||||
await tablesDB.updateRow(DATABASE_ID, TABLES.customerSearches, id, { isActive });
|
||||
} catch {
|
||||
return { ok: false, error: "Durum güncellenemedi." };
|
||||
}
|
||||
|
||||
revalidatePath("/customers/searches");
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
export async function deleteCustomerSearchAction(id: string): Promise<ActionState> {
|
||||
await requireTenant();
|
||||
const { tablesDB } = createAdminClient();
|
||||
|
||||
try {
|
||||
await tablesDB.deleteRow(DATABASE_ID, TABLES.customerSearches, id);
|
||||
} catch {
|
||||
return { ok: false, error: "Arama kriteri silinemedi." };
|
||||
}
|
||||
|
||||
revalidatePath("/customers/searches");
|
||||
return { ok: true };
|
||||
}
|
||||
Reference in New Issue
Block a user