feat: desktop image thumbnails, gallery lightbox portal, client-side compression, clickable table rows, fix header gap
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useActionState, startTransition } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { ResponsiveSheet, FormWizard } from "@/components/ui/responsive-sheet";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { createDealAction, updateDealAction } from "@/lib/appwrite/deal-actions";
|
||||
import type { Customer, Deal } from "@/lib/appwrite/schema";
|
||||
|
||||
interface DealFormSheetProps {
|
||||
open: boolean;
|
||||
onOpenChange: (v: boolean) => void;
|
||||
deal?: Deal | null;
|
||||
customers: Customer[];
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const EMPTY_STATE: { ok: boolean; error?: string; fieldErrors?: Record<string, string[]> } = { ok: false };
|
||||
|
||||
export function DealFormSheet({ open, onOpenChange, deal, customers, onSuccess }: DealFormSheetProps) {
|
||||
const action = deal ? updateDealAction.bind(null, deal.$id) : createDealAction;
|
||||
const [state, dispatch, isPending] = useActionState(action, EMPTY_STATE);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.ok) {
|
||||
toast.success(deal ? "İşlem güncellendi." : "İşlem kaydedildi.");
|
||||
onSuccess();
|
||||
onOpenChange(false);
|
||||
} else if (state.error) {
|
||||
toast.error(state.error);
|
||||
}
|
||||
}, [state]);
|
||||
|
||||
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
const fd = new FormData(e.currentTarget);
|
||||
startTransition(() => dispatch(fd));
|
||||
}
|
||||
|
||||
const steps = [
|
||||
{
|
||||
label: "İşlem",
|
||||
content: (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="grid gap-1.5">
|
||||
<Label>Tip</Label>
|
||||
<Select name="type" defaultValue={deal?.type ?? "satis"} required>
|
||||
<SelectTrigger><SelectValue placeholder="Seç" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="satis">Satış</SelectItem>
|
||||
<SelectItem value="kiralama">Kiralama</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<Label>İlan Başlığı</Label>
|
||||
<Input name="propertyTitle" placeholder="İsteğe bağlı" defaultValue={deal?.propertyTitle ?? ""} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<Label>Müşteri</Label>
|
||||
<select
|
||||
name="customerId"
|
||||
defaultValue={deal?.customerId ?? ""}
|
||||
className="border-input bg-background h-9 rounded-md border px-3 text-sm"
|
||||
>
|
||||
<option value="">— Seçiniz (opsiyonel)</option>
|
||||
{customers.map((c) => (
|
||||
<option key={c.$id} value={c.$id}>{c.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<Label>Satış / Kira Bedeli (₺)</Label>
|
||||
<Input name="salePrice" type="number" min={0} step="any" required placeholder="0"
|
||||
defaultValue={deal?.salePrice ?? ""} />
|
||||
{state.fieldErrors?.salePrice && (
|
||||
<p className="text-destructive text-xs">{state.fieldErrors.salePrice[0]}</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Komisyon",
|
||||
content: (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="grid gap-1.5">
|
||||
<Label>Oran (%)</Label>
|
||||
<Input name="commissionRate" type="number" min={0} max={100} step="any" required placeholder="3"
|
||||
defaultValue={deal?.commissionRate ?? ""} />
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<Label>Tutar (₺)</Label>
|
||||
<Input name="commissionAmount" type="number" min={0} step="any" required placeholder="0"
|
||||
defaultValue={deal?.commissionAmount ?? ""} />
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<Label>Ofis payı (%)</Label>
|
||||
<Input name="officeSharePercent" type="number" min={0} max={100} step="any" placeholder="50"
|
||||
defaultValue={deal?.officeSharePercent ?? ""} />
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<Label>Danışman payı (%)</Label>
|
||||
<Input name="agentSharePercent" type="number" min={0} max={100} step="any" placeholder="50"
|
||||
defaultValue={deal?.agentSharePercent ?? ""} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4">
|
||||
<p className="text-xs font-medium text-muted-foreground mb-3">Referans / Ortak Danışman</p>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="grid gap-1.5 col-span-2">
|
||||
<Label>Ad Soyad</Label>
|
||||
<Input name="referralName" placeholder="Ahmet Yılmaz (opsiyonel)"
|
||||
defaultValue={deal?.referralName ?? ""} />
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<Label>Telefon</Label>
|
||||
<Input name="referralPhone" placeholder="05xx xxx xx xx"
|
||||
defaultValue={deal?.referralPhone ?? ""} />
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<Label>Komisyondan pay (%)</Label>
|
||||
<Input name="referralPercent" type="number" min={0} max={100} step="any"
|
||||
placeholder="örn. 25"
|
||||
defaultValue={deal?.referralPercent ?? ""} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Kapanış",
|
||||
content: (
|
||||
<>
|
||||
<div className="grid gap-1.5">
|
||||
<Label>Kapanış Tarihi</Label>
|
||||
<Input name="closingDate" type="date"
|
||||
defaultValue={deal?.closingDate ? deal.closingDate.slice(0, 10) : ""} />
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<Label>Notlar</Label>
|
||||
<Textarea name="notes" rows={4} placeholder="Opsiyonel notlar..." defaultValue={deal?.notes ?? ""} />
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ResponsiveSheet open={open} onOpenChange={onOpenChange}
|
||||
title={deal ? "İşlemi Düzenle" : "Yeni İşlem"}
|
||||
maxWidth="sm:max-w-lg">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<FormWizard steps={steps} isPending={isPending} submitLabel={deal ? "Güncelle" : "Kaydet"} />
|
||||
</form>
|
||||
</ResponsiveSheet>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { DotsThree, Plus, PencilSimple, Trash, CheckCircle, XCircle, TrendUp, Wallet, Users, Clock, UserCheck } from '@/lib/icons';
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
DropdownMenu, DropdownMenuContent, DropdownMenuItem,
|
||||
DropdownMenuSeparator, DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog";
|
||||
import { DealFormSheet } from "./deal-form-sheet";
|
||||
import {
|
||||
updateDealStatusAction,
|
||||
deleteDealAction,
|
||||
} from "@/lib/appwrite/deal-actions";
|
||||
import type { Deal, DealStatus, Customer } from "@/lib/appwrite/schema";
|
||||
import {
|
||||
DEAL_TYPE_LABELS,
|
||||
DEAL_STATUS_LABELS,
|
||||
} from "@/lib/appwrite/schema";
|
||||
import type { TenantRole } from "@/lib/appwrite/tenant-guard";
|
||||
|
||||
interface FinanceClientProps {
|
||||
initialDeals: Deal[];
|
||||
role: TenantRole;
|
||||
userId: string;
|
||||
userName: string;
|
||||
customers: Customer[];
|
||||
}
|
||||
|
||||
const STATUS_FILTER_OPTIONS = [
|
||||
["all", "Tümü"],
|
||||
["bekleyen", "Bekleyen"],
|
||||
["tahsil_edildi", "Tahsil Edildi"],
|
||||
["iptal", "İptal"],
|
||||
] as const;
|
||||
|
||||
function fmt(n: number) {
|
||||
return n.toLocaleString("tr-TR", { maximumFractionDigits: 0 });
|
||||
}
|
||||
|
||||
export function FinanceClient({ initialDeals, role, userId, customers }: FinanceClientProps) {
|
||||
const router = useRouter();
|
||||
const [deals, setDeals] = useState<Deal[]>(initialDeals);
|
||||
const [sheetOpen, setSheetOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<Deal | null>(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState<Deal | null>(null);
|
||||
const [statusFilter, setStatusFilter] = useState<DealStatus | "all">("all");
|
||||
|
||||
const isOwnerOrAdmin = role === "owner" || role === "admin";
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (statusFilter === "all") return deals;
|
||||
return deals.filter((d) => (d.status ?? "bekleyen") === statusFilter);
|
||||
}, [deals, statusFilter]);
|
||||
|
||||
// ---- Stats ----
|
||||
const stats = useMemo(() => {
|
||||
const collected = deals.filter((d) => d.status === "tahsil_edildi");
|
||||
const pending = deals.filter((d) => (d.status ?? "bekleyen") === "bekleyen");
|
||||
|
||||
const totalCommission = collected.reduce((s, d) => s + d.commissionAmount, 0);
|
||||
const pendingCommission = pending.reduce((s, d) => s + d.commissionAmount, 0);
|
||||
|
||||
const agentEarnings = collected.reduce(
|
||||
(s, d) =>
|
||||
s + (d.agentSharePercent != null
|
||||
? (d.commissionAmount * d.agentSharePercent) / 100
|
||||
: d.commissionAmount),
|
||||
0,
|
||||
);
|
||||
const officeEarnings = collected.reduce(
|
||||
(s, d) =>
|
||||
s + (d.officeSharePercent != null
|
||||
? (d.commissionAmount * d.officeSharePercent) / 100
|
||||
: 0),
|
||||
0,
|
||||
);
|
||||
const referralPaid = collected.reduce(
|
||||
(s, d) =>
|
||||
s + (d.referralPercent != null
|
||||
? (d.commissionAmount * d.referralPercent) / 100
|
||||
: 0),
|
||||
0,
|
||||
);
|
||||
|
||||
// Group by agent for leaderboard (owner/admin only)
|
||||
const byAgent: Record<string, { name: string; count: number; earnings: number }> = {};
|
||||
collected.forEach((d) => {
|
||||
if (!byAgent[d.agentId]) byAgent[d.agentId] = { name: d.agentName, count: 0, earnings: 0 };
|
||||
byAgent[d.agentId].count++;
|
||||
byAgent[d.agentId].earnings +=
|
||||
d.agentSharePercent != null
|
||||
? (d.commissionAmount * d.agentSharePercent) / 100
|
||||
: d.commissionAmount;
|
||||
});
|
||||
const leaderboard = Object.entries(byAgent)
|
||||
.map(([id, v]) => ({ id, ...v }))
|
||||
.sort((a, b) => b.earnings - a.earnings);
|
||||
|
||||
return { totalCommission, pendingCommission, agentEarnings, officeEarnings, referralPaid, leaderboard, pendingCount: pending.length };
|
||||
}, [deals]);
|
||||
|
||||
useEffect(() => {
|
||||
const open = () => { setEditing(null); setSheetOpen(true); };
|
||||
const close = () => setSheetOpen(false);
|
||||
window.addEventListener("kovak:open-form-finance", open);
|
||||
window.addEventListener("kovak:close-form-finance", close);
|
||||
return () => {
|
||||
window.removeEventListener("kovak:open-form-finance", open);
|
||||
window.removeEventListener("kovak:close-form-finance", close);
|
||||
};
|
||||
}, []);
|
||||
|
||||
function openCreate() { setEditing(null); setSheetOpen(true); }
|
||||
function openEdit(d: Deal) { setEditing(d); setSheetOpen(true); }
|
||||
|
||||
async function handleStatusChange(deal: Deal, status: DealStatus) {
|
||||
setDeals((prev) =>
|
||||
prev.map((d) => (d.$id === deal.$id ? { ...d, status } : d)),
|
||||
);
|
||||
const result = await updateDealStatusAction(deal.$id, status);
|
||||
if (!result.ok) {
|
||||
setDeals((prev) =>
|
||||
prev.map((d) => (d.$id === deal.$id ? deal : d)),
|
||||
);
|
||||
toast.error(result.error ?? "Durum güncellenemedi.");
|
||||
}
|
||||
}
|
||||
|
||||
async function doDelete() {
|
||||
if (!deleteTarget) return;
|
||||
const result = await deleteDealAction(deleteTarget.$id);
|
||||
if (result.ok) {
|
||||
setDeals((prev) => prev.filter((d) => d.$id !== deleteTarget.$id));
|
||||
setDeleteTarget(null);
|
||||
toast.success("İşlem silindi.");
|
||||
} else {
|
||||
toast.error(result.error ?? "Silinemedi.");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 flex-1">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between gap-3 flex-wrap">
|
||||
<h1 className="text-2xl font-bold">Finans</h1>
|
||||
<Button onClick={openCreate} size="sm" data-tour="finance-add">
|
||||
<Plus className="mr-1.5 size-4" />
|
||||
Yeni İşlem
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats cards */}
|
||||
{isOwnerOrAdmin ? (
|
||||
<div data-tour="finance-stats" className="grid grid-cols-2 gap-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||
<StatCard
|
||||
icon={TrendUp}
|
||||
label="Toplam Komisyon"
|
||||
value={`₺${fmt(stats.totalCommission)}`}
|
||||
sub="tahsil edilen"
|
||||
color="text-emerald-600"
|
||||
/>
|
||||
<StatCard
|
||||
icon={Clock}
|
||||
label="Bekleyen Komisyon"
|
||||
value={`₺${fmt(stats.pendingCommission)}`}
|
||||
sub={`${stats.pendingCount} işlem`}
|
||||
color="text-amber-600"
|
||||
/>
|
||||
<StatCard
|
||||
icon={Wallet}
|
||||
label="Ofis Geliri"
|
||||
value={`₺${fmt(stats.officeEarnings)}`}
|
||||
sub="tahsil edilen paydan"
|
||||
color="text-blue-600"
|
||||
/>
|
||||
<StatCard
|
||||
icon={Users}
|
||||
label="Danışman Geliri"
|
||||
value={`₺${fmt(stats.agentEarnings)}`}
|
||||
sub="tahsil edilen paydan"
|
||||
color="text-purple-600"
|
||||
/>
|
||||
{stats.referralPaid > 0 && (
|
||||
<StatCard
|
||||
icon={UserCheck}
|
||||
label="Referans Ödemesi"
|
||||
value={`₺${fmt(stats.referralPaid)}`}
|
||||
sub="tahsil edilenden"
|
||||
color="text-rose-600"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-3">
|
||||
<StatCard
|
||||
icon={TrendUp}
|
||||
label="Kazancım"
|
||||
value={`₺${fmt(stats.agentEarnings)}`}
|
||||
sub="tahsil edilen"
|
||||
color="text-emerald-600"
|
||||
/>
|
||||
<StatCard
|
||||
icon={Clock}
|
||||
label="Bekleyen"
|
||||
value={`₺${fmt(stats.pendingCommission)}`}
|
||||
sub={`${stats.pendingCount} işlem`}
|
||||
color="text-amber-600"
|
||||
/>
|
||||
<StatCard
|
||||
icon={Wallet}
|
||||
label="Toplam İşlem"
|
||||
value={String(deals.length)}
|
||||
sub="kayıtlı"
|
||||
color="text-blue-600"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Leaderboard (owner/admin only) */}
|
||||
{isOwnerOrAdmin && stats.leaderboard.length > 0 && (
|
||||
<div className="rounded-lg border p-4">
|
||||
<p className="text-sm font-semibold mb-3">Danışman Performansı</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
{stats.leaderboard.map((agent, i) => (
|
||||
<div key={agent.id} className="flex items-center gap-3">
|
||||
<span className="text-muted-foreground text-xs w-4">{i + 1}.</span>
|
||||
<span className="text-sm flex-1 truncate">{agent.name}</span>
|
||||
<span className="text-xs text-muted-foreground">{agent.count} işlem</span>
|
||||
<span className="text-sm font-medium tabular-nums">₺{fmt(agent.earnings)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status filter */}
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{STATUS_FILTER_OPTIONS.map(([key, label]) => {
|
||||
const count = key === "all" ? deals.length : deals.filter((d) => (d.status ?? "bekleyen") === key).length;
|
||||
if (key !== "all" && count === 0) return null;
|
||||
return (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
onClick={() => setStatusFilter(key as DealStatus | "all")}
|
||||
className={`px-3 py-1 text-xs rounded-full font-medium transition-colors ${
|
||||
statusFilter === key
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
{label} <span className="opacity-70">{count}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div data-tour="finance-table" className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>İlan / Müşteri</TableHead>
|
||||
<TableHead>Tip</TableHead>
|
||||
{isOwnerOrAdmin && <TableHead>Danışman</TableHead>}
|
||||
<TableHead>Satış Bedeli</TableHead>
|
||||
<TableHead>Komisyon</TableHead>
|
||||
<TableHead>Danışman Payı</TableHead>
|
||||
<TableHead>Durum</TableHead>
|
||||
<TableHead>Tarih</TableHead>
|
||||
<TableHead />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filtered.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={isOwnerOrAdmin ? 9 : 8}
|
||||
className="text-muted-foreground text-center py-10"
|
||||
>
|
||||
{deals.length === 0 ? "Henüz işlem yok." : "Filtreye uyan işlem yok."}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{filtered.map((deal) => {
|
||||
const agentEarning =
|
||||
deal.agentSharePercent != null
|
||||
? (deal.commissionAmount * deal.agentSharePercent) / 100
|
||||
: deal.commissionAmount;
|
||||
const canEdit = isOwnerOrAdmin || deal.agentId === userId;
|
||||
|
||||
return (
|
||||
<TableRow key={deal.$id} className={deal.status === "iptal" ? "opacity-50" : ""}>
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium text-sm truncate max-w-[180px]">
|
||||
{deal.propertyTitle ?? "—"}
|
||||
</span>
|
||||
{deal.customerName && (
|
||||
<span className="text-muted-foreground text-xs">{deal.customerName}</span>
|
||||
)}
|
||||
{deal.referralName && (
|
||||
<span className="text-xs text-rose-600 flex items-center gap-0.5 mt-0.5">
|
||||
<UserCheck className="size-3 shrink-0" />
|
||||
{deal.referralName}
|
||||
{deal.referralPercent != null && ` (%${deal.referralPercent})`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{DEAL_TYPE_LABELS[deal.type]}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
{isOwnerOrAdmin && (
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
{deal.agentName}
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell className="tabular-nums text-sm">
|
||||
₺{fmt(deal.salePrice)}
|
||||
</TableCell>
|
||||
<TableCell className="tabular-nums text-sm">
|
||||
₺{fmt(deal.commissionAmount)}
|
||||
<span className="text-muted-foreground text-xs ml-1">
|
||||
(%{deal.commissionRate})
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="tabular-nums text-sm font-medium">
|
||||
₺{fmt(agentEarning)}
|
||||
{deal.agentSharePercent != null && (
|
||||
<span className="text-muted-foreground text-xs ml-1">
|
||||
(%{deal.agentSharePercent})
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<DealStatusBadge status={deal.status} />
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground text-sm">
|
||||
{deal.closingDate
|
||||
? new Date(deal.closingDate).toLocaleDateString("tr-TR")
|
||||
: "—"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="size-8">
|
||||
<DotsThree className="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{canEdit && (
|
||||
<DropdownMenuItem onClick={() => openEdit(deal)}>
|
||||
<PencilSimple className="mr-2 size-4" />
|
||||
Düzenle
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isOwnerOrAdmin && deal.status !== "tahsil_edildi" && (
|
||||
<DropdownMenuItem onClick={() => handleStatusChange(deal, "tahsil_edildi")}>
|
||||
<CheckCircle className="mr-2 size-4 text-emerald-600" />
|
||||
Tahsil Edildi
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isOwnerOrAdmin && deal.status !== "iptal" && (
|
||||
<DropdownMenuItem onClick={() => handleStatusChange(deal, "iptal")}>
|
||||
<XCircle className="mr-2 size-4 text-destructive" />
|
||||
İptal Et
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isOwnerOrAdmin && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => setDeleteTarget(deal)}
|
||||
className="text-destructive focus:text-destructive"
|
||||
>
|
||||
<Trash className="mr-2 size-4" />
|
||||
Sil
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<DealFormSheet
|
||||
open={sheetOpen}
|
||||
onOpenChange={setSheetOpen}
|
||||
deal={editing}
|
||||
customers={customers}
|
||||
onSuccess={() => router.refresh()}
|
||||
/>
|
||||
<DeleteConfirmDialog
|
||||
open={!!deleteTarget}
|
||||
onOpenChange={(v) => { if (!v) setDeleteTarget(null); }}
|
||||
title={`Bu işlem silinsin mi?`}
|
||||
description="İşlem kalıcı olarak silinecek ve geri alınamaz."
|
||||
onConfirm={doDelete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatCard({
|
||||
icon: Icon,
|
||||
label,
|
||||
value,
|
||||
sub,
|
||||
color,
|
||||
}: {
|
||||
icon: React.ElementType;
|
||||
label: string;
|
||||
value: string;
|
||||
sub: string;
|
||||
color: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-lg border bg-card p-4 flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className={`size-4 ${color}`} />
|
||||
<span className="text-muted-foreground text-xs">{label}</span>
|
||||
</div>
|
||||
<p className="text-xl font-bold tabular-nums">{value}</p>
|
||||
<p className="text-muted-foreground text-xs">{sub}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DealStatusBadge({ status }: { status?: string | null }) {
|
||||
const map: Record<string, string> = {
|
||||
bekleyen: "bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300",
|
||||
tahsil_edildi: "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300",
|
||||
iptal: "bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300",
|
||||
};
|
||||
const key = status ?? "bekleyen";
|
||||
const label = DEAL_STATUS_LABELS[key as keyof typeof DEAL_STATUS_LABELS] ?? key;
|
||||
return (
|
||||
<span className={`inline-flex text-xs font-medium px-2 py-0.5 rounded-full ${map[key] ?? map.bekleyen}`}>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Building2, User } from "lucide-react";
|
||||
import { Buildings, User } from '@/lib/icons';
|
||||
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -36,7 +36,7 @@ export function ScopeToggle({
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2 text-sm font-medium">
|
||||
<Building2 className="size-4" />
|
||||
<Buildings className="size-4" />
|
||||
Şirket
|
||||
</div>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
|
||||
Reference in New Issue
Block a user