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
@@ -0,0 +1,101 @@
"use client";
import { useActionState, useEffect } from "react";
import { Loader2 } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { createCustomerAction, updateCustomerAction } from "@/lib/appwrite/customer-actions";
import type { Customer } from "@/lib/appwrite/schema";
type ActionState = { ok: boolean; error?: string; fieldErrors?: Record<string, string[]> };
const INITIAL: ActionState = { ok: false };
interface CustomerFormSheetProps {
open: boolean;
onOpenChange: (v: boolean) => void;
customer?: Customer | null;
onSuccess?: () => void;
}
export function CustomerFormSheet({ open, onOpenChange, customer, onSuccess }: CustomerFormSheetProps) {
const action = customer
? updateCustomerAction.bind(null, customer.$id)
: createCustomerAction;
const [state, formAction, isPending] = useActionState(action, INITIAL);
useEffect(() => {
if (state.ok) {
toast.success(customer ? "Müşteri güncellendi." : "Müşteri oluşturuldu.");
onSuccess?.();
onOpenChange(false);
} else if (state.error) {
toast.error(state.error);
}
}, [state]);
const fe = state.fieldErrors ?? {};
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent className="sm:max-w-md">
<SheetHeader>
<SheetTitle>{customer ? "Müşteriyi Düzenle" : "Yeni Müşteri"}</SheetTitle>
</SheetHeader>
<form action={formAction} className="mt-4 space-y-4 pb-6">
<div className="grid gap-1.5">
<Label htmlFor="name">Ad Soyad *</Label>
<Input id="name" name="name" defaultValue={customer?.name} placeholder="Ahmet Yılmaz" />
{fe.name && <p className="text-destructive text-xs">{fe.name[0]}</p>}
</div>
<div className="grid gap-1.5">
<Label>Müşteri tipi *</Label>
<select name="type" defaultValue={customer?.type ?? "alici"}
className="border-input bg-background h-9 rounded-md border px-3 text-sm">
<option value="alici">Alıcı</option>
<option value="kiraci">Kiracı</option>
<option value="yatirimci">Yatırımcı</option>
</select>
{fe.type && <p className="text-destructive text-xs">{fe.type[0]}</p>}
</div>
<div className="grid gap-1.5">
<Label htmlFor="phone">Telefon</Label>
<Input id="phone" name="phone" type="tel" defaultValue={customer?.phone ?? ""} placeholder="+90 555 123 45 67" />
</div>
<div className="grid gap-1.5">
<Label htmlFor="email">Email</Label>
<Input id="email" name="email" type="email" defaultValue={customer?.email ?? ""} placeholder="ahmet@example.com" />
{fe.email && <p className="text-destructive text-xs">{fe.email[0]}</p>}
</div>
<div className="grid gap-1.5">
<Label htmlFor="notes">Notlar</Label>
<Textarea id="notes" name="notes" rows={3} defaultValue={customer?.notes ?? ""} placeholder="Müşteri hakkında notlar..." />
</div>
<SheetFooter>
<Button type="submit" disabled={isPending} className="w-full">
{isPending && <Loader2 className="mr-2 size-4 animate-spin" />}
{customer ? "Güncelle" : "Oluştur"}
</Button>
</SheetFooter>
</form>
</SheetContent>
</Sheet>
);
}