From 7ee310415812511c10ddac0914a9ff701175ba10 Mon Sep 17 00:00:00 2001 From: Poyraz Avsever Date: Thu, 4 Jun 2026 19:53:44 +0300 Subject: [PATCH] feat(calendar): implement calendar event management with create, update, and delete functionalities - Added actions for creating, updating, and deleting calendar events in `actions.ts`. - Introduced `CalendarClient` component to handle event display and interaction in `calendar-client.tsx`. - Refactored `CalendarPage` to fetch events, clients, projects, and tasks from Supabase and pass them to `CalendarClient`. - Enhanced form handling for event creation and editing with proper validation and error handling. --- app/(dashboard)/calendar/actions.ts | 106 +++++ app/(dashboard)/calendar/calendar-client.tsx | 456 +++++++++++++++++++ app/(dashboard)/calendar/page.tsx | 430 ++++------------- 3 files changed, 661 insertions(+), 331 deletions(-) create mode 100644 app/(dashboard)/calendar/actions.ts create mode 100644 app/(dashboard)/calendar/calendar-client.tsx diff --git a/app/(dashboard)/calendar/actions.ts b/app/(dashboard)/calendar/actions.ts new file mode 100644 index 0000000..ef6cc5c --- /dev/null +++ b/app/(dashboard)/calendar/actions.ts @@ -0,0 +1,106 @@ +"use server"; + +import { createClient } from "@/lib/supabase/server"; +import { revalidatePath } from "next/cache"; + +const EVENT_TYPES = ["meeting", "focus", "deadline", "personal", "finance"] as const; + +function cleanText(value: FormDataEntryValue | null) { + const text = typeof value === "string" ? value.trim() : ""; + return text.length > 0 && text !== "__none" ? text : null; +} + +function readType(value: FormDataEntryValue | null) { + const type = typeof value === "string" ? value : "focus"; + return EVENT_TYPES.includes(type as (typeof EVENT_TYPES)[number]) ? type : "focus"; +} + +async function getCurrentUserId() { + const supabase = await createClient(); + const { + data: { user }, + error, + } = await supabase.auth.getUser(); + + if (error || !user) { + throw new Error("Takvim işlemi için giriş yapmış kullanıcı bulunamadı."); + } + + return { supabase, userId: user.id }; +} + +function readPayload(formData: FormData) { + return { + title: cleanText(formData.get("title")), + description: cleanText(formData.get("description")), + type: readType(formData.get("type")), + starts_at: cleanText(formData.get("starts_at")), + ends_at: cleanText(formData.get("ends_at")), + client_id: cleanText(formData.get("client_id")), + project_id: cleanText(formData.get("project_id")), + task_id: cleanText(formData.get("task_id")), + }; +} + +export async function createCalendarEventRecord(formData: FormData) { + const { supabase, userId } = await getCurrentUserId(); + const payload = readPayload(formData); + + if (!payload.title || !payload.starts_at) { + throw new Error("Etkinlik başlığı ve başlangıç zamanı zorunludur."); + } + + const { error } = await supabase.from("calendar_events").insert({ + user_id: userId, + ...payload, + }); + + if (error) { + throw new Error(`Etkinlik eklenemedi: ${error.message}`); + } + + revalidatePath("/calendar"); +} + +export async function updateCalendarEventRecord(formData: FormData) { + const { supabase, userId } = await getCurrentUserId(); + const id = cleanText(formData.get("id")); + const payload = readPayload(formData); + + if (!id || !payload.title || !payload.starts_at) { + throw new Error("Etkinlik güncellemek için başlık, başlangıç ve kayıt kimliği zorunludur."); + } + + const { error } = await supabase + .from("calendar_events") + .update(payload) + .eq("id", id) + .eq("user_id", userId); + + if (error) { + throw new Error(`Etkinlik güncellenemedi: ${error.message}`); + } + + revalidatePath("/calendar"); +} + +export async function deleteCalendarEventRecord(formData: FormData) { + const { supabase, userId } = await getCurrentUserId(); + const id = cleanText(formData.get("id")); + + if (!id) { + throw new Error("Silinecek etkinlik bulunamadı."); + } + + const { error } = await supabase + .from("calendar_events") + .delete() + .eq("id", id) + .eq("user_id", userId); + + if (error) { + throw new Error(`Etkinlik silinemedi: ${error.message}`); + } + + revalidatePath("/calendar"); +} diff --git a/app/(dashboard)/calendar/calendar-client.tsx b/app/(dashboard)/calendar/calendar-client.tsx new file mode 100644 index 0000000..432fb94 --- /dev/null +++ b/app/(dashboard)/calendar/calendar-client.tsx @@ -0,0 +1,456 @@ +"use client"; + +import { + createCalendarEventRecord, + deleteCalendarEventRecord, + updateCalendarEventRecord, +} from "@/app/(dashboard)/calendar/actions"; +import { Badge, Button, Card, CardContent, Input, Label, Textarea } from "poyraz-ui/atoms"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "poyraz-ui/molecules"; +import { CalendarDays, Clock, Pencil, Plus, Trash2 } from "lucide-react"; +import { useMemo, useState } from "react"; + +export type CalendarRelationOption = { + id: string; + name: string; +}; + +export type CalendarTaskOption = { + id: string; + title: string; +}; + +export type CalendarEventItem = { + id: string; + title: string; + description: string | null; + type: "meeting" | "focus" | "deadline" | "personal" | "finance"; + starts_at: string; + ends_at: string | null; + client_id: string | null; + project_id: string | null; + task_id: string | null; + clientName: string | null; + projectName: string | null; + taskTitle: string | null; +}; + +const typeLabels = { + meeting: "Toplantı", + focus: "Odak", + deadline: "Deadline", + personal: "Kişisel", + finance: "Finans", +}; + +const typeClasses = { + meeting: "border-blue-200 bg-blue-50 text-blue-700", + focus: "border-emerald-200 bg-emerald-50 text-emerald-700", + deadline: "border-rose-200 bg-rose-50 text-rose-700", + personal: "border-amber-200 bg-amber-50 text-amber-700", + finance: "border-primary/20 bg-primary/10 text-primary", +}; + +type CalendarClientProps = { + events: CalendarEventItem[]; + clients: CalendarRelationOption[]; + projects: CalendarRelationOption[]; + tasks: CalendarTaskOption[]; +}; + +export function CalendarClient({ events, clients, projects, tasks }: CalendarClientProps) { + const [monthDate, setMonthDate] = useState(() => new Date()); + const [selectedDate, setSelectedDate] = useState(() => toDateKey(new Date())); + const days = useMemo(() => buildMonthDays(monthDate), [monthDate]); + const eventsByDay = useMemo(() => groupEventsByDay(events), [events]); + const selectedEvents = eventsByDay.get(selectedDate) || []; + const upcomingEvents = events + .filter((event) => new Date(event.starts_at) >= startOfToday()) + .slice(0, 6); + + function shiftMonth(amount: number) { + setMonthDate((current) => new Date(current.getFullYear(), current.getMonth() + amount, 1)); + } + + return ( +
+
+
+
+ + Planlama +
+
+

Takvim

+

+ Toplantı, odak bloğu, deadline, kişisel ve finans etkinliklerini yönet. +

+
+
+ + +
+ +
+ + +
+
+

+ {formatMonth(monthDate)} +

+

{events.length} etkinlik

+
+
+ + + +
+
+ +
+ {["Pzt", "Sal", "Çar", "Per", "Cum", "Cmt", "Paz"].map((day) => ( +
+ {day} +
+ ))} +
+
+ {days.map((day) => { + const dayEvents = eventsByDay.get(day.key) || []; + const isSelected = selectedDate === day.key; + + return ( + + ); + })} +
+
+
+ +
+ + +
+

+ {formatDateLabel(selectedDate)} +

+

+ {selectedEvents.length} etkinlik +

+
+ + +
+
+ + + +

Yaklaşan etkinlikler

+ +
+
+
+
+
+ ); +} + +function EventList({ + events, + clients, + projects, + tasks, + compact = false, +}: { + events: CalendarEventItem[]; + clients: CalendarRelationOption[]; + projects: CalendarRelationOption[]; + tasks: CalendarTaskOption[]; + compact?: boolean; +}) { + if (events.length === 0) { + return

Etkinlik yok.

; + } + + return ( +
+ {events.map((event) => ( +
+
+
+
{event.title}
+
+ + {formatTimeRange(event)} +
+ {!compact ? ( +
+ {event.projectName || event.clientName || event.taskTitle || event.description || "Bağlantı yok"} +
+ ) : null} +
+ {typeLabels[event.type]} +
+ {!compact ? ( +
+ +
+ + +
+
+ ) : null} +
+ ))} +
+ ); +} + +function CalendarEventDialog({ + mode, + event, + defaultDate, + clients, + projects, + tasks, +}: { + mode: "create" | "edit"; + event?: CalendarEventItem; + defaultDate?: string; + clients: CalendarRelationOption[]; + projects: CalendarRelationOption[]; + tasks: CalendarTaskOption[]; +}) { + const [open, setOpen] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const action = mode === "create" ? createCalendarEventRecord : updateCalendarEventRecord; + + async function handleSubmit(formData: FormData) { + setIsSubmitting(true); + try { + await action(formData); + setOpen(false); + } finally { + setIsSubmitting(false); + } + } + + return ( + + + + + +
+ {event ? : null} + + {mode === "create" ? "Yeni etkinlik" : "Etkinliği düzenle"} + Takvim etkinliğini proje, görev veya müşteriyle ilişkilendir. + + +
+ +
+ + + + +
+
+
+ ); +} + +function EventFormFields({ + event, + defaultDate, + clients, + projects, + tasks, +}: { + event?: CalendarEventItem; + defaultDate?: string; + clients: CalendarRelationOption[]; + projects: CalendarRelationOption[]; + tasks: CalendarTaskOption[]; +}) { + const startsAt = event?.starts_at ? toDateTimeLocal(event.starts_at) : `${defaultDate || toDateKey(new Date())}T09:00`; + + return ( +
+
+ + +
+
+ +