EventBuzz_Console/src/app/components/dashboard/SendWhatsAppNotificationPan...

242 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useCallback, useEffect, useState } from "react";
import { uploadMediaFile } from "../../../services/api";
import { whatsappBlastsApi } from "../../../services/api/whatsappBlasts";
import { PanelShell, RadioGroup, Label, Field, HelperText, IS, focusIn, focusOut } from "../panels/PanelShell";
import { WhatsAppCtaEditor, normalizeCtasForSave, type WhatsAppCtaItem } from "../whatsapp/WhatsAppCtaEditor";
function toDatetimeLocalValue(d: Date) {
const pad = (n: number) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
export function SendWhatsAppNotificationPanel({
eventId,
eventName,
onClose,
onSuccess,
}: {
eventId: string;
eventName?: string;
onClose: () => void;
onSuccess?: () => void;
}) {
const [body, setBody] = useState("");
const [mediaType, setMediaType] = useState<"none" | "image" | "video">("none");
const [mediaUrl, setMediaUrl] = useState("");
const [ctas, setCtas] = useState<WhatsAppCtaItem[]>([]);
const [when, setWhen] = useState<"now" | "schedule">("now");
const [scheduleLocal, setScheduleLocal] = useState("");
const [eligible, setEligible] = useState<number | null>(null);
const [eligibleError, setEligibleError] = useState("");
const [uploading, setUploading] = useState(false);
const [saving, setSaving] = useState(false);
const [err, setErr] = useState("");
const loadEligible = useCallback(async () => {
setEligibleError("");
const res = await whatsappBlastsApi.eligibleCount(eventId);
if (res.error) {
setEligible(null);
setEligibleError(res.error);
return;
}
setEligible(res.data?.count ?? 0);
}, [eventId]);
useEffect(() => {
void loadEligible();
const t = window.setInterval(() => void loadEligible(), 30_000);
return () => window.clearInterval(t);
}, [loadEligible]);
const onPickMedia = async (file: File | null) => {
if (!file) return;
const mime = String(file.type || "").toLowerCase();
const nextType = mime.startsWith("video/") ? "video" : mime.startsWith("image/") ? "image" : null;
if (!nextType) {
setErr("Attach an image or video file.");
return;
}
setUploading(true);
setErr("");
try {
const up = await uploadMediaFile(file, { eventId, module: "whatsapp/notifications" });
if (!up.url) {
setErr("Upload failed");
return;
}
setMediaType(nextType);
setMediaUrl(up.url);
} catch (e) {
setErr(e instanceof Error ? e.message : "Upload failed");
} finally {
setUploading(false);
}
};
const submit = async () => {
const text = body.trim();
let ctasPayload: WhatsAppCtaItem[] = [];
try {
ctasPayload = normalizeCtasForSave(ctas);
} catch (e) {
setErr(e instanceof Error ? e.message : "Invalid buttons");
return;
}
if (ctasPayload.length && !text) {
setErr("Message text is required when buttons are set.");
return;
}
if (!text && !mediaUrl) {
setErr("Enter a message or attach media.");
return;
}
let sendAt: string | undefined;
if (when === "schedule") {
if (!scheduleLocal) {
setErr("Pick a date and time.");
return;
}
const d = new Date(scheduleLocal);
if (Number.isNaN(d.getTime())) {
setErr("Invalid schedule time.");
return;
}
sendAt = d.toISOString();
}
setSaving(true);
setErr("");
const res = await whatsappBlastsApi.create(eventId, {
body: text,
mediaType: mediaUrl ? mediaType : "none",
mediaUrl: mediaUrl || "",
ctas: ctasPayload,
...(sendAt ? { sendAt } : {}),
});
setSaving(false);
if (res.error) {
setErr(res.error);
return;
}
onSuccess?.();
onClose();
};
const eligibleLine =
eligibleError
? eligibleError
: eligible == null
? "Counting people in the 24-hour WhatsApp window…"
: `${eligible} ${eligible === 1 ? "person" : "people"} on this event can receive this now (open WhatsApp chat, last 24 hours).`;
return (
<PanelShell
onClose={onClose}
title="Send WhatsApp notification"
breadcrumb={eventName ? `Event Home · ${eventName}` : "Event Home · WhatsApp Notifications"}
description="Sends the same message to everyone on this event with an open WhatsApp chat still inside Meta’s 24-hour window."
completedCount={1}
totalSections={1}
ctaLabel={saving ? "Sending…" : when === "schedule" ? "Schedule" : "Send now"}
ctaDisabled={saving || uploading}
onCtaClick={() => void submit()}
showProgress={false}
panelWidth="min(560px, 100vw)"
>
<div style={{ padding: "16px 20px", display: "flex", flexDirection: "column", gap: 14 }}>
<p style={{ margin: 0, fontSize: "var(--text-sm)", color: "var(--card-foreground)" }}>{eligibleLine}</p>
<Field>
<Label text="Message" />
<textarea
value={body}
onChange={(e) => setBody(e.target.value)}
onFocus={focusIn}
onBlur={focusOut}
rows={6}
placeholder="Write the WhatsApp message…"
style={{ ...IS, minHeight: 120, resize: "vertical" }}
/>
</Field>
<Field>
<Label text="Image or video (optional)" />
{mediaUrl ? (
<div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
<a href={mediaUrl} target="_blank" rel="noreferrer" style={{ fontSize: "var(--text-sm)", color: "var(--primary)" }}>
{mediaType === "video" ? "Video attached" : "Image attached"}
</a>
<button
type="button"
onClick={() => {
setMediaUrl("");
setMediaType("none");
}}
style={{
fontSize: "var(--text-xs)",
fontFamily: "var(--font-noto-sans)",
border: "1px solid var(--border)",
background: "var(--secondary)",
borderRadius: "var(--radius-button)",
padding: "4px 8px",
cursor: "pointer",
}}
>
Remove
</button>
</div>
) : (
<input
type="file"
accept="image/*,video/*"
disabled={uploading}
onChange={(e) => {
const file = e.target.files?.[0] || null;
e.target.value = "";
void onPickMedia(file);
}}
/>
)}
<HelperText text={uploading ? "Uploading…" : "Optional. Recipients outside the 24-hour window are skipped."} />
</Field>
<Field>
<WhatsAppCtaEditor value={ctas} onChange={setCtas} />
</Field>
<Field>
<Label text="When" />
<RadioGroup
name="wa-blast-when"
value={when}
onChange={(v) => setWhen(v === "schedule" ? "schedule" : "now")}
options={[
{ label: "Send now", value: "now" },
{ label: "Schedule", value: "schedule" },
]}
/>
</Field>
{when === "schedule" ? (
<Field>
<Label text="Schedule for" required />
<input
type="datetime-local"
value={scheduleLocal}
min={toDatetimeLocalValue(new Date())}
onChange={(e) => setScheduleLocal(e.target.value)}
onFocus={focusIn}
onBlur={focusOut}
style={IS}
/>
</Field>
) : null}
{err ? (
<p style={{ margin: 0, fontSize: "var(--text-sm)", color: "var(--destructive)" }}>{err}</p>
) : null}
</div>
</PanelShell>
);
}