feat(i18n): update analytics and calendar views for dynamic locales

This commit is contained in:
poyrazavsever
2026-07-20 11:08:38 +03:00
parent 542cb66759
commit 5210a180d4
5 changed files with 158 additions and 57 deletions
+23 -12
View File
@@ -1,6 +1,8 @@
"use client"; "use client";
import { useRouter, usePathname, useSearchParams } from "next/navigation"; import { useRouter, usePathname, useSearchParams } from "next/navigation";
import { useTranslations } from "@/components/i18n/i18n-provider";
import { getDocumentIntlLocale } from "@/lib/i18n/browser";
import { Card, CardContent } from "poyraz-ui/atoms"; import { Card, CardContent } from "poyraz-ui/atoms";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "poyraz-ui/molecules"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "poyraz-ui/molecules";
import { import {
@@ -24,6 +26,7 @@ type AnalyticsClientProps = {
const COLORS = ["var(--poyraz-primary)", "var(--poyraz-destructive)", "#eab308", "#3b82f6", "#8b5cf6"]; const COLORS = ["var(--poyraz-primary)", "var(--poyraz-destructive)", "#eab308", "#3b82f6", "#8b5cf6"];
export function AnalyticsClient({ data }: AnalyticsClientProps) { export function AnalyticsClient({ data }: AnalyticsClientProps) {
const t = useTranslations();
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
@@ -37,28 +40,36 @@ export function AnalyticsClient({ data }: AnalyticsClientProps) {
const { projectIncomeData, completedTasks, activeTasks } = data.metrics; const { projectIncomeData, completedTasks, activeTasks } = data.metrics;
const taskStatusData = [ const taskStatusData = [
{ name: "Tamamlanan", value: completedTasks }, { name: t("analytics.tasks.completed"), value: completedTasks },
{ name: "Devam Eden", value: activeTasks } { name: t("analytics.tasks.ongoing"), value: activeTasks }
]; ];
const formatCurrency = (val: number) => {
return new Intl.NumberFormat(getDocumentIntlLocale(), {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
}).format(val);
};
return ( return (
<div className="mx-auto flex max-w-7xl flex-col gap-6"> <div className="mx-auto flex max-w-7xl flex-col gap-6">
<div className="flex flex-col gap-4 border-b border-border pb-5 lg:flex-row lg:items-end lg:justify-between"> <div className="flex flex-col gap-4 border-b border-border pb-5 lg:flex-row lg:items-end lg:justify-between">
<div> <div>
<h1 className="text-3xl font-semibold tracking-normal text-foreground"> <h1 className="text-3xl font-semibold tracking-normal text-foreground">
Performans ve Finans Analizi {t("analytics.title")}
</h1> </h1>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Select value={data.range} onValueChange={handleRangeChange}> <Select value={data.range} onValueChange={handleRangeChange}>
<SelectTrigger className="w-[160px]"> <SelectTrigger className="w-[160px]">
<SelectValue placeholder="Tarih aralığı" /> <SelectValue placeholder={t("analytics.range.placeholder")} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="this_week">Bu Hafta</SelectItem> <SelectItem value="this_week">{t("analytics.range.thisWeek")}</SelectItem>
<SelectItem value="this_month">Bu Ay</SelectItem> <SelectItem value="this_month">{t("analytics.range.thisMonth")}</SelectItem>
<SelectItem value="this_year">Bu Yıl</SelectItem> <SelectItem value="this_year">{t("analytics.range.thisYear")}</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@@ -67,7 +78,7 @@ export function AnalyticsClient({ data }: AnalyticsClientProps) {
<div className="grid gap-6 lg:grid-cols-2"> <div className="grid gap-6 lg:grid-cols-2">
<Card> <Card>
<CardContent className="p-6"> <CardContent className="p-6">
<h3 className="mb-6 text-sm font-semibold text-foreground">Proje Bazlı Gelir Dağılımı</h3> <h3 className="mb-6 text-sm font-semibold text-foreground">{t("analytics.sections.incomeDistribution")}</h3>
<div className="h-[300px] w-full"> <div className="h-[300px] w-full">
{projectIncomeData.length > 0 ? ( {projectIncomeData.length > 0 ? (
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
@@ -86,7 +97,7 @@ export function AnalyticsClient({ data }: AnalyticsClientProps) {
))} ))}
</Pie> </Pie>
<Tooltip <Tooltip
formatter={(value) => `${Number(value ?? 0)}`} formatter={(value) => formatCurrency(Number(value ?? 0))}
contentStyle={{ contentStyle={{
backgroundColor: 'var(--poyraz-background)', backgroundColor: 'var(--poyraz-background)',
borderColor: 'var(--poyraz-border)', borderColor: 'var(--poyraz-border)',
@@ -97,7 +108,7 @@ export function AnalyticsClient({ data }: AnalyticsClientProps) {
</PieChart> </PieChart>
</ResponsiveContainer> </ResponsiveContainer>
) : ( ) : (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">Veri bulunamadı.</div> <div className="flex h-full items-center justify-center text-sm text-muted-foreground">{t("analytics.empty")}</div>
)} )}
</div> </div>
</CardContent> </CardContent>
@@ -105,7 +116,7 @@ export function AnalyticsClient({ data }: AnalyticsClientProps) {
<Card> <Card>
<CardContent className="p-6"> <CardContent className="p-6">
<h3 className="mb-6 text-sm font-semibold text-foreground">Görev Durumu Analizi</h3> <h3 className="mb-6 text-sm font-semibold text-foreground">{t("analytics.sections.taskStatus")}</h3>
<div className="h-[300px] w-full"> <div className="h-[300px] w-full">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<BarChart data={taskStatusData}> <BarChart data={taskStatusData}>
@@ -124,7 +135,7 @@ export function AnalyticsClient({ data }: AnalyticsClientProps) {
<div key={index} className="flex items-center justify-between gap-6 text-xs"> <div key={index} className="flex items-center justify-between gap-6 text-xs">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: entry.color }} /> <div className="w-2 h-2 rounded-full" style={{ backgroundColor: entry.color }} />
<span className="text-muted-foreground">Görev Sayısı</span> <span className="text-muted-foreground">{t("analytics.tasks.count")}</span>
</div> </div>
<span className="font-semibold text-foreground"> <span className="font-semibold text-foreground">
{entry.value} {entry.value}
+13 -1
View File
@@ -1,6 +1,10 @@
import { AnalyticsClient, type AnalyticsData } from "./analytics-client"; import { AnalyticsClient, type AnalyticsData } from "./analytics-client";
import { parseDashboardRange, resolveDashboardRange } from "@/server/services/analytics-range"; import { parseDashboardRange, resolveDashboardRange } from "@/server/services/analytics-range";
import { requireFreelancerBackend } from "@/server/web/freelancer"; import { requireFreelancerBackend } from "@/server/web/freelancer";
import { requireFreelancer } from "@/server/auth/session";
import { resolveFreelancerLocale } from "@/server/i18n/resolver";
import { getClientI18nPayload } from "@/server/i18n/translator";
import { I18nProvider } from "@/components/i18n/i18n-provider";
export const metadata = { title: "Analizler" }; export const metadata = { title: "Analizler" };
@@ -9,11 +13,19 @@ export default async function AnalyticsPage({
}: { }: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>; searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) { }) {
const context = await requireFreelancer();
const resolvedLocale = await resolveFreelancerLocale(context);
const payload = getClientI18nPayload(resolvedLocale.locale, ["analytics"]);
const params = await searchParams; const params = await searchParams;
const range = parseDashboardRange(params.range); const range = parseDashboardRange(params.range);
const { actor, service } = await requireFreelancerBackend(); const { actor, service } = await requireFreelancerBackend();
const metrics = service.getFreelancerAnalytics(actor, resolveDashboardRange(range)); const metrics = service.getFreelancerAnalytics(actor, resolveDashboardRange(range));
const data: AnalyticsData = { metrics, range }; const data: AnalyticsData = { metrics, range };
return <AnalyticsClient data={data} />; return (
<I18nProvider locale={payload.locale} messages={payload.messages}>
<AnalyticsClient data={data} />
</I18nProvider>
);
} }
+16 -5
View File
@@ -3,6 +3,7 @@
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { cleanText, optionalDate, requiredText } from "@/server/web/form-data"; import { cleanText, optionalDate, requiredText } from "@/server/web/form-data";
import { requireFreelancerBackend } from "@/server/web/freelancer"; import { requireFreelancerBackend } from "@/server/web/freelancer";
import { parseContentTranslationsFromFormData } from "@/server/i18n/content";
const EVENT_TYPES = ["meeting", "focus", "deadline", "personal", "finance"] as const; const EVENT_TYPES = ["meeting", "focus", "deadline", "personal", "finance"] as const;
@@ -12,16 +13,26 @@ function eventType(value: FormDataEntryValue | null) {
: "focus"; : "focus";
} }
function payload(formData: FormData) { function payload(
formData: FormData,
service: Awaited<ReturnType<typeof requireFreelancerBackend>>["service"],
actor: Awaited<ReturnType<typeof requireFreelancerBackend>>["actor"]
) {
const context = service.contentTranslations.getLocalizationContext(actor);
const translations = parseContentTranslationsFromFormData(formData, "calendar_event", context);
const defaultTitle = translations?.[context.defaultLocale]?.title ?? "";
const defaultDesc = translations?.[context.defaultLocale]?.description ?? "";
return { return {
title: requiredText(formData.get("title"), "Etkinlik başlığı zorunludur."), title: defaultTitle || requiredText(formData.get("title"), "Etkinlik başlığı zorunludur."),
description: cleanText(formData.get("description")), description: defaultDesc || cleanText(formData.get("description")),
type: eventType(formData.get("type")), type: eventType(formData.get("type")),
startsAt: optionalDate(formData.get("starts_at")), startsAt: optionalDate(formData.get("starts_at")),
endsAt: optionalDate(formData.get("ends_at")), endsAt: optionalDate(formData.get("ends_at")),
clientId: cleanText(formData.get("client_id")), clientId: cleanText(formData.get("client_id")),
projectId: cleanText(formData.get("project_id")), projectId: cleanText(formData.get("project_id")),
taskId: cleanText(formData.get("task_id")), taskId: cleanText(formData.get("task_id")),
translations,
}; };
} }
@@ -42,7 +53,7 @@ function completeRelations(
export async function createCalendarEventRecord(formData: FormData) { export async function createCalendarEventRecord(formData: FormData) {
const backend = await requireFreelancerBackend(); const backend = await requireFreelancerBackend();
const value = completeRelations(payload(formData), backend.service, backend.actor); const value = completeRelations(payload(formData, backend.service, backend.actor), backend.service, backend.actor);
if (!value.startsAt) throw new Error("Etkinlik başlangıç zamanı zorunludur."); if (!value.startsAt) throw new Error("Etkinlik başlangıç zamanı zorunludur.");
backend.service.createCalendarEvent(backend.actor, value); backend.service.createCalendarEvent(backend.actor, value);
revalidatePath("/calendar"); revalidatePath("/calendar");
@@ -51,7 +62,7 @@ export async function createCalendarEventRecord(formData: FormData) {
export async function updateCalendarEventRecord(formData: FormData) { export async function updateCalendarEventRecord(formData: FormData) {
const backend = await requireFreelancerBackend(); const backend = await requireFreelancerBackend();
const id = requiredText(formData.get("id"), "Etkinlik kaydı bulunamadı."); const id = requiredText(formData.get("id"), "Etkinlik kaydı bulunamadı.");
const value = completeRelations(payload(formData), backend.service, backend.actor); const value = completeRelations(payload(formData, backend.service, backend.actor), backend.service, backend.actor);
if (!value.startsAt) throw new Error("Etkinlik başlangıç zamanı zorunludur."); if (!value.startsAt) throw new Error("Etkinlik başlangıç zamanı zorunludur.");
backend.service.updateCalendarEvent(backend.actor, id, value); backend.service.updateCalendarEvent(backend.actor, id, value);
revalidatePath("/calendar"); revalidatePath("/calendar");
+74 -32
View File
@@ -21,6 +21,10 @@ import {
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
Tabs,
TabsContent,
TabsList,
TabsTrigger,
toast, toast,
} from "poyraz-ui/molecules"; } from "poyraz-ui/molecules";
import { Clock, Pencil, Plus, Trash2 } from "lucide-react"; import { Clock, Pencil, Plus, Trash2 } from "lucide-react";
@@ -49,6 +53,7 @@ export type CalendarEventItem = {
clientName: string | null; clientName: string | null;
projectName: string | null; projectName: string | null;
taskTitle: string | null; taskTitle: string | null;
translations?: Record<string, Record<string, string>>;
}; };
const typeLabels = { const typeLabels = {
@@ -72,9 +77,10 @@ type CalendarClientProps = {
clients: CalendarRelationOption[]; clients: CalendarRelationOption[];
projects: CalendarRelationOption[]; projects: CalendarRelationOption[];
tasks: CalendarTaskOption[]; tasks: CalendarTaskOption[];
activeLocales: { code: string; name: string }[];
}; };
export function CalendarClient({ events, clients, projects, tasks }: CalendarClientProps) { export function CalendarClient({ events, clients, projects, tasks, activeLocales }: CalendarClientProps) {
const t = useTranslations(); const t = useTranslations();
const [monthDate, setMonthDate] = useState(() => new Date()); const [monthDate, setMonthDate] = useState(() => new Date());
const [selectedDate, setSelectedDate] = useState(() => toDateKey(new Date())); const [selectedDate, setSelectedDate] = useState(() => toDateKey(new Date()));
@@ -102,6 +108,7 @@ export function CalendarClient({ events, clients, projects, tasks }: CalendarCli
clients={clients} clients={clients}
projects={projects} projects={projects}
tasks={tasks} tasks={tasks}
activeLocales={activeLocales}
/> />
</div> </div>
@@ -113,7 +120,7 @@ export function CalendarClient({ events, clients, projects, tasks }: CalendarCli
<h2 className="text-lg font-semibold text-foreground"> <h2 className="text-lg font-semibold text-foreground">
{formatMonth(monthDate)} {formatMonth(monthDate)}
</h2> </h2>
<p className="text-sm text-muted-foreground">{events.length} etkinlik</p> <p className="text-sm text-muted-foreground">{t("common.itemsCount", { count: events.length })}</p>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button effect="shine" type="button" variant="secondary" onClick={() => shiftMonth(-1)}> <Button effect="shine" type="button" variant="secondary" onClick={() => shiftMonth(-1)}>
@@ -186,7 +193,7 @@ export function CalendarClient({ events, clients, projects, tasks }: CalendarCli
{formatDateLabel(selectedDate)} {formatDateLabel(selectedDate)}
</h2> </h2>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{selectedEvents.length} etkinlik {t("common.itemsCount", { count: selectedEvents.length })}
</p> </p>
</div> </div>
<CalendarEventDialog <CalendarEventDialog
@@ -195,15 +202,16 @@ export function CalendarClient({ events, clients, projects, tasks }: CalendarCli
clients={clients} clients={clients}
projects={projects} projects={projects}
tasks={tasks} tasks={tasks}
activeLocales={activeLocales}
/> />
<EventList events={selectedEvents} clients={clients} projects={projects} tasks={tasks} /> <EventList events={selectedEvents} clients={clients} projects={projects} tasks={tasks} activeLocales={activeLocales} />
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardContent className="space-y-3 p-4"> <CardContent className="space-y-3 p-4">
<h2 className="text-base font-semibold text-foreground">Yaklaşan etkinlikler</h2> <h2 className="text-base font-semibold text-foreground">Yaklaşan etkinlikler</h2>
<EventList events={upcomingEvents} clients={clients} projects={projects} tasks={tasks} compact /> <EventList events={upcomingEvents} clients={clients} projects={projects} tasks={tasks} activeLocales={activeLocales} compact />
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@@ -217,14 +225,17 @@ function EventList({
clients, clients,
projects, projects,
tasks, tasks,
activeLocales,
compact = false, compact = false,
}: { }: {
events: CalendarEventItem[]; events: CalendarEventItem[];
clients: CalendarRelationOption[]; clients: CalendarRelationOption[];
projects: CalendarRelationOption[]; projects: CalendarRelationOption[];
tasks: CalendarTaskOption[]; tasks: CalendarTaskOption[];
activeLocales: { code: string; name: string }[];
compact?: boolean; compact?: boolean;
}) { }) {
const t = useTranslations();
if (events.length === 0) { if (events.length === 0) {
return <p className="text-sm text-muted-foreground">Etkinlik yok.</p>; return <p className="text-sm text-muted-foreground">Etkinlik yok.</p>;
} }
@@ -246,16 +257,16 @@ function EventList({
</div> </div>
) : null} ) : null}
</div> </div>
<Badge className={typeClasses[event.type]}>{typeLabels[event.type]}</Badge> <Badge className={typeClasses[event.type]}>{t(`calendar.types.${event.type}` as any)}</Badge>
</div> </div>
{!compact ? ( {!compact ? (
<div className="mt-3 flex gap-2"> <div className="mt-3 flex gap-2">
<CalendarEventDialog mode="edit" event={event} clients={clients} projects={projects} tasks={tasks} /> <CalendarEventDialog mode="edit" event={event} clients={clients} projects={projects} tasks={tasks} activeLocales={activeLocales} />
<form action={deleteCalendarEventRecord}> <form action={deleteCalendarEventRecord}>
<input type="hidden" name="id" value={event.id} /> <input type="hidden" name="id" value={event.id} />
<Button effect="shine" type="submit" variant="secondary" className="gap-2 text-rose-600"> <Button effect="shine" type="submit" variant="secondary" className="gap-2 text-rose-600">
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
Sil {t("calendar.delete.confirm")}
</Button> </Button>
</form> </form>
</div> </div>
@@ -273,6 +284,7 @@ function CalendarEventDialog({
clients, clients,
projects, projects,
tasks, tasks,
activeLocales,
}: { }: {
mode: "create" | "edit"; mode: "create" | "edit";
event?: CalendarEventItem; event?: CalendarEventItem;
@@ -280,7 +292,9 @@ function CalendarEventDialog({
clients: CalendarRelationOption[]; clients: CalendarRelationOption[];
projects: CalendarRelationOption[]; projects: CalendarRelationOption[];
tasks: CalendarTaskOption[]; tasks: CalendarTaskOption[];
activeLocales: { code: string; name: string }[];
}) { }) {
const t = useTranslations();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const action = mode === "create" ? createCalendarEventRecord : updateCalendarEventRecord; const action = mode === "create" ? createCalendarEventRecord : updateCalendarEventRecord;
@@ -290,12 +304,12 @@ function CalendarEventDialog({
try { try {
await action(formData); await action(formData);
setOpen(false); setOpen(false);
toast.success(mode === "create" ? "Etkinlik eklendi." : "Etkinlik güncellendi."); toast.success(mode === "create" ? t("calendar.messages.created") : t("calendar.messages.updated"));
} catch (error) { } catch (error) {
toast.error( toast.error(
error instanceof Error error instanceof Error
? error.message ? error.message
: "Etkinlik kaydedilirken beklenmeyen bir hata oluştu.", : t("calendar.errors.saveFailed"),
); );
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
@@ -307,25 +321,25 @@ function CalendarEventDialog({
<DialogTrigger asChild> <DialogTrigger asChild>
<Button effect="shine" className="gap-2" variant={mode === "create" ? "default" : "secondary"}> <Button effect="shine" className="gap-2" variant={mode === "create" ? "default" : "secondary"}>
{mode === "create" ? <Plus className="h-4 w-4" /> : <Pencil className="h-4 w-4" />} {mode === "create" ? <Plus className="h-4 w-4" /> : <Pencil className="h-4 w-4" />}
{mode === "create" ? "Etkinlik ekle" : "Düzenle"} {mode === "create" ? t("calendar.actions.add") : t("calendar.form.editTitle")}
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="flex max-h-[calc(100dvh-2rem)] w-[calc(100vw-2rem)] flex-col overflow-hidden p-0 sm:max-h-[min(680px,calc(100dvh-4rem))] sm:max-w-xl data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95"> <DialogContent className="flex max-h-[calc(100dvh-2rem)] w-[calc(100vw-2rem)] flex-col overflow-hidden p-0 sm:max-h-[min(680px,calc(100dvh-4rem))] sm:max-w-xl data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95">
<form action={handleSubmit} className="flex min-h-0 flex-1 flex-col overflow-hidden"> <form action={handleSubmit} className="flex min-h-0 flex-1 flex-col overflow-hidden">
{event ? <input type="hidden" name="id" value={event.id} /> : null} {event ? <input type="hidden" name="id" value={event.id} /> : null}
<DialogHeader className="shrink-0 px-5 pb-4 pt-5 pr-12"> <DialogHeader className="shrink-0 px-5 pb-4 pt-5 pr-12">
<DialogTitle>{mode === "create" ? "Yeni etkinlik" : "Etkinliği düzenle"}</DialogTitle> <DialogTitle>{mode === "create" ? t("calendar.form.createTitle") : t("calendar.form.editTitle")}</DialogTitle>
<DialogDescription>Takvim etkinliğini proje, görev veya müşteriyle ilişkilendir.</DialogDescription> <DialogDescription>{t("calendar.description")}</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="tiny-scrollbar min-h-0 flex-1 overflow-y-auto px-5 pb-5"> <div className="tiny-scrollbar min-h-0 flex-1 overflow-y-auto px-5 pb-5">
<EventFormFields event={event} defaultDate={defaultDate} clients={clients} projects={projects} tasks={tasks} /> <EventFormFields event={event} defaultDate={defaultDate} clients={clients} projects={projects} tasks={tasks} activeLocales={activeLocales} />
</div> </div>
<DialogFooter className="shrink-0 border-t border-border bg-background p-5"> <DialogFooter className="shrink-0 border-t border-border bg-background p-5">
<Button variant="default" effect="shine" type="submit" disabled={isSubmitting} className="w-full gap-2 sm:w-auto"> <Button variant="default" effect="shine" type="submit" disabled={isSubmitting} className="w-full gap-2 sm:w-auto">
{mode === "create" ? <Plus className="h-4 w-4" /> : <Pencil className="h-4 w-4" />} {mode === "create" ? <Plus className="h-4 w-4" /> : <Pencil className="h-4 w-4" />}
{isSubmitting ? "Kaydediliyor" : mode === "create" ? "Etkinliği ekle" : "Değişiklikleri kaydet"} {isSubmitting ? "..." : t("calendar.form.save")}
</Button> </Button>
</DialogFooter> </DialogFooter>
</form> </form>
@@ -340,55 +354,83 @@ function EventFormFields({
clients, clients,
projects, projects,
tasks, tasks,
activeLocales,
}: { }: {
event?: CalendarEventItem; event?: CalendarEventItem;
defaultDate?: string; defaultDate?: string;
clients: CalendarRelationOption[]; clients: CalendarRelationOption[];
projects: CalendarRelationOption[]; projects: CalendarRelationOption[];
tasks: CalendarTaskOption[]; tasks: CalendarTaskOption[];
activeLocales: { code: string; name: string }[];
}) { }) {
const t = useTranslations();
const startsAt = event?.starts_at ? toDateTimeLocal(event.starts_at) : `${defaultDate || toDateKey(new Date())}T09:00`; const startsAt = event?.starts_at ? toDateTimeLocal(event.starts_at) : `${defaultDate || toDateKey(new Date())}T09:00`;
return ( return (
<div className="grid gap-4"> <div className="grid gap-4">
{activeLocales.length > 1 ? (
<Tabs defaultValue={activeLocales[0].code}>
<TabsList className="mb-4">
{activeLocales.map((locale) => (
<TabsTrigger key={locale.code} value={locale.code}>{locale.name}</TabsTrigger>
))}
</TabsList>
{activeLocales.map((locale) => (
<TabsContent key={locale.code} value={locale.code} className="space-y-4">
<div className="grid gap-2"> <div className="grid gap-2">
<Label>Başlık</Label> <Label>{t("calendar.form.title")} ({locale.code})</Label>
<Input name="title" defaultValue={event?.title || ""} required placeholder="Örn. Müşteri toplantısı" /> <Input name={`i18n.${locale.code}.title`} defaultValue={event?.translations?.[locale.code]?.title ?? ""} required={locale.code === activeLocales[0].code} />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label>Açıklama</Label> <Label>{t("calendar.form.description")} ({locale.code})</Label>
<Textarea name={`i18n.${locale.code}.description`} defaultValue={event?.translations?.[locale.code]?.description ?? ""} rows={3} />
</div>
</TabsContent>
))}
</Tabs>
) : (
<div className="space-y-4">
<div className="grid gap-2">
<Label>{t("calendar.form.title")}</Label>
<Input name="title" defaultValue={event?.title || ""} required />
</div>
<div className="grid gap-2">
<Label>{t("calendar.form.description")}</Label>
<Textarea name="description" defaultValue={event?.description || ""} rows={3} /> <Textarea name="description" defaultValue={event?.description || ""} rows={3} />
</div> </div>
</div>
)}
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<SelectField name="type" label="Tür" defaultValue={event?.type || "focus"}> <SelectField name="type" label={t("calendar.form.type") ?? "Tür"} defaultValue={event?.type || "focus"}>
<SelectItem value="meeting">Toplantı</SelectItem> <SelectItem value="meeting">{t("calendar.types.meeting")}</SelectItem>
<SelectItem value="focus">Odak</SelectItem> <SelectItem value="focus">{t("calendar.types.focus")}</SelectItem>
<SelectItem value="deadline">Deadline</SelectItem> <SelectItem value="deadline">{t("calendar.types.deadline")}</SelectItem>
<SelectItem value="personal">Kişisel</SelectItem> <SelectItem value="personal">{t("calendar.types.personal")}</SelectItem>
<SelectItem value="finance">Finans</SelectItem> <SelectItem value="finance">{t("calendar.types.finance")}</SelectItem>
</SelectField> </SelectField>
<SelectField name="client_id" label="Müşteri" defaultValue={event?.client_id || "__none"}> <SelectField name="client_id" label={t("calendar.form.client")} defaultValue={event?.client_id || "__none"}>
<SelectItem value="__none">Müşteri yok</SelectItem> <SelectItem value="__none">{t("calendar.form.selectClient")}</SelectItem>
{clients.map((client) => <SelectItem key={client.id} value={client.id}>{client.name}</SelectItem>)} {clients.map((client) => <SelectItem key={client.id} value={client.id}>{client.name}</SelectItem>)}
</SelectField> </SelectField>
</div> </div>
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<SelectField name="project_id" label="Proje" defaultValue={event?.project_id || "__none"}> <SelectField name="project_id" label={t("calendar.form.project")} defaultValue={event?.project_id || "__none"}>
<SelectItem value="__none">Proje yok</SelectItem> <SelectItem value="__none">{t("calendar.form.selectProject")}</SelectItem>
{projects.map((project) => <SelectItem key={project.id} value={project.id}>{project.name}</SelectItem>)} {projects.map((project) => <SelectItem key={project.id} value={project.id}>{project.name}</SelectItem>)}
</SelectField> </SelectField>
<SelectField name="task_id" label="Görev" defaultValue={event?.task_id || "__none"}> <SelectField name="task_id" label={t("calendar.form.task")} defaultValue={event?.task_id || "__none"}>
<SelectItem value="__none">Görev yok</SelectItem> <SelectItem value="__none">{t("calendar.form.selectTask")}</SelectItem>
{tasks.map((task) => <SelectItem key={task.id} value={task.id}>{task.title}</SelectItem>)} {tasks.map((task) => <SelectItem key={task.id} value={task.id}>{task.title}</SelectItem>)}
</SelectField> </SelectField>
</div> </div>
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<div className="grid gap-2"> <div className="grid gap-2">
<Label>Başlangıç</Label> <Label>{t("calendar.form.start")}</Label>
<Input name="starts_at" type="datetime-local" defaultValue={startsAt} required /> <Input name="starts_at" type="datetime-local" defaultValue={startsAt} required />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label>Bitiş</Label> <Label>{t("calendar.form.end")}</Label>
<Input name="ends_at" type="datetime-local" defaultValue={event?.ends_at ? toDateTimeLocal(event.ends_at) : ""} /> <Input name="ends_at" type="datetime-local" defaultValue={event?.ends_at ? toDateTimeLocal(event.ends_at) : ""} />
</div> </div>
</div> </div>
+27 -2
View File
@@ -1,8 +1,26 @@
import { CalendarClient, type CalendarEventItem, type CalendarRelationOption, type CalendarTaskOption } from "@/app/(dashboard)/calendar/calendar-client"; import { CalendarClient, type CalendarEventItem, type CalendarRelationOption, type CalendarTaskOption } from "@/app/(dashboard)/calendar/calendar-client";
import { requireFreelancerBackend } from "@/server/web/freelancer"; import { requireFreelancerBackend } from "@/server/web/freelancer";
import { resolveFreelancerLocale } from "@/server/i18n/resolver";
import { getClientI18nPayload } from "@/server/i18n/translator";
import { I18nProvider } from "@/components/i18n/i18n-provider";
import type { ContentTranslationRow } from "@/server/i18n/content";
function buildTranslations(rows: ContentTranslationRow[] | undefined) {
if (!rows) return undefined;
const result: Record<string, Record<string, string>> = {};
for (const row of rows) {
if (!result[row.locale]) result[row.locale] = {};
result[row.locale][row.field] = row.value;
}
return result;
}
export default async function CalendarPage() { export default async function CalendarPage() {
const { actor, service } = await requireFreelancerBackend(); const { context, actor, service } = await requireFreelancerBackend();
const resolvedLocale = await resolveFreelancerLocale(context);
const payload = getClientI18nPayload(resolvedLocale.locale, ["calendar"]);
const activeLocales = service.listLocales(actor).filter(l => l.status !== "archived").map(l => ({ code: l.code, name: l.nativeName }));
const eventRows = service.listCalendarEvents(actor); const eventRows = service.listCalendarEvents(actor);
const clientRows = service.listClients(actor); const clientRows = service.listClients(actor);
const projectRows = service.listProjects(actor); const projectRows = service.listProjects(actor);
@@ -11,6 +29,8 @@ export default async function CalendarPage() {
const projects = new Map(projectRows.map((item) => [item.id, item.name])); const projects = new Map(projectRows.map((item) => [item.id, item.name]));
const tasks = new Map(taskRows.map((item) => [item.id, item.title])); const tasks = new Map(taskRows.map((item) => [item.id, item.title]));
const translationsMap = service.contentTranslations.listBatch("calendar_event", eventRows.map(e => e.id));
const events: CalendarEventItem[] = eventRows.map((event) => ({ const events: CalendarEventItem[] = eventRows.map((event) => ({
id: event.id, id: event.id,
title: event.title, title: event.title,
@@ -24,6 +44,7 @@ export default async function CalendarPage() {
clientName: event.clientId ? clients.get(event.clientId) ?? null : null, clientName: event.clientId ? clients.get(event.clientId) ?? null : null,
projectName: event.projectId ? projects.get(event.projectId) ?? null : null, projectName: event.projectId ? projects.get(event.projectId) ?? null : null,
taskTitle: event.taskId ? tasks.get(event.taskId) ?? null : null, taskTitle: event.taskId ? tasks.get(event.taskId) ?? null : null,
translations: buildTranslations(translationsMap.get(event.id)),
})); }));
const clientOptions: CalendarRelationOption[] = clientRows const clientOptions: CalendarRelationOption[] = clientRows
.filter((item) => item.status !== "archived") .filter((item) => item.status !== "archived")
@@ -35,5 +56,9 @@ export default async function CalendarPage() {
.filter((item) => item.status !== "done" && item.status !== "cancelled") .filter((item) => item.status !== "done" && item.status !== "cancelled")
.map(({ id, title }) => ({ id, title })); .map(({ id, title }) => ({ id, title }));
return <CalendarClient events={events} clients={clientOptions} projects={projectOptions} tasks={taskOptions} />; return (
<I18nProvider payload={payload}>
<CalendarClient events={events} clients={clientOptions} projects={projectOptions} tasks={taskOptions} activeLocales={activeLocales} />
</I18nProvider>
);
} }