EventBuzz_Console/src/app/components/attendance/AttendanceWhatsAppNotifyFie...

341 lines
11 KiB
TypeScript

import React, { useEffect, useState } from "react";
import { whatsappTemplatesApi } from "../../../services/api";
import type { WaOverrideSlot } from "../../../services/api/campaigns";
import type {
AttendanceWhatsAppNotifySettings,
WhatsAppTemplateRecord,
WhatsAppVariableBindings,
} from "../../../services/api/types";
import { WA_TEMPLATE_MERGE_OPTIONS } from "../messaging/mergePlaceholders";
type SlotMode = "default" | "merge" | "fixed";
type SlotRow = { mode: SlotMode; mergeKey?: string; fixed?: string };
function emptySlotRow(): SlotRow {
return { mode: "default" };
}
function parseSlotsFromOverrides(
n: number,
section: "header" | "body" | "url",
notify: AttendanceWhatsAppNotifySettings | undefined,
defaultBindings: string[] | undefined,
): SlotRow[] {
const o = notify?.waOverrides?.[section];
const out: SlotRow[] = [];
for (let i = 0; i < n; i += 1) {
const slot = String(i + 1);
const raw = o?.[slot] as WaOverrideSlot | undefined;
if (!raw) {
out.push({ mode: "default" });
continue;
}
if (raw.kind === "fixed") {
out.push({ mode: "fixed", fixed: raw.value || "" });
} else {
const def = String(defaultBindings?.[i] || "").trim();
if (def && raw.key === def) {
out.push({ mode: "default" });
} else {
out.push({ mode: "merge", mergeKey: raw.key || "" });
}
}
}
return out;
}
function buildSectionOverrides(
rows: SlotRow[],
defaultBindings: string[] | undefined,
): Record<string, WaOverrideSlot> | undefined {
const out: Record<string, WaOverrideSlot> = {};
for (let i = 0; i < rows.length; i += 1) {
const row = rows[i];
const def = String(defaultBindings?.[i] || "").trim();
if (row.mode === "default") continue;
if (row.mode === "fixed") {
out[String(i + 1)] = { kind: "fixed", value: String(row.fixed || "") };
} else if (row.mode === "merge") {
const k = String(row.mergeKey || "").trim();
if (k && k !== def) out[String(i + 1)] = { kind: "merge", key: k };
}
}
return Object.keys(out).length ? out : undefined;
}
function hasImageHeader(components: Record<string, unknown>[]): boolean {
return components.some(
(c) =>
String(c.type || "").toUpperCase() === "HEADER" && String(c.format || "").toUpperCase() === "IMAGE",
);
}
const IS: React.CSSProperties = {
width: "100%",
padding: "8px 12px",
border: "1px solid var(--border)",
borderRadius: "var(--radius-button)",
background: "var(--input-background)",
fontSize: "var(--text-sm)",
color: "var(--card-foreground)",
boxSizing: "border-box",
};
function SlotTable({
title,
rows,
setRows,
defaultBindings,
}: {
title: string;
rows: SlotRow[];
setRows: (next: SlotRow[]) => void;
defaultBindings: string[];
}) {
if (!rows.length) return null;
return (
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<div style={{ fontSize: 12, fontWeight: 600 }}>{title}</div>
{rows.map((row, idx) => (
<div
key={idx}
style={{
display: "grid",
gridTemplateColumns: "72px 100px 1fr",
gap: 8,
alignItems: "center",
}}
>
<span style={{ fontSize: 12, color: "var(--muted)" }}>{`{{${idx + 1}}}`}</span>
<select
style={IS}
value={row.mode}
onChange={(e) => {
const mode = e.target.value as SlotMode;
const next = [...rows];
next[idx] =
mode === "merge"
? {
mode: "merge",
mergeKey:
WA_TEMPLATE_MERGE_OPTIONS.find((o) => o.mergeKey)?.mergeKey ||
"contact.firstName",
}
: mode === "fixed"
? { mode: "fixed", fixed: "" }
: { mode: "default" };
setRows(next);
}}
>
<option value="default">Template default</option>
<option value="merge">Merge field</option>
<option value="fixed">Fixed text</option>
</select>
{row.mode === "merge" ? (
<select
style={IS}
value={row.mergeKey || ""}
onChange={(e) => {
const next = [...rows];
next[idx] = { mode: "merge", mergeKey: e.target.value };
setRows(next);
}}
>
{WA_TEMPLATE_MERGE_OPTIONS.filter((o) => o.mergeKey).map((o) => (
<option key={o.mergeKey} value={o.mergeKey}>
{o.label}
</option>
))}
</select>
) : row.mode === "fixed" ? (
<input
style={IS}
value={row.fixed || ""}
placeholder="Fixed value"
onChange={(e) => {
const next = [...rows];
next[idx] = { mode: "fixed", fixed: e.target.value };
setRows(next);
}}
/>
) : (
<span style={{ fontSize: 11, color: "var(--muted)" }}>
{defaultBindings[idx] ? `Uses ${defaultBindings[idx]}` : "Sample / binding from template"}
</span>
)}
</div>
))}
</div>
);
}
export function AttendanceWhatsAppNotifyFields({
value,
onChange,
}: {
value: AttendanceWhatsAppNotifySettings;
onChange: (next: AttendanceWhatsAppNotifySettings) => void;
}) {
const [templates, setTemplates] = useState<WhatsAppTemplateRecord[]>([]);
const [detail, setDetail] = useState<WhatsAppTemplateRecord | null>(null);
const [headerRows, setHeaderRows] = useState<SlotRow[]>([]);
const [bodyRows, setBodyRows] = useState<SlotRow[]>([]);
const [urlRows, setUrlRows] = useState<SlotRow[]>([]);
const [showImage, setShowImage] = useState(false);
useEffect(() => {
void whatsappTemplatesApi.list({ status: "APPROVED" }).then((r) => {
if (!r.error) setTemplates(r.data?.templates || []);
});
}, []);
useEffect(() => {
const id = value.whatsappTemplateId;
if (!id) {
setDetail(null);
setHeaderRows([]);
setBodyRows([]);
setUrlRows([]);
setShowImage(false);
return;
}
void whatsappTemplatesApi.get(String(id)).then((r) => {
if (!r.data) return;
setDetail(r.data);
const vb: WhatsAppVariableBindings = r.data.variableBindings || { header: [], body: [], url: [] };
setShowImage(hasImageHeader((r.data.components || []) as Record<string, unknown>[]));
setHeaderRows(parseSlotsFromOverrides((vb.header || []).length, "header", value, vb.header));
setBodyRows(parseSlotsFromOverrides((vb.body || []).length, "body", value, vb.body));
setUrlRows(parseSlotsFromOverrides((vb.url || []).length, "url", value, vb.url));
});
// Only re-load detail when template id changes; slot edits push via emitOverrides
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value.whatsappTemplateId]);
const emitOverrides = (
nextHeader: SlotRow[],
nextBody: SlotRow[],
nextUrl: SlotRow[],
partial?: Partial<AttendanceWhatsAppNotifySettings>,
) => {
const vb = detail?.variableBindings || { header: [], body: [], url: [] };
const waOverrides: NonNullable<AttendanceWhatsAppNotifySettings["waOverrides"]> = {};
const h = buildSectionOverrides(nextHeader, vb.header);
const b = buildSectionOverrides(nextBody, vb.body);
const u = buildSectionOverrides(nextUrl, vb.url);
if (h) waOverrides.header = h;
if (b) waOverrides.body = b;
if (u) waOverrides.url = u;
onChange({
...value,
...partial,
waOverrides: Object.keys(waOverrides).length ? waOverrides : null,
});
};
return (
<div
style={{
display: "flex",
flexDirection: "column",
gap: 10,
padding: 12,
border: "1px solid var(--border)",
borderRadius: 8,
background: "var(--secondary)",
}}
>
<label style={{ display: "flex", alignItems: "center", gap: 8, fontSize: "var(--text-sm)", cursor: "pointer" }}>
<input
type="checkbox"
checked={Boolean(value.enabled)}
onChange={(e) =>
onChange({
...value,
enabled: e.target.checked,
...(e.target.checked
? {}
: { whatsappTemplateId: null, waHeaderImageUrl: "", waOverrides: null }),
})
}
/>
Send WhatsApp template on check-in (once per person)
</label>
<p style={{ margin: 0, fontSize: 11, color: "var(--muted)" }}>
Sent after the first successful check-in. Duplicate check-ins do not resend.
</p>
{value.enabled ? (
<>
<label style={{ fontSize: 12, fontWeight: 500 }}>
Template
<select
style={{ ...IS, marginTop: 6 }}
value={value.whatsappTemplateId || ""}
onChange={(e) =>
onChange({
...value,
whatsappTemplateId: e.target.value || null,
waOverrides: null,
waHeaderImageUrl: "",
})
}
>
<option value="">Select APPROVED template…</option>
{templates.map((t) => (
<option key={t._id} value={t._id}>
{t.name} ({t.language})
</option>
))}
</select>
</label>
{showImage ? (
<label style={{ fontSize: 12, fontWeight: 500 }}>
Header image URL
<input
style={{ ...IS, marginTop: 6 }}
value={value.waHeaderImageUrl || ""}
placeholder="https://…"
onChange={(e) => onChange({ ...value, waHeaderImageUrl: e.target.value })}
/>
</label>
) : null}
{detail ? (
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
<SlotTable
title="Header variables"
rows={headerRows}
defaultBindings={detail.variableBindings?.header || []}
setRows={(next) => {
setHeaderRows(next);
emitOverrides(next, bodyRows, urlRows);
}}
/>
<SlotTable
title="Body variables"
rows={bodyRows}
defaultBindings={detail.variableBindings?.body || []}
setRows={(next) => {
setBodyRows(next);
emitOverrides(headerRows, next, urlRows);
}}
/>
<SlotTable
title="URL button variables"
rows={urlRows}
defaultBindings={detail.variableBindings?.url || []}
setRows={(next) => {
setUrlRows(next);
emitOverrides(headerRows, bodyRows, next);
}}
/>
</div>
) : null}
</>
) : null}
</div>
);
}