import React, { useState, useMemo, useCallback } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { __ } from "../lib/i18n"; import { deleteEmailSequence, fetchEmailLogs, fetchEmailSequences, updateEmailSequenceStatus, } from "../api/email-automation-api"; import { useToast } from "../components/ui/toast"; import { Card, CardContent, CardFooter, CardHeader, CardTitle, } from "../components/ui/card"; import { Button } from "../components/ui/button"; import { Badge } from "../components/ui/badge"; import { PageHeader } from "../components/common/PageHeader"; import { ConfirmationDialog } from "../components/ui/confirmation-dialog"; import { Modal } from "../components/ui/modal"; import { Alert } from "../components/ui/alert"; import { Label } from "../components/ui/label"; import PremiumUpgradeCard from "./premium-pages/EmailAutomation"; import { Mail, Edit, Clock, FileText, Bell, Plus, Trash2, Play, Pause, Zap, GitBranch, Loader2, Save, Settings2, Send, } from "lucide-react"; import { isProPluginActive, isEmailAutomationModuleEnabled, } from "../lib/plugin-utils"; import { useEmailSettingsManager } from "../hooks/useEmailSettingsManager"; import { EmailDeliverySection } from "../components/settings/EmailDeliverySection"; import { EmailTemplatesList } from "../components/email/EmailTemplatesList"; import { Pagination } from "../components/shared"; interface EmailLog { id: number; template_id?: number | null; template_key: string; sequence_id?: number | null; recipient_email: string; recipient_name: string; subject: string; // Full HTML body — only loaded when the detail modal asks for it // (the list query also returns it, but it can be large; the modal // is the only consumer of this field). body?: string; context_type?: string | null; context_id?: number | null; status: "sent" | "failed" | "opened" | "clicked"; error_message?: string | null; // Server-side JSON-encoded — may arrive as a string OR a parsed object // depending on the response shape. metadata?: string | Record | null; sent_at: string; } interface EmailSequence { id: number; name: string; description: string; trigger_type: string; trigger_config: any; status: "active" | "paused" | "draft"; applicable_to: string; trip_ids: number[]; steps_count?: number; } const EmailSequencesList: React.FC = () => { const queryClient = useQueryClient(); const { showToast } = useToast(); const { data: sequencesData, isLoading } = useQuery({ queryKey: ["email-sequences"], queryFn: () => fetchEmailSequences(), enabled: isEmailAutomationModuleEnabled(), }); const deleteMutation = useMutation({ mutationFn: async (id: number) => { return await deleteEmailSequence(id); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["email-sequences"] }); showToast(__("Sequence deleted"), "success"); }, }); const toggleStatusMutation = useMutation({ mutationFn: async ({ id, status }: { id: number; status: string }) => { return await updateEmailSequenceStatus(id, status); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["email-sequences"] }); showToast(__("Sequence status updated"), "success"); }, }); const sequences = ( Array.isArray(sequencesData) ? sequencesData : [] ) as EmailSequence[]; const navigateToCreate = () => { window.location.href = "admin.php?page=yatra&subpage=email-automation&tab=sequence&action=create"; }; const navigateToEdit = (id: number) => { window.location.href = `admin.php?page=yatra&subpage=email-automation&tab=sequence&action=edit&id=${id}`; }; if (isLoading) { return (
{/* Header Skeleton */}
{/* Sequences List Skeleton */}
{[1, 2, 3].map((i) => (
))}
); } const getStatusBadge = (status: string) => { switch (status) { case "active": return ( {__("Active")} ); case "paused": return ( {__("Paused")} ); default: return ( {__("Draft")} ); } }; const emailEvents = (window as any).yatraAdmin?.emailEvents || []; const getTriggerLabel = (triggerType: string) => { const ev = emailEvents.find((e: { key?: string }) => e.key === triggerType); return ev?.name || triggerType; }; return (
{/* Header */}

{__("Email Sequences")}

{__("Create automated email workflows triggered by events")}

{/* Sequences List */} {sequences.length === 0 ? (

{__("No Sequences Yet")}

{__( "Create your first automated email sequence to engage customers at the right time.", )}

) : (
{sequences.map((sequence) => (

{sequence.name}

{getStatusBadge(sequence.status)}

{sequence.description || __("No description")}

{getTriggerLabel(sequence.trigger_type)} {sequence.steps_count || 0} {__("steps")}
{sequence.status === "active" ? ( ) : ( )}
))}
)}
); }; const EmailLogsList: React.FC = () => { const [page, setPage] = useState(1); const perPage = 20; // Modal state for the per-row "View details" action. Holds the // selected log row so the modal can render the full payload without // a second API roundtrip — the list query already returns body + // metadata. const [viewingLog, setViewingLog] = useState(null); const { data: logsData, isLoading } = useQuery({ queryKey: ["email-logs", page, perPage], queryFn: () => fetchEmailLogs({ page, per_page: perPage }), enabled: isEmailAutomationModuleEnabled(), }); const logs = (logsData?.items ?? []) as EmailLog[]; const totalItems = logsData?.total ?? 0; const totalPages = Math.max(1, Math.ceil(totalItems / perPage)); if (isLoading) { return (
{[1, 2, 3, 4, 5].map((i) => ( ))} {[1, 2, 3, 4, 5].map((row) => ( {[1, 2, 3, 4, 5].map((col) => ( ))} ))}
); } return ( {__("Email Logs")} {logs.length === 0 ? (
{__("No emails sent yet")}
) : (
{logs.map((log) => ( ))}
{__("Recipient")} {__("Subject")} {__("Template")} {__("Status")} {__("Sent")} {__("Actions")}
{log.recipient_name || "-"}
{log.recipient_email}
{log.subject} {log.template_key} {log.status} {log.sent_at ? new Date(log.sent_at).toLocaleString() : "—"}
)} {logs.length > 0 && totalPages > 1 && (
)}
{viewingLog && ( setViewingLog(null)} /> )}
); }; /* -------------------------------------------------------------------------- */ /* EmailLogDetailsModal */ /* */ /* Read-only inspector for a single email log row. Shows recipient, status, */ /* template, full HTML body, error message (if failed), and metadata JSON. */ /* */ /* Body is rendered inside a sandboxed iframe (srcdoc + sandbox="") so: */ /* 1. Operator-visible markup looks like the real email */ /* 2. The iframe can't navigate the parent, run scripts, or read storage */ /* 3. Plugin CSS doesn't leak into the preview (and vice versa) */ /* -------------------------------------------------------------------------- */ const EmailLogDetailsModal: React.FC<{ log: EmailLog; onClose: () => void; }> = ({ log, onClose }) => { // Body view toggle — "preview" renders the HTML in a sandboxed // iframe; "raw" shows the source markup in a
. Operators
  // debugging template variables / merge tags want the raw view to
  // verify substitutions; visual proofing wants the preview.
  const [bodyView, setBodyView] = useState<"preview" | "raw">("preview");
  // Tracks whether the raw HTML has been copied to clipboard so we
  // can flash a "Copied!" label briefly.
  const [copied, setCopied] = useState(false);
  const copyRawToClipboard = async () => {
    if (!log.body) return;
    try {
      await navigator.clipboard.writeText(log.body);
      setCopied(true);
      window.setTimeout(() => setCopied(false), 1500);
    } catch {
      /* clipboard denied — silently skip */
    }
  };

  // Parse metadata defensively — server stores it as a JSON string in
  // longtext, but some code paths may deliver it as a parsed object.
  const metadata: Record | null = useMemo(() => {
    if (!log.metadata) return null;
    if (typeof log.metadata === "object")
      return log.metadata as Record;
    try {
      const parsed = JSON.parse(String(log.metadata));
      return typeof parsed === "object" && parsed !== null ? parsed : null;
    } catch {
      return null;
    }
  }, [log.metadata]);

  const statusClass =
    log.status === "sent"
      ? "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"
      : log.status === "failed"
        ? "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
        : "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400";

  return (
    
          
          {__("Email details", "yatra")}
        
      }
      size="full"
      footer={
        
} >
{/* Header strip — status + sent_at + recipient at a glance */}
{log.status} {log.sent_at ? new Date(log.sent_at).toLocaleString() : "—"}
{log.recipient_name || "—"}
{log.recipient_email}
{log.subject || "—"} {log.template_key || "—"} {log.template_id ? ( (id {log.template_id}) ) : null} {log.sequence_id ? ( #{log.sequence_id} ) : ( {__("Transactional (no sequence)", "yatra")} )} {(log.context_type || log.context_id) && ( {log.context_type || "—"} {log.context_id ? ` #${log.context_id}` : ""} )}
{/* Error block — only shown on failed status */} {log.status === "failed" && log.error_message && (
              {log.error_message}
            
)} {/* Body section. Two view modes: */} {/* - Preview: sandboxed iframe so the email's own CSS / inline */} {/* styles render accurately without leaking into admin chrome. */} {/* `sandbox=""` (empty string) is the strictest mode — no */} {/* scripts, no forms, no top-nav, no clickable links. */} {/* - Raw: HTML source in a
 so operators can verify merge    */}
        {/*     tags, debug template variable substitution, copy for tickets. */}
        
{/* View-toggle pills. Single-purpose styling so the active */} {/* mode is unmistakable. */}
{bodyView === "raw" && log.body && ( )} {bodyView === "preview" && ( {__("Sandboxed — links not clickable", "yatra")} )}
{log.body && log.body.trim() !== "" ? ( bodyView === "preview" ? (