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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user