feat: daily activity summary email

- activity-email-actions: sendDailySummaryAction — filters today's
  activities by dueDate, sends personalized email via Appwrite Messaging
  - 'me': current user's activities only
  - 'team' (owner/admin): each member gets their own activities separately
- send-summary-dialog: dialog with me/team radio (owner/admin only sees
  team option), inline error + toast on success
- activities-client: 'Günlük Özet' button in header, role prop added
- activities page: passes ctx.role to client
This commit is contained in:
egecankomur
2026-05-12 17:26:50 +03:00
parent fe86bfe6b2
commit 5ac6a1f8b0
4 changed files with 289 additions and 0 deletions
@@ -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
</button>
</div>
<SendSummaryDialog role={role} />
<Button onClick={openCreate} size="sm" data-tour="activities-add">
<Plus className="mr-1.5 size-4" />
Yeni Aktivite
@@ -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 (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" size="sm">
<Envelope className="mr-1.5 size-4" />
Günlük Özet
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-sm">
<DialogHeader>
<DialogTitle>Günlük aktivite özeti</DialogTitle>
<DialogDescription>
Bugün için planlanmış aktiviteler e-posta ile gönderilir.
</DialogDescription>
</DialogHeader>
<form action={formAction} className="flex flex-col gap-5 pt-1">
{canSendToTeam && (
<RadioGroup name="target" defaultValue="me" className="gap-3">
<label className="flex items-start gap-3 rounded-lg border p-3 cursor-pointer has-[[data-state=checked]]:border-primary has-[[data-state=checked]]:bg-primary/5 transition-colors">
<RadioGroupItem value="me" id="target-me" className="mt-0.5" />
<div>
<div className="flex items-center gap-1.5 font-medium text-sm">
<User className="size-4 text-muted-foreground" />
Sadece bana
</div>
<p className="text-muted-foreground text-xs mt-0.5">
Yalnızca sizin bugünkü aktiviteleriniz gönderilir.
</p>
</div>
</label>
<label className="flex items-start gap-3 rounded-lg border p-3 cursor-pointer has-[[data-state=checked]]:border-primary has-[[data-state=checked]]:bg-primary/5 transition-colors">
<RadioGroupItem value="team" id="target-team" className="mt-0.5" />
<div>
<div className="flex items-center gap-1.5 font-medium text-sm">
<Users className="size-4 text-muted-foreground" />
Tüm ekibe
</div>
<p className="text-muted-foreground text-xs mt-0.5">
Her danışmana kendi aktiviteleri ayrı ayrı gönderilir.
</p>
</div>
</label>
</RadioGroup>
)}
{!canSendToTeam && (
<input type="hidden" name="target" value="me" />
)}
{state.error && (
<p className="text-destructive text-sm text-center" role="alert">
{state.error}
</p>
)}
<Button type="submit" className="w-full" disabled={isPending}>
{isPending ? (
<>
<CircleNotch className="size-4 animate-spin" />
Gönderiliyor...
</>
) : (
<>
<Envelope className="size-4" />
Gönder
</>
)}
</Button>
</form>
</DialogContent>
</Dialog>
);
}