Files
kovakemlak-crm/src/components/activities/activities-client.tsx
T
egecankomur 7c23a2b4ae feat: activity assignment + team view for owner/admin
db: activities.assigneeId column (string, optional)

activity-actions: assigneeId saved on create (default: self), cleared on update
validation: assigneeId added to activitySchema
schema: assigneeId field on Activity type

activity-form-sheet: Atanan Kişi dropdown for owner/admin with member list
activity-team-view: new component — activities grouped by assignee,
  completion/edit/delete actions, overdue indicator, member avatars
activities-client: Ekip tab (owner/admin only), members + currentUserId props
activities page: fetches team memberships + user details, passes to client
activity-email-actions: filter by assigneeId ?? createdBy for both me/team modes
2026-05-12 17:40:21 +03:00

280 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { DotsThree, Plus, PencilSimple, Trash, CheckCircle, List, CalendarDots, Users } 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, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
completeActivityAction,
deleteActivityAction,
} from "@/lib/appwrite/activity-actions";
import { ActivityFormSheet } from "./activity-form-sheet";
import { ActivityCalendar } from "./activity-calendar";
import { ActivityTeamView } from "./activity-team-view";
import { SendSummaryDialog } from "./send-summary-dialog";
import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog";
import type { Activity, Customer, Property } from "@/lib/appwrite/schema";
import { ACTIVITY_TYPE_LABELS } from "@/lib/appwrite/schema";
import type { TenantRole } from "@/lib/appwrite/tenant-guard";
import type { TeamMember } from "./activity-form-sheet";
type ViewMode = "list" | "calendar" | "team";
interface ActivitiesClientProps {
initialActivities: Activity[];
customers: Customer[];
properties: Property[];
role: TenantRole;
members: TeamMember[];
currentUserId: string;
}
export function ActivitiesClient({
initialActivities,
customers,
properties,
role,
members,
currentUserId,
}: ActivitiesClientProps) {
const router = useRouter();
const [activities, setActivities] = useState(initialActivities);
const [sheetOpen, setSheetOpen] = useState(false);
const [editing, setEditing] = useState<Activity | null>(null);
const [deleteTarget, setDeleteTarget] = useState<Activity | null>(null);
const [viewMode, setViewMode] = useState<ViewMode>("list");
function customerName(id?: string | null) {
if (!id) return "—";
return customers.find((c) => c.$id === id)?.name ?? "—";
}
function propertyTitle(id?: string | null) {
if (!id) return "—";
return properties.find((p) => p.$id === id)?.title ?? "—";
}
useEffect(() => {
const open = () => { setEditing(null); setSheetOpen(true); };
const close = () => setSheetOpen(false);
window.addEventListener("kovak:open-form-activities", open);
window.addEventListener("kovak:close-form-activities", close);
return () => {
window.removeEventListener("kovak:open-form-activities", open);
window.removeEventListener("kovak:close-form-activities", close);
};
}, []);
function openCreate() { setEditing(null); setSheetOpen(true); }
function openEdit(a: Activity) { setEditing(a); setSheetOpen(true); }
async function handleComplete(a: Activity) {
const result = await completeActivityAction(a.$id);
if (result.ok) {
setActivities((prev) =>
prev.map((x) => x.$id === a.$id ? { ...x, completedAt: new Date().toISOString() } : x),
);
toast.success("Aktivite tamamlandı.");
} else {
toast.error(result.error ?? "Tamamlanamadı.");
}
}
async function doDelete() {
if (!deleteTarget) return;
const result = await deleteActivityAction(deleteTarget.$id);
if (result.ok) {
setActivities((prev) => prev.filter((x) => x.$id !== deleteTarget.$id));
setDeleteTarget(null);
toast.success("Aktivite silindi.");
} else {
toast.error(result.error ?? "Silinemedi.");
}
}
return (
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between gap-3 flex-wrap">
<h1 className="text-2xl font-bold">Aktiviteler</h1>
<div className="flex items-center gap-2">
{/* View toggle */}
<div className="flex rounded-md border overflow-hidden">
<button
type="button"
onClick={() => setViewMode("list")}
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium transition-colors ${
viewMode === "list"
? "bg-primary text-primary-foreground"
: "bg-background text-muted-foreground hover:text-foreground"
}`}
>
<List className="size-3.5" />
Liste
</button>
<button
type="button"
onClick={() => setViewMode("calendar")}
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium transition-colors ${
viewMode === "calendar"
? "bg-primary text-primary-foreground"
: "bg-background text-muted-foreground hover:text-foreground"
}`}
>
<CalendarDots className="size-3.5" />
Takvim
</button>
{(role === "owner" || role === "admin") && (
<button
type="button"
onClick={() => setViewMode("team")}
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium transition-colors ${
viewMode === "team"
? "bg-primary text-primary-foreground"
: "bg-background text-muted-foreground hover:text-foreground"
}`}
>
<Users className="size-3.5" />
Ekip
</button>
)}
</div>
<SendSummaryDialog role={role} />
<Button onClick={openCreate} size="sm" data-tour="activities-add">
<Plus className="mr-1.5 size-4" />
Yeni Aktivite
</Button>
</div>
</div>
{/* Calendar view */}
{viewMode === "calendar" && (
<ActivityCalendar
activities={activities}
customers={customers}
properties={properties}
onEdit={openEdit}
onComplete={handleComplete}
/>
)}
{/* Team view — owner/admin only */}
{viewMode === "team" && (
<ActivityTeamView
activities={activities}
members={members}
currentUserId={currentUserId}
onEdit={openEdit}
onComplete={handleComplete}
onDelete={setDeleteTarget}
/>
)}
{/* List view */}
{viewMode === "list" && (
<div data-tour="activities-table" className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Tip</TableHead>
<TableHead>Başlık</TableHead>
<TableHead>Müşteri</TableHead>
<TableHead>İlan</TableHead>
<TableHead>Tarih</TableHead>
<TableHead>Durum</TableHead>
<TableHead />
</TableRow>
</TableHeader>
<TableBody>
{activities.length === 0 && (
<TableRow>
<TableCell colSpan={7} className="text-muted-foreground text-center py-10">
Henüz aktivite yok.
</TableCell>
</TableRow>
)}
{activities.map((a) => (
<TableRow key={a.$id}>
<TableCell>
<Badge variant="outline">
{ACTIVITY_TYPE_LABELS[a.type] ?? a.type}
</Badge>
</TableCell>
<TableCell className="font-medium max-w-[180px] truncate">{a.title}</TableCell>
<TableCell>{customerName(a.customerId)}</TableCell>
<TableCell className="max-w-[140px] truncate">{propertyTitle(a.propertyId)}</TableCell>
<TableCell className="text-muted-foreground">
{a.dueDate ? new Date(a.dueDate).toLocaleDateString("tr-TR") : "—"}
</TableCell>
<TableCell>
{a.completedAt ? (
<Badge variant="secondary">Tamamlandı</Badge>
) : (
<Badge>Açık</Badge>
)}
</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="size-8">
<DotsThree className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{!a.completedAt && (
<DropdownMenuItem onClick={() => handleComplete(a)}>
<CheckCircle className="mr-2 size-4" />
Tamamla
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={() => openEdit(a)}>
<PencilSimple className="mr-2 size-4" />
Düzenle
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setDeleteTarget(a)}
className="text-destructive focus:text-destructive"
>
<Trash className="mr-2 size-4" />
Sil
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
<ActivityFormSheet
open={sheetOpen}
onOpenChange={setSheetOpen}
activity={editing}
customers={customers}
properties={properties}
members={members}
role={role}
currentUserId={currentUserId}
onSuccess={() => router.refresh()}
/>
<DeleteConfirmDialog
open={!!deleteTarget}
onOpenChange={(v) => { if (!v) setDeleteTarget(null); }}
title="Bu aktivite silinsin mi?"
description="Bu aktivite kalıcı olarak silinecek ve geri alınamaz."
onConfirm={doDelete}
/>
</div>
);
}