a40e68254b
- scoring.ts: pure scoreMatch + scoreMatchBreakdown with per-criterion weights - matching.ts: soft scoring (0-100), updates score on re-sync, threshold 20 - search-form-sheet: weight selectors (1-5) per criterion - customer-search-actions: save/update weight fields - storage-actions: upload/delete property images to property-images bucket - storage-utils: getPropertyImagePreviewUrl, parseImageIds helpers - property-image-uploader: client component with preview grid + delete - property-form-sheet: integrated image uploader - properties/[id]: detail page with gallery, specs, matches sidebar - properties-client: Detay link in dropdown - matches page: MatchesClient with click-to-breakdown dialog - sunum page: cover image from first imageIds entry - matches-client + match-breakdown-dialog: score breakdown per criterion
171 lines
5.1 KiB
TypeScript
171 lines
5.1 KiB
TypeScript
"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,
|
||
priceWeight: data.priceWeight,
|
||
m2Weight: data.m2Weight,
|
||
locationWeight: data.locationWeight,
|
||
roomCountWeight: data.roomCountWeight,
|
||
propertyTypeWeight: data.propertyTypeWeight,
|
||
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,
|
||
priceWeight: data.priceWeight,
|
||
m2Weight: data.m2Weight,
|
||
locationWeight: data.locationWeight,
|
||
roomCountWeight: data.roomCountWeight,
|
||
propertyTypeWeight: data.propertyTypeWeight,
|
||
});
|
||
|
||
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 };
|
||
}
|