7c23a2b4ae
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
280 lines
10 KiB
TypeScript
280 lines
10 KiB
TypeScript
"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>
|
||
);
|
||
}
|