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,109 @@
"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 { createInvestorAction, updateInvestorAction } from "@/lib/appwrite/investor-actions";
import type { Investor } from "@/lib/appwrite/schema";
type ActionState = { ok: boolean; error?: string; fieldErrors?: Record<string, string[]> };
const INITIAL: ActionState = { ok: false };
interface InvestorFormSheetProps {
open: boolean;
onOpenChange: (v: boolean) => void;
investor?: Investor | null;
onSuccess?: () => void;
}
export function InvestorFormSheet({ open, onOpenChange, investor, onSuccess }: InvestorFormSheetProps) {
const action = investor
? updateInvestorAction.bind(null, investor.$id)
: createInvestorAction;
const [state, formAction, isPending] = useActionState(action, INITIAL);
useEffect(() => {
if (state.ok) {
toast.success(investor ? "Yatırımcı güncellendi." : "Yatırımcı 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>{investor ? "Yatırımcıyı Düzenle" : "Yeni Yatırımcı"}</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={investor?.name} placeholder="Mehmet Demir" />
{fe.name && <p className="text-destructive text-xs">{fe.name[0]}</p>}
</div>
<div className="grid gap-1.5">
<Label htmlFor="email">Email *</Label>
<Input id="email" name="email" type="email" defaultValue={investor?.email} placeholder="mehmet@example.com" />
{fe.email && <p className="text-destructive text-xs">{fe.email[0]}</p>}
</div>
<div className="grid gap-1.5">
<Label htmlFor="phone">Telefon</Label>
<Input id="phone" name="phone" type="tel" defaultValue={investor?.phone ?? ""} placeholder="+90 555 123 45 67" />
</div>
<div className="grid grid-cols-2 gap-3">
<div className="grid gap-1.5">
<Label htmlFor="budget">Bütçe</Label>
<Input id="budget" name="budget" type="number" min="0" defaultValue={investor?.budget ?? ""} placeholder="5000000" />
</div>
<div className="grid gap-1.5">
<Label>Para birimi</Label>
<select
name="currency"
defaultValue={investor?.currency ?? "TRY"}
className="border-input bg-background h-9 rounded-md border px-3 text-sm"
>
<option value="TRY">TRY</option>
<option value="USD">USD</option>
<option value="EUR">EUR</option>
</select>
</div>
</div>
<div className="grid gap-1.5">
<Label htmlFor="notes">Notlar</Label>
<Textarea id="notes" name="notes" rows={3} defaultValue={investor?.notes ?? ""} placeholder="Yatırım tercihleri..." />
</div>
<SheetFooter>
<Button type="submit" disabled={isPending} className="w-full">
{isPending && <Loader2 className="mr-2 size-4 animate-spin" />}
{investor ? "Güncelle" : "Oluştur"}
</Button>
</SheetFooter>
</form>
</SheetContent>
</Sheet>
);
}