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:
egecankomur
2026-05-05 12:03:48 +03:00
parent 2f17c342ca
commit 4ef0482732
34 changed files with 3174 additions and 36 deletions
+37 -4
View File
@@ -1,8 +1,41 @@
export default function Page() {
export const dynamic = "force-dynamic";
import { Query } from "node-appwrite";
import { requireTenant } from "@/lib/appwrite/tenant-guard";
import { listCustomers } from "@/lib/appwrite/customer-queries";
import { listProperties } from "@/lib/appwrite/property-queries";
import { DATABASE_ID, TABLES, type Activity } from "@/lib/appwrite/schema";
import { createAdminClient } from "@/lib/appwrite/server";
import { ActivitiesClient } from "@/components/activities/activities-client";
export default async function ActivitiesPage() {
const ctx = await requireTenant();
const { tablesDB } = createAdminClient();
const [customers, properties, activitiesResult] = await Promise.all([
listCustomers(ctx.tenantId),
listProperties(ctx.tenantId),
tablesDB.listRows({
databaseId: DATABASE_ID,
tableId: TABLES.activities,
queries: [
Query.equal("tenantId", ctx.tenantId),
Query.orderDesc("$createdAt"),
Query.limit(300),
],
}),
]);
const activities = activitiesResult.rows as unknown as Activity[];
return (
<div className="flex flex-1 flex-col gap-4 p-4">
<h1 className="text-2xl font-bold capitalize">activities</h1>
<p className="text-muted-foreground">Yakında...</p>
<div className="flex flex-1 flex-col gap-4 p-4 md:p-6">
<ActivitiesClient
initialActivities={activities}
customers={customers}
properties={properties}
/>
</div>
);
}
+77 -4
View File
@@ -1,8 +1,81 @@
export default function MatchesPage() {
export const dynamic = "force-dynamic";
import { Query } from "node-appwrite";
import { requireTenant } from "@/lib/appwrite/tenant-guard";
import { listCustomers } from "@/lib/appwrite/customer-queries";
import { listProperties } from "@/lib/appwrite/property-queries";
import { DATABASE_ID, TABLES, type PropertyMatch } from "@/lib/appwrite/schema";
import { createAdminClient } from "@/lib/appwrite/server";
export default async function MatchesPage() {
const ctx = await requireTenant();
const { tablesDB } = createAdminClient();
const [customers, properties, matchesResult] = await Promise.all([
listCustomers(ctx.tenantId),
listProperties(ctx.tenantId),
tablesDB.listRows({
databaseId: DATABASE_ID,
tableId: TABLES.propertyMatches,
queries: [
Query.equal("tenantId", ctx.tenantId),
Query.orderDesc("$createdAt"),
Query.limit(500),
],
}),
]);
const matches = matchesResult.rows as unknown as PropertyMatch[];
const customerMap = Object.fromEntries(customers.map((c) => [c.$id, c.name]));
const propertyMap = Object.fromEntries(properties.map((p) => [p.$id, p.title]));
return (
<div className="flex flex-1 flex-col gap-4 p-4">
<h1 className="text-2xl font-bold">Eşleşmeler</h1>
<p className="text-muted-foreground">Yakında...</p>
<div className="flex flex-1 flex-col gap-4 p-4 md:p-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold">Eşleşmeler</h1>
<span className="text-muted-foreground text-sm">{matches.length} eşleşme</span>
</div>
<div className="rounded-md border">
<table className="w-full text-sm">
<thead>
<tr className="border-b">
<th className="text-left p-3 font-medium">Müşteri</th>
<th className="text-left p-3 font-medium">İlan</th>
<th className="text-left p-3 font-medium">Tarih</th>
<th className="text-left p-3 font-medium">Görüntülendi</th>
</tr>
</thead>
<tbody>
{matches.length === 0 && (
<tr>
<td colSpan={4} className="text-muted-foreground text-center py-10">
Henüz eşleşme yok.
</td>
</tr>
)}
{matches.map((m) => (
<tr key={m.$id} className="border-b last:border-0 hover:bg-muted/30">
<td className="p-3">{customerMap[m.customerId] ?? m.customerId}</td>
<td className="p-3">{propertyMap[m.propertyId] ?? m.propertyId}</td>
<td className="p-3 text-muted-foreground">
{new Date(m.$createdAt).toLocaleDateString("tr-TR")}
</td>
<td className="p-3">
{m.viewedAt ? (
<span className="text-green-600 text-xs">
{new Date(m.viewedAt).toLocaleDateString("tr-TR")}
</span>
) : (
<span className="text-muted-foreground text-xs">Hayır</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
+12 -4
View File
@@ -1,8 +1,16 @@
export default function CustomersPage() {
export const dynamic = "force-dynamic";
import { requireTenant } from "@/lib/appwrite/tenant-guard";
import { listCustomers } from "@/lib/appwrite/customer-queries";
import { CustomersClient } from "@/components/customers/customers-client";
export default async function CustomersPage() {
const ctx = await requireTenant();
const customers = await listCustomers(ctx.tenantId);
return (
<div className="flex flex-1 flex-col gap-4 p-4">
<h1 className="text-2xl font-bold">Müşteriler</h1>
<p className="text-muted-foreground">Yakında...</p>
<div className="flex flex-1 flex-col gap-4 p-4 md:p-6">
<CustomersClient initialCustomers={customers} />
</div>
);
}
@@ -1,8 +1,35 @@
export default function SearchesPage() {
export const dynamic = "force-dynamic";
import { Query } from "node-appwrite";
import { requireTenant } from "@/lib/appwrite/tenant-guard";
import { listCustomers } from "@/lib/appwrite/customer-queries";
import { DATABASE_ID, TABLES, type CustomerSearch } from "@/lib/appwrite/schema";
import { createAdminClient } from "@/lib/appwrite/server";
import { SearchesClient } from "@/components/customers/searches-client";
export default async function SearchesPage() {
const ctx = await requireTenant();
const { tablesDB } = createAdminClient();
const [customers, searchesResult] = await Promise.all([
listCustomers(ctx.tenantId),
tablesDB.listRows({
databaseId: DATABASE_ID,
tableId: TABLES.customerSearches,
queries: [
Query.equal("tenantId", ctx.tenantId),
Query.orderDesc("$createdAt"),
Query.limit(500),
],
}),
]);
const searches = searchesResult.rows as unknown as CustomerSearch[];
return (
<div className="flex flex-1 flex-col gap-4 p-4">
<h1 className="text-2xl font-bold">Arama Kriterleri</h1>
<p className="text-muted-foreground">Yakında...</p>
<div className="flex flex-1 flex-col gap-4 p-4 md:p-6">
<SearchesClient initialSearches={searches} customers={customers} />
</div>
);
}
+27 -4
View File
@@ -1,8 +1,31 @@
export default function Page() {
export const dynamic = "force-dynamic";
import { Query } from "node-appwrite";
import { requireTenant } from "@/lib/appwrite/tenant-guard";
import { DATABASE_ID, TABLES, type Investor } from "@/lib/appwrite/schema";
import { createAdminClient } from "@/lib/appwrite/server";
import { InvestorsClient } from "@/components/investors/investors-client";
export default async function InvestorsPage() {
const ctx = await requireTenant();
const { tablesDB } = createAdminClient();
const result = await tablesDB.listRows({
databaseId: DATABASE_ID,
tableId: TABLES.investors,
queries: [
Query.equal("tenantId", ctx.tenantId),
Query.orderDesc("$createdAt"),
Query.limit(200),
],
});
const investors = result.rows as unknown as Investor[];
return (
<div className="flex flex-1 flex-col gap-4 p-4">
<h1 className="text-2xl font-bold capitalize">investors</h1>
<p className="text-muted-foreground">Yakında...</p>
<div className="flex flex-1 flex-col gap-4 p-4 md:p-6">
<InvestorsClient initialInvestors={investors} />
</div>
);
}
+37 -4
View File
@@ -1,8 +1,41 @@
export default function Page() {
export const dynamic = "force-dynamic";
import { Query } from "node-appwrite";
import { requireTenant } from "@/lib/appwrite/tenant-guard";
import { listCustomers } from "@/lib/appwrite/customer-queries";
import { listProperties } from "@/lib/appwrite/property-queries";
import { DATABASE_ID, TABLES, type Presentation } from "@/lib/appwrite/schema";
import { createAdminClient } from "@/lib/appwrite/server";
import { PresentationsClient } from "@/components/presentations/presentations-client";
export default async function PresentationsPage() {
const ctx = await requireTenant();
const { tablesDB } = createAdminClient();
const [customers, properties, presResult] = await Promise.all([
listCustomers(ctx.tenantId),
listProperties(ctx.tenantId),
tablesDB.listRows({
databaseId: DATABASE_ID,
tableId: TABLES.presentations,
queries: [
Query.equal("tenantId", ctx.tenantId),
Query.orderDesc("$createdAt"),
Query.limit(200),
],
}),
]);
const presentations = presResult.rows as unknown as Presentation[];
return (
<div className="flex flex-1 flex-col gap-4 p-4">
<h1 className="text-2xl font-bold capitalize">presentations</h1>
<p className="text-muted-foreground">Yakında...</p>
<div className="flex flex-1 flex-col gap-4 p-4 md:p-6">
<PresentationsClient
initialPresentations={presentations}
customers={customers}
properties={properties}
/>
</div>
);
}
+12 -4
View File
@@ -1,8 +1,16 @@
export default function Page() {
export const dynamic = "force-dynamic";
import { requireTenant } from "@/lib/appwrite/tenant-guard";
import { listProperties } from "@/lib/appwrite/property-queries";
import { PropertiesClient } from "@/components/properties/properties-client";
export default async function PropertiesPage() {
const ctx = await requireTenant();
const properties = await listProperties(ctx.tenantId);
return (
<div className="flex flex-1 flex-col gap-4 p-4">
<h1 className="text-2xl font-bold capitalize">properties</h1>
<p className="text-muted-foreground">Yakında...</p>
<div className="flex flex-1 flex-col gap-4 p-4 md:p-6">
<PropertiesClient initialProperties={properties} />
</div>
);
}