Files
kovakemlak-crm/src/lib/appwrite/customer-search-actions.ts
T
egecankomur a40e68254b feat: weighted match scoring, photo upload, property detail page
- 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
2026-05-05 19:55:34 +03:00

171 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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 };
}