diff --git a/src/app/(dashboard)/activities/page.tsx b/src/app/(dashboard)/activities/page.tsx index d5d4afc..410b356 100644 --- a/src/app/(dashboard)/activities/page.tsx +++ b/src/app/(dashboard)/activities/page.tsx @@ -35,6 +35,7 @@ export default async function ActivitiesPage() { initialActivities={activities} customers={customers} properties={properties} + role={ctx.role} /> ); diff --git a/src/components/activities/activities-client.tsx b/src/components/activities/activities-client.tsx index b76bc7c..527640d 100644 --- a/src/components/activities/activities-client.tsx +++ b/src/components/activities/activities-client.tsx @@ -19,9 +19,11 @@ import { } from "@/lib/appwrite/activity-actions"; import { ActivityFormSheet } from "./activity-form-sheet"; import { ActivityCalendar } from "./activity-calendar"; +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"; type ViewMode = "list" | "calendar"; @@ -29,12 +31,14 @@ interface ActivitiesClientProps { initialActivities: Activity[]; customers: Customer[]; properties: Property[]; + role: TenantRole; } export function ActivitiesClient({ initialActivities, customers, properties, + role, }: ActivitiesClientProps) { const router = useRouter(); const [activities, setActivities] = useState(initialActivities); @@ -123,6 +127,7 @@ export function ActivitiesClient({ Takvim + Yeni Aktivite diff --git a/src/components/activities/send-summary-dialog.tsx b/src/components/activities/send-summary-dialog.tsx new file mode 100644 index 0000000..7e19871 --- /dev/null +++ b/src/components/activities/send-summary-dialog.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { useActionState, useEffect } from "react"; +import { Envelope, CircleNotch, Users, User } from "@/lib/icons"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Label } from "@/components/ui/label"; +import { sendDailySummaryAction } from "@/lib/appwrite/activity-email-actions"; +import type { SendSummaryState } from "@/lib/appwrite/activity-email-actions"; +import type { TenantRole } from "@/lib/appwrite/tenant-guard"; + +const initialState: SendSummaryState = { ok: false }; + +interface Props { + role: TenantRole; +} + +export function SendSummaryDialog({ role }: Props) { + const canSendToTeam = role === "owner" || role === "admin"; + const [state, formAction, isPending] = useActionState(sendDailySummaryAction, initialState); + + useEffect(() => { + if (!state.ok && state.error) return; // shown inline + if (state.ok) { + const msg = + state.sent === 1 + ? "Günlük özet e-postanıza gönderildi." + : `Günlük özet ${state.sent} ekip üyesine gönderildi.`; + toast.success(msg); + } + }, [state]); + + return ( + + + + + Günlük Özet + + + + + Günlük aktivite özeti + + Bugün için planlanmış aktiviteler e-posta ile gönderilir. + + + + + {canSendToTeam && ( + + + + + + + Sadece bana + + + Yalnızca sizin bugünkü aktiviteleriniz gönderilir. + + + + + + + + + + Tüm ekibe + + + Her danışmana kendi aktiviteleri ayrı ayrı gönderilir. + + + + + )} + + {!canSendToTeam && ( + + )} + + {state.error && ( + + {state.error} + + )} + + + {isPending ? ( + <> + + Gönderiliyor... + > + ) : ( + <> + + Gönder + > + )} + + + + + ); +} diff --git a/src/lib/appwrite/activity-email-actions.ts b/src/lib/appwrite/activity-email-actions.ts new file mode 100644 index 0000000..c250951 --- /dev/null +++ b/src/lib/appwrite/activity-email-actions.ts @@ -0,0 +1,166 @@ +"use server"; + +import { ID } from "node-appwrite"; +import { Query } from "node-appwrite"; + +import { createAdminClient } from "./server"; +import { requireTenant } from "./tenant-guard"; +import { DATABASE_ID, TABLES, ACTIVITY_TYPE_LABELS, type Activity } from "./schema"; +import type { AuthState } from "./auth-types"; + +export type SendTarget = "me" | "team"; + +export type SendSummaryState = AuthState & { sent?: number }; + +export async function sendDailySummaryAction( + _prev: SendSummaryState, + formData: FormData, +): Promise { + let ctx; + try { + ctx = await requireTenant(); + } catch { + return { ok: false, error: "Oturum geçersiz." }; + } + + const target = (formData.get("target") ?? "me") as SendTarget; + + if (target === "team" && !["owner", "admin"].includes(ctx.role)) { + return { ok: false, error: "Bu işlem için yetkiniz yok." }; + } + + const { tablesDB, teams, users, messaging } = createAdminClient(); + + const todayStr = new Date().toISOString().split("T")[0]; // "YYYY-MM-DD" + + const activitiesResult = await tablesDB.listRows({ + databaseId: DATABASE_ID, + tableId: TABLES.activities, + queries: [ + Query.equal("tenantId", ctx.tenantId), + Query.limit(500), + ], + }); + + const allActivities = JSON.parse(JSON.stringify(activitiesResult.rows)) as Activity[]; + const todayActivities = allActivities.filter( + (a) => a.dueDate?.startsWith(todayStr), + ); + + const dateLabel = new Date().toLocaleDateString("tr-TR", { + day: "numeric", + month: "long", + }); + + if (target === "me") { + const mine = todayActivities.filter((a) => a.createdBy === ctx.user.id); + if (mine.length === 0) { + return { ok: false, error: "Bugün için planlanmış aktiviteniz bulunmuyor." }; + } + await messaging.createEmail( + ID.unique(), + `Bugünün Aktiviteleri — ${dateLabel}`, + buildEmailHtml(mine, ctx.user.name), + [], + [ctx.user.id], + [], + ); + return { ok: true, sent: 1 }; + } + + // send_to_team: her üyeye kendi aktiviteleri + const membershipsResult = await teams.listMemberships(ctx.tenantId); + let sentCount = 0; + + for (const m of membershipsResult.memberships) { + if (!m.userId || !m.confirm) continue; + + const memberActivities = todayActivities.filter((a) => a.createdBy === m.userId); + if (memberActivities.length === 0) continue; + + const member = await users.get(m.userId).catch(() => null); + if (!member) continue; + + await messaging.createEmail( + ID.unique(), + `Bugünün Aktiviteleri — ${dateLabel}`, + buildEmailHtml(memberActivities, member.name), + [], + [m.userId], + [], + ); + sentCount++; + } + + if (sentCount === 0) { + return { + ok: false, + error: "Ekip üyelerinin bugün planlanmış aktivitesi bulunmuyor.", + }; + } + + return { ok: true, sent: sentCount }; +} + +function buildEmailHtml(activities: Activity[], name: string): string { + const today = new Date().toLocaleDateString("tr-TR", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + }); + + const rows = activities + .map((a) => { + const done = a.completedAt + ? `✓ Tamamlandı` + : `○ Bekliyor`; + const type = ACTIVITY_TYPE_LABELS[a.type] ?? a.type; + const desc = a.description + ? `${a.description}` + : ""; + return ` + + + ${a.title} + ${desc} + + + ${type} + + ${done} + `; + }) + .join(""); + + return ` + + + + Emlak CRM · Günlük Özet + Merhaba, ${name} + ${today} + + + + Bugün için ${activities.length} aktivite planlanmış: + + + + + Aktivite + Tür + Durum + + + ${rows} + + + + + Bu e-posta Emlak CRM tarafından otomatik olarak gönderilmiştir. + + + +`; +}
+ Yalnızca sizin bugünkü aktiviteleriniz gönderilir. +
+ Her danışmana kendi aktiviteleri ayrı ayrı gönderilir. +
+ {state.error} +
Emlak CRM · Günlük Özet
${today}
+ Bugün için ${activities.length} aktivite planlanmış: +
+ Bu e-posta Emlak CRM tarafından otomatik olarak gönderilmiştir. +