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,117 @@
|
||||
"use server";
|
||||
|
||||
import { ID, Permission, Role } from "node-appwrite";
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
import { activitySchema } from "@/lib/validation/activities";
|
||||
import { DATABASE_ID, TABLES } from "./schema";
|
||||
import { createAdminClient } from "./server";
|
||||
import { requireTenant } from "@/lib/appwrite/tenant-guard";
|
||||
|
||||
type ActionState = { ok: boolean; error?: string; fieldErrors?: Record<string, string[]> };
|
||||
|
||||
export async function createActivityAction(
|
||||
_prev: ActionState,
|
||||
formData: FormData,
|
||||
): Promise<ActionState> {
|
||||
const ctx = await requireTenant();
|
||||
const raw = Object.fromEntries(formData.entries());
|
||||
const parsed = activitySchema.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.activities,
|
||||
ID.unique(),
|
||||
{
|
||||
tenantId: ctx.tenantId,
|
||||
type: data.type,
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
customerId: data.customerId,
|
||||
propertyId: data.propertyId,
|
||||
dueDate: data.dueDate,
|
||||
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: "Aktivite oluşturulamadı." };
|
||||
}
|
||||
|
||||
revalidatePath("/activities");
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
export async function updateActivityAction(
|
||||
id: string,
|
||||
_prev: ActionState,
|
||||
formData: FormData,
|
||||
): Promise<ActionState> {
|
||||
await requireTenant();
|
||||
const raw = Object.fromEntries(formData.entries());
|
||||
const parsed = activitySchema.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.activities, id, {
|
||||
type: data.type,
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
customerId: data.customerId,
|
||||
propertyId: data.propertyId,
|
||||
dueDate: data.dueDate,
|
||||
});
|
||||
} catch {
|
||||
return { ok: false, error: "Aktivite güncellenemedi." };
|
||||
}
|
||||
|
||||
revalidatePath("/activities");
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
export async function completeActivityAction(id: string): Promise<ActionState> {
|
||||
await requireTenant();
|
||||
const { tablesDB } = createAdminClient();
|
||||
|
||||
try {
|
||||
await tablesDB.updateRow(DATABASE_ID, TABLES.activities, id, {
|
||||
completedAt: new Date().toISOString(),
|
||||
});
|
||||
} catch {
|
||||
return { ok: false, error: "Aktivite tamamlanamadı." };
|
||||
}
|
||||
|
||||
revalidatePath("/activities");
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
export async function deleteActivityAction(id: string): Promise<ActionState> {
|
||||
await requireTenant();
|
||||
const { tablesDB } = createAdminClient();
|
||||
|
||||
try {
|
||||
await tablesDB.deleteRow(DATABASE_ID, TABLES.activities, id);
|
||||
} catch {
|
||||
return { ok: false, error: "Aktivite silinemedi." };
|
||||
}
|
||||
|
||||
revalidatePath("/activities");
|
||||
return { ok: true };
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
"use server";
|
||||
|
||||
import { ID, Permission, Role } from "node-appwrite";
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
import { customerSchema } from "@/lib/validation/customers";
|
||||
import { DATABASE_ID, TABLES } from "./schema";
|
||||
import { createAdminClient } from "./server";
|
||||
import { requireTenant } from "@/lib/appwrite/tenant-guard";
|
||||
|
||||
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 { 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,
|
||||
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,
|
||||
notes: data.notes,
|
||||
});
|
||||
} catch {
|
||||
return { ok: false, error: "Müşteri 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 };
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import "server-only";
|
||||
|
||||
import { Query } from "node-appwrite";
|
||||
|
||||
import { DATABASE_ID, TABLES, type Customer } from "./schema";
|
||||
import { createAdminClient } from "./server";
|
||||
|
||||
export async function listCustomers(tenantId: string): Promise<Customer[]> {
|
||||
const { tablesDB } = createAdminClient();
|
||||
const result = await tablesDB.listRows({
|
||||
databaseId: DATABASE_ID,
|
||||
tableId: TABLES.customers,
|
||||
queries: [Query.equal("tenantId", tenantId), Query.orderDesc("$createdAt"), Query.limit(500)],
|
||||
});
|
||||
return result.rows as unknown as Customer[];
|
||||
}
|
||||
|
||||
export async function getCustomer(id: string, tenantId: string): Promise<Customer | null> {
|
||||
const { tablesDB } = createAdminClient();
|
||||
const row = (await tablesDB.getRow(DATABASE_ID, TABLES.customers, id)) as unknown as Customer;
|
||||
if (row.tenantId !== tenantId) return null;
|
||||
return row;
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
"use server";
|
||||
|
||||
import { ID, Permission, Role } from "node-appwrite";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { z } from "zod";
|
||||
|
||||
import { DATABASE_ID, TABLES } from "./schema";
|
||||
import { createAdminClient } from "./server";
|
||||
import { requireTenant } from "@/lib/appwrite/tenant-guard";
|
||||
|
||||
const investorSchema = z.object({
|
||||
name: z.string().trim().min(2, "En az 2 karakter").max(255),
|
||||
email: z.string().email("Geçerli bir email girin."),
|
||||
phone: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(30)
|
||||
.optional()
|
||||
.transform((v) => v || undefined),
|
||||
budget: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((v) => (v ? Number(v) : undefined))
|
||||
.pipe(z.number().min(0).optional()),
|
||||
currency: z
|
||||
.string()
|
||||
.max(10)
|
||||
.optional()
|
||||
.transform((v) => v || undefined),
|
||||
notes: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(2000)
|
||||
.optional()
|
||||
.transform((v) => v || undefined),
|
||||
});
|
||||
|
||||
type ActionState = { ok: boolean; error?: string; fieldErrors?: Record<string, string[]> };
|
||||
|
||||
export async function createInvestorAction(
|
||||
_prev: ActionState,
|
||||
formData: FormData,
|
||||
): Promise<ActionState> {
|
||||
const ctx = await requireTenant();
|
||||
const raw = Object.fromEntries(formData.entries());
|
||||
const parsed = investorSchema.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.investors,
|
||||
ID.unique(),
|
||||
{
|
||||
tenantId: ctx.tenantId,
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
phone: data.phone,
|
||||
budget: data.budget,
|
||||
currency: data.currency ?? "TRY",
|
||||
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: "Yatırımcı oluşturulamadı." };
|
||||
}
|
||||
|
||||
revalidatePath("/investors");
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
export async function updateInvestorAction(
|
||||
id: string,
|
||||
_prev: ActionState,
|
||||
formData: FormData,
|
||||
): Promise<ActionState> {
|
||||
await requireTenant();
|
||||
const raw = Object.fromEntries(formData.entries());
|
||||
const parsed = investorSchema.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.investors, id, {
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
phone: data.phone,
|
||||
budget: data.budget,
|
||||
currency: data.currency,
|
||||
notes: data.notes,
|
||||
});
|
||||
} catch {
|
||||
return { ok: false, error: "Yatırımcı güncellenemedi." };
|
||||
}
|
||||
|
||||
revalidatePath("/investors");
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
export async function deleteInvestorAction(id: string): Promise<ActionState> {
|
||||
await requireTenant();
|
||||
const { tablesDB } = createAdminClient();
|
||||
|
||||
try {
|
||||
await tablesDB.deleteRow(DATABASE_ID, TABLES.investors, id);
|
||||
} catch {
|
||||
return { ok: false, error: "Yatırımcı silinemedi." };
|
||||
}
|
||||
|
||||
revalidatePath("/investors");
|
||||
return { ok: true };
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import "server-only";
|
||||
|
||||
import { ID, Permission, Query, Role } from "node-appwrite";
|
||||
|
||||
import { DATABASE_ID, TABLES, type Property, type CustomerSearch } from "./schema";
|
||||
import { createAdminClient } from "./server";
|
||||
|
||||
function priceMatches(price: number, min?: number | null, max?: number | null): boolean {
|
||||
if (min != null && price < min) return false;
|
||||
if (max != null && price > max) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function m2Matches(m2?: number | null, min?: number | null, max?: number | null): boolean {
|
||||
if (!m2) return true;
|
||||
if (min != null && m2 < min) return false;
|
||||
if (max != null && m2 > max) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function listMatches(value: string, jsonList?: string | null): boolean {
|
||||
if (!jsonList) return true;
|
||||
try {
|
||||
const arr = JSON.parse(jsonList) as string[];
|
||||
if (!arr.length) return true;
|
||||
return arr.includes(value);
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function cityMatches(city: string, jsonCities?: string | null): boolean {
|
||||
if (!jsonCities) return true;
|
||||
try {
|
||||
const arr = JSON.parse(jsonCities) as string[];
|
||||
if (!arr.length) return true;
|
||||
return arr.some((c) => c.toLowerCase() === city.toLowerCase());
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function districtMatches(district?: string | null, jsonDistricts?: string | null): boolean {
|
||||
if (!jsonDistricts) return true;
|
||||
try {
|
||||
const arr = JSON.parse(jsonDistricts) as string[];
|
||||
if (!arr.length) return true;
|
||||
if (!district) return false;
|
||||
return arr.some((d) => d.toLowerCase() === district.toLowerCase());
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export async function matchPropertyToSearches(
|
||||
property: Property,
|
||||
tenantId: string,
|
||||
createdBy: string,
|
||||
): Promise<void> {
|
||||
if (property.status !== "aktif") return;
|
||||
|
||||
const { tablesDB } = createAdminClient();
|
||||
|
||||
const searchesResult = await tablesDB.listRows({
|
||||
databaseId: DATABASE_ID,
|
||||
tableId: TABLES.customerSearches,
|
||||
queries: [
|
||||
Query.equal("tenantId", tenantId),
|
||||
Query.equal("isActive", true),
|
||||
Query.limit(200),
|
||||
],
|
||||
});
|
||||
|
||||
const searches = searchesResult.rows as unknown as CustomerSearch[];
|
||||
|
||||
for (const search of searches) {
|
||||
if (search.listingType && search.listingType !== property.listingType) continue;
|
||||
if (!listMatches(property.propertyType, search.propertyTypes)) continue;
|
||||
if (property.roomCount && !listMatches(property.roomCount, search.roomCounts)) continue;
|
||||
if (!priceMatches(property.price, search.minPrice, search.maxPrice)) continue;
|
||||
if (!m2Matches(property.netM2, search.minM2, search.maxM2)) continue;
|
||||
if (!cityMatches(property.city, search.cities)) continue;
|
||||
if (!districtMatches(property.district, search.districts)) continue;
|
||||
|
||||
// duplicate kontrolü
|
||||
const existing = await tablesDB.listRows({
|
||||
databaseId: DATABASE_ID,
|
||||
tableId: TABLES.propertyMatches,
|
||||
queries: [
|
||||
Query.equal("propertyId", property.$id),
|
||||
Query.equal("searchId", search.$id),
|
||||
Query.limit(1),
|
||||
],
|
||||
});
|
||||
if (existing.rows.length > 0) continue;
|
||||
|
||||
await tablesDB.createRow(
|
||||
DATABASE_ID,
|
||||
TABLES.propertyMatches,
|
||||
ID.unique(),
|
||||
{
|
||||
tenantId,
|
||||
propertyId: property.$id,
|
||||
customerId: search.customerId,
|
||||
searchId: search.$id,
|
||||
notified: false,
|
||||
createdBy,
|
||||
},
|
||||
[
|
||||
Permission.read(Role.team(tenantId)),
|
||||
Permission.update(Role.team(tenantId)),
|
||||
Permission.delete(Role.team(tenantId, "owner")),
|
||||
Permission.delete(Role.team(tenantId, "admin")),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
"use server";
|
||||
|
||||
import crypto from "crypto";
|
||||
import { ID, Permission, Role } from "node-appwrite";
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
import { presentationSchema } from "@/lib/validation/presentations";
|
||||
import { DATABASE_ID, TABLES } from "./schema";
|
||||
import { createAdminClient } from "./server";
|
||||
import { requireTenant } from "@/lib/appwrite/tenant-guard";
|
||||
|
||||
type ActionState = { ok: boolean; error?: string; fieldErrors?: Record<string, string[]>; id?: string };
|
||||
|
||||
export async function createPresentationAction(
|
||||
_prev: ActionState,
|
||||
formData: FormData,
|
||||
): Promise<ActionState> {
|
||||
const ctx = await requireTenant();
|
||||
const selectedIds = formData.getAll("propertyIdsRaw") as string[];
|
||||
const raw = {
|
||||
...Object.fromEntries(formData.entries()),
|
||||
propertyIds: JSON.stringify(selectedIds),
|
||||
};
|
||||
const parsed = presentationSchema.safeParse(raw);
|
||||
if (!parsed.success) {
|
||||
return { ok: false, fieldErrors: parsed.error.flatten().fieldErrors };
|
||||
}
|
||||
|
||||
const { tablesDB } = createAdminClient();
|
||||
const data = parsed.data;
|
||||
const id = ID.unique();
|
||||
const shareToken = crypto.randomBytes(16).toString("hex");
|
||||
|
||||
try {
|
||||
await tablesDB.createRow(
|
||||
DATABASE_ID,
|
||||
TABLES.presentations,
|
||||
id,
|
||||
{
|
||||
tenantId: ctx.tenantId,
|
||||
title: data.title,
|
||||
customerId: data.customerId,
|
||||
propertyIds: data.propertyIds,
|
||||
shareToken,
|
||||
expiresAt: data.expiresAt,
|
||||
viewCount: 0,
|
||||
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: "Sunum oluşturulamadı." };
|
||||
}
|
||||
|
||||
revalidatePath("/presentations");
|
||||
return { ok: true, id };
|
||||
}
|
||||
|
||||
export async function updatePresentationAction(
|
||||
id: string,
|
||||
_prev: ActionState,
|
||||
formData: FormData,
|
||||
): Promise<ActionState> {
|
||||
await requireTenant();
|
||||
const selectedIds = formData.getAll("propertyIdsRaw") as string[];
|
||||
const raw = {
|
||||
...Object.fromEntries(formData.entries()),
|
||||
propertyIds: JSON.stringify(selectedIds),
|
||||
};
|
||||
const parsed = presentationSchema.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.presentations, id, {
|
||||
title: data.title,
|
||||
customerId: data.customerId,
|
||||
propertyIds: data.propertyIds,
|
||||
expiresAt: data.expiresAt,
|
||||
notes: data.notes,
|
||||
});
|
||||
} catch {
|
||||
return { ok: false, error: "Sunum güncellenemedi." };
|
||||
}
|
||||
|
||||
revalidatePath("/presentations");
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
export async function deletePresentationAction(id: string): Promise<ActionState> {
|
||||
await requireTenant();
|
||||
const { tablesDB } = createAdminClient();
|
||||
|
||||
try {
|
||||
await tablesDB.deleteRow(DATABASE_ID, TABLES.presentations, id);
|
||||
} catch {
|
||||
return { ok: false, error: "Sunum silinemedi." };
|
||||
}
|
||||
|
||||
revalidatePath("/presentations");
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
export async function incrementPresentationViewCount(id: string, currentCount: number): Promise<void> {
|
||||
const { tablesDB } = createAdminClient();
|
||||
await tablesDB.updateRow(DATABASE_ID, TABLES.presentations, id, {
|
||||
viewCount: currentCount + 1,
|
||||
}).catch(() => {});
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
"use server";
|
||||
|
||||
import { ID, Permission, Role } from "node-appwrite";
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
import { propertySchema } from "@/lib/validation/properties";
|
||||
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[]> };
|
||||
|
||||
export async function createPropertyAction(
|
||||
_prev: ActionState,
|
||||
formData: FormData,
|
||||
): Promise<ActionState> {
|
||||
const ctx = await requireTenant();
|
||||
const raw = Object.fromEntries(formData.entries());
|
||||
const parsed = propertySchema.safeParse(raw);
|
||||
if (!parsed.success) {
|
||||
return { ok: false, fieldErrors: parsed.error.flatten().fieldErrors };
|
||||
}
|
||||
|
||||
const { tablesDB } = createAdminClient();
|
||||
const id = ID.unique();
|
||||
const data = parsed.data;
|
||||
|
||||
try {
|
||||
const row = await tablesDB.createRow(
|
||||
DATABASE_ID,
|
||||
TABLES.properties,
|
||||
id,
|
||||
{
|
||||
tenantId: ctx.tenantId,
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
propertyType: data.propertyType,
|
||||
listingType: data.listingType,
|
||||
status: data.status ?? "aktif",
|
||||
price: data.price,
|
||||
currency: data.currency ?? "TRY",
|
||||
roomCount: data.roomCount,
|
||||
grossM2: data.grossM2,
|
||||
netM2: data.netM2,
|
||||
floor: data.floor,
|
||||
totalFloors: data.totalFloors,
|
||||
buildingAge: data.buildingAge,
|
||||
city: data.city,
|
||||
district: data.district,
|
||||
neighborhood: data.neighborhood,
|
||||
address: data.address,
|
||||
mapLat: data.mapLat,
|
||||
mapLng: data.mapLng,
|
||||
featuresJson: data.featuresJson,
|
||||
imageIds: data.imageIds,
|
||||
createdBy: ctx.user.id,
|
||||
assigneeId: data.assigneeId,
|
||||
},
|
||||
[
|
||||
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 matchPropertyToSearches(row as unknown as Property, ctx.tenantId, ctx.user.id).catch(
|
||||
() => {},
|
||||
);
|
||||
} catch {
|
||||
return { ok: false, error: "İlan oluşturulamadı." };
|
||||
}
|
||||
|
||||
revalidatePath("/properties");
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
export async function updatePropertyAction(
|
||||
id: string,
|
||||
_prev: ActionState,
|
||||
formData: FormData,
|
||||
): Promise<ActionState> {
|
||||
const ctx = await requireTenant();
|
||||
const raw = Object.fromEntries(formData.entries());
|
||||
const parsed = propertySchema.safeParse(raw);
|
||||
if (!parsed.success) {
|
||||
return { ok: false, fieldErrors: parsed.error.flatten().fieldErrors };
|
||||
}
|
||||
|
||||
const { tablesDB } = createAdminClient();
|
||||
const data = parsed.data;
|
||||
|
||||
try {
|
||||
const row = await tablesDB.updateRow(DATABASE_ID, TABLES.properties, id, {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
propertyType: data.propertyType,
|
||||
listingType: data.listingType,
|
||||
status: data.status,
|
||||
price: data.price,
|
||||
currency: data.currency,
|
||||
roomCount: data.roomCount,
|
||||
grossM2: data.grossM2,
|
||||
netM2: data.netM2,
|
||||
floor: data.floor,
|
||||
totalFloors: data.totalFloors,
|
||||
buildingAge: data.buildingAge,
|
||||
city: data.city,
|
||||
district: data.district,
|
||||
neighborhood: data.neighborhood,
|
||||
address: data.address,
|
||||
mapLat: data.mapLat,
|
||||
mapLng: data.mapLng,
|
||||
featuresJson: data.featuresJson,
|
||||
imageIds: data.imageIds,
|
||||
assigneeId: data.assigneeId,
|
||||
});
|
||||
|
||||
await matchPropertyToSearches(row as unknown as Property, ctx.tenantId, ctx.user.id).catch(
|
||||
() => {},
|
||||
);
|
||||
} catch {
|
||||
return { ok: false, error: "İlan güncellenemedi." };
|
||||
}
|
||||
|
||||
revalidatePath("/properties");
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
export async function deletePropertyAction(id: string): Promise<ActionState> {
|
||||
await requireTenant();
|
||||
const { tablesDB } = createAdminClient();
|
||||
|
||||
try {
|
||||
await tablesDB.deleteRow(DATABASE_ID, TABLES.properties, id);
|
||||
} catch {
|
||||
return { ok: false, error: "İlan silinemedi." };
|
||||
}
|
||||
|
||||
revalidatePath("/properties");
|
||||
return { ok: true };
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import "server-only";
|
||||
|
||||
import { Query } from "node-appwrite";
|
||||
|
||||
import { DATABASE_ID, TABLES, type Property } from "./schema";
|
||||
import { createAdminClient } from "./server";
|
||||
|
||||
export async function listProperties(tenantId: string): Promise<Property[]> {
|
||||
const { tablesDB } = createAdminClient();
|
||||
const result = await tablesDB.listRows({
|
||||
databaseId: DATABASE_ID,
|
||||
tableId: TABLES.properties,
|
||||
queries: [Query.equal("tenantId", tenantId), Query.orderDesc("$createdAt"), Query.limit(200)],
|
||||
});
|
||||
return result.rows as unknown as Property[];
|
||||
}
|
||||
|
||||
export async function getProperty(id: string, tenantId: string): Promise<Property | null> {
|
||||
const { tablesDB } = createAdminClient();
|
||||
const row = (await tablesDB.getRow(DATABASE_ID, TABLES.properties, id)) as unknown as Property;
|
||||
if (row.tenantId !== tenantId) return null;
|
||||
return row;
|
||||
}
|
||||
Reference in New Issue
Block a user