"use client"; import Link from "next/link"; import { useState, useReducer, useEffect, useRef } from "react"; import { useRouter } from "next/navigation"; import type { Campaign, CampaignType, CampaignStatus, AudienceRules } from "@/actions/communications/campaigns"; import { formatDate } from "@/lib/format-date"; import type { Template } from "@/actions/communications/templates"; import type { AudiencePreviewResult } from "@/actions/communications/send"; import { previewCampaignAudience, sendCampaign } from "@/actions/communications/send"; import { upsertCampaign, deleteCampaign } from "@/actions/communications/campaigns"; import { getCommunicationTemplates } from "@/actions/communications/templates"; import { getCommunicationSegments, type Segment } from "@/actions/communications/segments"; const CAMPAIGN_TYPES: { value: CampaignType; label: string }[] = [ { value: "operational", label: "Operational" }, { value: "marketing", label: "Marketing" }, { value: "transactional", label: "Transactional" }, ]; const STATUS_COLORS: Record = { draft: "bg-stone-100 text-stone-600", scheduled: "bg-blue-100 text-blue-700", sending: "bg-yellow-100 text-yellow-700", sent: "bg-emerald-100 text-emerald-700", canceled: "bg-red-100 text-red-700", }; const TYPE_COLORS: Record = { operational: "bg-purple-100 text-purple-700", marketing: "bg-amber-100 text-amber-700", transactional: "bg-sky-100 text-sky-700", }; // Icon components const Icons = { plus: (className: string) => ( ), x: (className: string) => ( ), mail: (className: string) => ( ), arrowRight: (className: string) => ( ), }; // ────────────────────────────────────────────────────────────────────────── // Campaign Edit Panel state (useReducer) // ────────────────────────────────────────────────────────────────────────── type State = { saving: boolean; sending: boolean; name: string; subject: string; bodyText: string; templateId: string; campaignType: CampaignType; audienceTarget: string; stopId: string; dateFrom: string; dateTo: string; scheduleMode: "now" | "later"; scheduledAt: string; segments: Segment[]; selectedSegmentId: string; preview: AudiencePreviewResult | null; previewLoading: boolean; error: string; nowMin: string; }; type Action = | { type: "SET_SAVING"; value: boolean } | { type: "SET_SENDING"; value: boolean } | { type: "SET_NAME"; value: string } | { type: "SET_SUBJECT"; value: string } | { type: "SET_BODY_TEXT"; value: string } | { type: "SET_TEMPLATE_ID"; value: string } | { type: "SET_CAMPAIGN_TYPE"; value: CampaignType } | { type: "SET_AUDIENCE_TARGET"; value: string; resetSegment: boolean } | { type: "SET_STOP_ID"; value: string } | { type: "SET_DATE_FROM"; value: string } | { type: "SET_DATE_TO"; value: string } | { type: "SET_SCHEDULE_MODE"; value: "now" | "later" } | { type: "SET_SCHEDULED_AT"; value: string } | { type: "SET_SEGMENTS"; value: Segment[] } | { type: "SET_SELECTED_SEGMENT_ID"; value: string } | { type: "APPLY_AUDIENCE_FROM_SEGMENT"; target: string; stop_id?: string; date_from?: string; date_to?: string; selectedSegmentId: string; } | { type: "SET_PREVIEW"; value: AudiencePreviewResult | null } | { type: "SET_PREVIEW_LOADING"; value: boolean } | { type: "SET_ERROR"; value: string } | { type: "SET_NOW_MIN"; value: string }; function makeInitialState(campaign?: Campaign): State { return { saving: false, sending: false, name: campaign?.name ?? "", subject: campaign?.subject ?? "", bodyText: campaign?.body_text ?? "", templateId: campaign?.template_id ?? "", campaignType: campaign?.campaign_type ?? "operational", audienceTarget: campaign?.audience_rules?.target ?? "stop", stopId: campaign?.audience_rules?.stop_id ?? "", dateFrom: campaign?.audience_rules?.date_from ?? "", dateTo: campaign?.audience_rules?.date_to ?? "", scheduleMode: campaign?.scheduled_at ? "later" : "now", scheduledAt: campaign?.scheduled_at ? new Date(campaign.scheduled_at).toISOString().slice(0, 16) : "", segments: [], selectedSegmentId: "", preview: null, previewLoading: false, error: "", nowMin: "", }; } function reducer(state: State, action: Action): State { switch (action.type) { case "SET_SAVING": return { ...state, saving: action.value }; case "SET_SENDING": return { ...state, sending: action.value }; case "SET_NAME": return { ...state, name: action.value }; case "SET_SUBJECT": return { ...state, subject: action.value }; case "SET_BODY_TEXT": return { ...state, bodyText: action.value }; case "SET_TEMPLATE_ID": return { ...state, templateId: action.value }; case "SET_CAMPAIGN_TYPE": return { ...state, campaignType: action.value }; case "SET_AUDIENCE_TARGET": return { ...state, audienceTarget: action.value, selectedSegmentId: action.resetSegment ? "" : state.selectedSegmentId, }; case "SET_STOP_ID": return { ...state, stopId: action.value }; case "SET_DATE_FROM": return { ...state, dateFrom: action.value }; case "SET_DATE_TO": return { ...state, dateTo: action.value }; case "SET_SCHEDULE_MODE": return { ...state, scheduleMode: action.value }; case "SET_SCHEDULED_AT": return { ...state, scheduledAt: action.value }; case "SET_SEGMENTS": return { ...state, segments: action.value }; case "SET_SELECTED_SEGMENT_ID": return { ...state, selectedSegmentId: action.value }; case "APPLY_AUDIENCE_FROM_SEGMENT": return { ...state, audienceTarget: action.target, stopId: action.stop_id ?? state.stopId, dateFrom: action.date_from ?? state.dateFrom, dateTo: action.date_to ?? state.dateTo, selectedSegmentId: action.selectedSegmentId, }; case "SET_PREVIEW": return { ...state, preview: action.value }; case "SET_PREVIEW_LOADING": return { ...state, previewLoading: action.value }; case "SET_ERROR": return { ...state, error: action.value }; case "SET_NOW_MIN": return { ...state, nowMin: action.value }; } } // ────────────────────────────────────────────────────────────────────────── // New Campaign Modal (kept untouched - already small) // ────────────────────────────────────────────────────────────────────────── function NewCampaignModal({ isOpen, onClose, onSuccess, brandId }: { isOpen: boolean; onClose: () => void; onSuccess: () => void; brandId: string; }) { const router = useRouter(); const [saving, setSaving] = useState(false); const [name, setName] = useState(""); const [campaignType, setCampaignType] = useState("operational"); const [error, setError] = useState(""); if (!isOpen) return null; const handleCreate = async () => { if (!name.trim()) { setError("Campaign name is required"); return; } setSaving(true); setError(""); const result = await upsertCampaign({ brand_id: brandId, name: name.trim(), subject: "", body_text: "", campaign_type: campaignType, status: "draft", audience_rules: { target: "all_customers" }, }); setSaving(false); if (!result.success) { setError(result.error ?? "Failed to create campaign"); return; } setName(""); setCampaignType("operational"); onSuccess(); router.refresh(); }; return (
{/* Backdrop */}
{/* Content */}
{error && (
{error}
)}
setName(e.target.value)} className="w-full border border-[var(--admin-border)] rounded-lg px-3 py-2.5 text-sm bg-white text-[var(--admin-text-primary)] focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 outline-none" placeholder="e.g. Weekly Pickup Reminder" />
{/* Footer */}
); } // ────────────────────────────────────────────────────────────────────────── // Campaign List Panel (kept untouched - already small) // ────────────────────────────────────────────────────────────────────────── export default function CampaignListPanel({ initialCampaigns, brandId = "64294306-5f42-463d-a5e8-2ad6c81a96de" }: { initialCampaigns: Campaign[], brandId?: string }) { const router = useRouter(); const [campaigns, setCampaigns] = useState(initialCampaigns); const [filterType, setFilterType] = useState("all"); const [filterStatus, setFilterStatus] = useState("all"); const [showNewModal, setShowNewModal] = useState(false); const filtered = campaigns.filter((c) => { if (filterType !== "all" && c.campaign_type !== filterType) return false; if (filterStatus !== "all" && c.status !== filterStatus) return false; return true; }); return (
setShowNewModal(false)} onSuccess={() => { setShowNewModal(false); router.refresh(); }} brandId={brandId} /> {/* Header */}
{Icons.mail("w-4 h-4 text-[var(--admin-bg)]")}

Campaigns

{filtered.length} campaign{filtered.length !== 1 ? "s" : ""}

{/* Filters */}
{/* Table */} {filtered.length === 0 ? (
No campaigns found
) : (
{filtered.map((c) => ( router.push(`/admin/communications/campaigns/${c.id}`)} aria-label={`View campaign ${c.name}`} > ))}
Name Type Status Created Sent
{c.name} {c.campaign_type} {c.status} {formatDate(new Date(c.created_at))} {c.sent_at ? formatDate(new Date(c.sent_at)) : "—"} {Icons.arrowRight("h-4 w-4 text-[var(--admin-text-muted)]")}
)}
); } // ────────────────────────────────────────────────────────────────────────── // Campaign Edit Panel (refactored — useReducer + subcomponents) // ────────────────────────────────────────────────────────────────────────── export function CampaignEditPanel({ campaign, templates, mode, brandId, }: { campaign?: Campaign; templates: Template[]; mode: "edit" | "new"; brandId: string; }) { const router = useRouter(); const [state, dispatch] = useReducer(reducer, campaign, makeInitialState); const bodyHtmlRef = useRef(campaign?.body_html ?? ""); // Load saved segments on mount useEffect(() => { getCommunicationSegments(brandId).then((result) => { if (result.success) dispatch({ type: "SET_SEGMENTS", value: result.segments }); }); }, [brandId]); // Compute the "now" floor for the datetime-local input. This must run // only in the browser to avoid hydration mismatches. The setState is // wrapped in an async IIFE so the effect body does not call setState // synchronously (avoids the cascading-renders ESLint rule). useEffect(() => { void (async () => { dispatch({ type: "SET_NOW_MIN", value: new Date().toISOString().slice(0, 16) }); })(); }, []); // When a segment is selected, populate audience fields in one reducer call function applySegment(segmentId: string) { if (!segmentId) { dispatch({ type: "SET_SELECTED_SEGMENT_ID", value: "" }); return; } const seg = state.segments.find((s) => s.id === segmentId); if (!seg) { dispatch({ type: "SET_SELECTED_SEGMENT_ID", value: segmentId }); return; } const rules = seg.rules; dispatch({ type: "APPLY_AUDIENCE_FROM_SEGMENT", target: rules.target ?? state.audienceTarget, stop_id: rules.stop_id, date_from: rules.date_from, date_to: rules.date_to, selectedSegmentId: segmentId, }); } const loadPreview = async () => { dispatch({ type: "SET_PREVIEW_LOADING", value: true }); const rules: AudienceRules = { target: state.audienceTarget as AudienceRules["target"], stop_id: state.stopId || undefined, date_from: state.dateFrom || undefined, date_to: state.dateTo || undefined, }; const result = await previewCampaignAudience(brandId, rules); dispatch({ type: "SET_PREVIEW", value: result }); dispatch({ type: "SET_PREVIEW_LOADING", value: false }); }; const applyTemplate = (t: Template) => { dispatch({ type: "SET_SUBJECT", value: t.subject }); dispatch({ type: "SET_BODY_TEXT", value: t.body_text }); bodyHtmlRef.current = t.body_html ?? ""; dispatch({ type: "SET_CAMPAIGN_TYPE", value: (t.campaign_type as CampaignType) ?? "operational", }); }; const handleSave = async () => { dispatch({ type: "SET_SAVING", value: true }); dispatch({ type: "SET_ERROR", value: "" }); const rules: AudienceRules = { target: state.audienceTarget as AudienceRules["target"], stop_id: state.stopId || undefined, date_from: state.dateFrom || undefined, date_to: state.dateTo || undefined, }; const effectiveScheduledAt = state.scheduleMode === "later" && state.scheduledAt ? new Date(state.scheduledAt).toISOString() : null; const result = await upsertCampaign({ id: campaign?.id, brand_id: brandId, name: state.name, subject: state.subject, body_text: state.bodyText, body_html: bodyHtmlRef.current || undefined, template_id: state.templateId || undefined, campaign_type: state.campaignType, status: state.scheduleMode === "later" ? "scheduled" : (campaign?.status ?? "draft"), audience_rules: rules, scheduled_at: effectiveScheduledAt ?? undefined, }); dispatch({ type: "SET_SAVING", value: false }); if (!result.success) { dispatch({ type: "SET_ERROR", value: result.error ?? "Failed to save" }); return; } router.push("/admin/communications"); router.refresh(); }; const handleSend = async () => { dispatch({ type: "SET_SENDING", value: true }); dispatch({ type: "SET_ERROR", value: "" }); if (!campaign?.id) { dispatch({ type: "SET_ERROR", value: "Save as draft first" }); dispatch({ type: "SET_SENDING", value: false }); return; } const result = await sendCampaign(campaign.id, brandId); dispatch({ type: "SET_SENDING", value: false }); if (!result.success) { dispatch({ type: "SET_ERROR", value: result.error ?? "Failed to send" }); return; } router.push("/admin/communications"); router.refresh(); }; return (
{state.error && (
{state.error}
)}
dispatch({ type: "SET_NAME", value: v })} onSetCampaignType={(v) => dispatch({ type: "SET_CAMPAIGN_TYPE", value: v })} onTemplateChange={(templateId) => { dispatch({ type: "SET_TEMPLATE_ID", value: templateId }); const t = templates.find((t) => t.id === templateId); if (t) applyTemplate(t); }} /> dispatch({ type: "SET_AUDIENCE_TARGET", value: v, resetSegment: true }) } onSetStopId={(v) => dispatch({ type: "SET_STOP_ID", value: v })} onSetDateFrom={(v) => dispatch({ type: "SET_DATE_FROM", value: v })} onSetDateTo={(v) => dispatch({ type: "SET_DATE_TO", value: v })} onApplySegment={applySegment} onPreview={loadPreview} /> dispatch({ type: "SET_SUBJECT", value: v })} onSetBodyText={(v) => dispatch({ type: "SET_BODY_TEXT", value: v })} /> dispatch({ type: "SET_SCHEDULE_MODE", value: v })} onSetScheduledAt={(v) => dispatch({ type: "SET_SCHEDULED_AT", value: v })} /> 0} scheduleMode={state.scheduleMode} scheduledAt={state.scheduledAt} campaign={campaign} onSave={handleSave} onSend={handleSend} />
); } // ── EditHeader subcomponent ──────────────────────────────────────────────── function EditHeader({ mode }: { mode: "edit" | "new" }) { return (
{Icons.mail("w-4 h-4 text-[var(--admin-bg)]")}

{mode === "new" ? "New Campaign" : "Edit Campaign"}

{mode === "new" ? "Create a new email campaign" : "Update campaign settings"}

); } // ── BasicInfoSection subcomponent ────────────────────────────────────────── function BasicInfoSection({ name, campaignType, templateId, templates, onSetName, onSetCampaignType, onTemplateChange, }: { name: string; campaignType: CampaignType; templateId: string; templates: Template[]; onSetName: (v: string) => void; onSetCampaignType: (v: CampaignType) => void; onTemplateChange: (templateId: string) => void; }) { return (

Basic Info

onSetName(e.target.value)} className="w-full border border-[var(--admin-border)] rounded-lg px-3 py-2 text-sm bg-white text-[var(--admin-text-primary)]" placeholder="e.g. Tuxedo Stop 5/15 Reminder" />
); } // ── AudienceSection subcomponent ─────────────────────────────────────────── function AudienceSection({ audienceTarget, stopId, dateFrom, dateTo, segments, selectedSegmentId, preview, previewLoading, onSetAudienceTarget, onSetStopId, onSetDateFrom, onSetDateTo, onApplySegment, onPreview, }: { audienceTarget: string; stopId: string; dateFrom: string; dateTo: string; segments: Segment[]; selectedSegmentId: string; preview: AudiencePreviewResult | null; previewLoading: boolean; onSetAudienceTarget: (v: string) => void; onSetStopId: (v: string) => void; onSetDateFrom: (v: string) => void; onSetDateTo: (v: string) => void; onApplySegment: (id: string) => void; onPreview: () => void; }) { return (

Audience

{segments.length > 0 && (
Saved segment:
)}
{audienceTarget === "stop" && (
onSetStopId(e.target.value)} className="w-full border border-[var(--admin-border)] rounded-lg px-3 py-2 text-sm bg-white text-[var(--admin-text-primary)]" placeholder="UUID" />
onSetDateFrom(e.target.value)} className="w-full border border-[var(--admin-border)] rounded-lg px-3 py-2 text-sm bg-white text-[var(--admin-text-primary)]" />
onSetDateTo(e.target.value)} className="w-full border border-[var(--admin-border)] rounded-lg px-3 py-2 text-sm bg-white text-[var(--admin-text-primary)]" />
)} {preview && (
{preview.count} customers match this audience {preview.sample_customers.length > 0 && (
Sample: {preview.sample_customers.slice(0, 5).map((c) => c.email).join(", ")}
)}
)}
); } // ── MessageSection subcomponent ──────────────────────────────────────────── function MessageSection({ subject, bodyText, onSetSubject, onSetBodyText, }: { subject: string; bodyText: string; onSetSubject: (v: string) => void; onSetBodyText: (v: string) => void; }) { return (

Message Content

onSetSubject(e.target.value)} className="w-full border border-[var(--admin-border)] rounded-lg px-3 py-2 text-sm bg-white text-[var(--admin-text-primary)]" placeholder="Email subject line" />