// ---------- Astro Analytics (Bizsuria) ----------
// Collections dashboard for the Bizsuria org running Astro arrears blasts:
// who was reached, who promised to pay (PTP), how much of the outstanding
// balance that covers, and which calls went to the manager (Fauzan) as a
// dispute. Built purely from /logs, which the server already scopes to the
// caller's org — so a client-mode user only ever sees their own campaigns.
//
// Dev mode has no such scoping, so it mirrors the server's own org rule
// (callLogStore logMatchesOrganization): a call counts only if it belongs to
// a client with the "astro" custom dashboard assigned, or ran on one of that
// client's assistants. Deliberately NOT matched by assistant/campaign name —
// internal test calls on "Farah"/"Astro"-named assistants would leak in.

const ASTRO_OUTCOME_DEFS = [
  { key: "ptp", color: "var(--ok)" },
  { key: "alreadyPaid", color: "var(--info)" },
  { key: "cbr", color: "var(--info)" },
  { key: "transferred", color: "var(--a)" },
  { key: "transferMissed", color: "var(--err)" },
  { key: "customerEnded", color: "var(--warn)" },
  { key: "agentEnded", color: "var(--a)" },
  { key: "noAnswer", color: "var(--c-text-4)" },
  { key: "failed", color: "var(--err)" },
  { key: "other", color: "var(--c-line-2)" },
];
Object.assign(window.STRINGS.en, { "astro.outcome.agentEnded": "AI ended call" });
Object.assign(window.STRINGS.ms, { "astro.outcome.agentEnded": "AI tamatkan panggilan" });
Object.assign(window.STRINGS.en, {
  "astro.outcome.transferMissed": "Manager not reached",
  "astro.stat.transferred": "Manager transfers",
  "astro.stat.transferredMeta": "{{answered}} answered · {{missed}} not reached · click to list",
  "astro.info.stat.transferred": "Every call the AI tried to hand to a manager, whatever the call's final outcome. Answered means a manager actually picked up.",
  "astro.card.transfers.title": "Manager transfers",
  "astro.card.transfers.subtitle": "{{answered}} of {{tracked}} answered ({{pct}}%) · click a row",
  "astro.info.transfers": "How each transfer to a manager ended, read from the call itself. Answered: a manager picked up. Didn't answer: it rang until the time limit. Declined or line busy: the manager's line ended the ringing. Customer hung up while waiting: they left before anyone answered. Failed: the transfer could not start. Not tracked: older calls that did not record the result. Answer rate leaves out not-tracked calls.",
  "astro.transfers.result": "Result",
  "astro.transfers.byManager": "By manager",
  "astro.transfers.byHour": "By hour (Malaysia time)",
  "astro.transfers.answeredOf": "{{answered}} of {{total}} answered",
  "astro.transfers.waiting": "{{count}} debtors waiting for a call back · {{amount}}",
  "astro.transferResult.answered": "Manager answered",
  "astro.transferResult.noAnswer": "Manager didn't answer",
  "astro.transferResult.declined": "Declined or line busy",
  "astro.transferResult.abandoned": "Customer hung up while waiting",
  "astro.transferResult.failed": "Transfer failed",
  "astro.transferResult.untracked": "Not tracked (older call)",
  "astro.action.managerCallback": "Manager to call back",
});
Object.assign(window.STRINGS.ms, {
  "astro.outcome.transferMissed": "Pengurus tidak dapat dihubungi",
  "astro.stat.transferred": "Pindahan kepada pengurus",
  "astro.stat.transferredMeta": "{{answered}} dijawab · {{missed}} tidak dapat dihubungi · klik untuk senarai",
  "astro.info.stat.transferred": "Setiap panggilan yang AI cuba serahkan kepada pengurus, apa pun keputusan akhir panggilan. Dijawab bermaksud pengurus benar-benar mengangkat panggilan.",
  "astro.card.transfers.title": "Pindahan kepada pengurus",
  "astro.card.transfers.subtitle": "{{answered}} daripada {{tracked}} dijawab ({{pct}}%) · klik satu baris",
  "astro.info.transfers": "Bagaimana setiap pindahan kepada pengurus berakhir, dibaca daripada panggilan itu sendiri. Dijawab: pengurus mengangkat panggilan. Tidak jawab: berdering hingga had masa. Ditolak atau talian sibuk: talian pengurus menamatkan deringan. Pelanggan letak telefon semasa menunggu: pelanggan keluar sebelum dijawab. Gagal: pindahan tidak dapat dimulakan. Tidak direkod: panggilan lama yang tidak merekod keputusan. Kadar jawapan tidak mengira panggilan tidak direkod.",
  "astro.transfers.result": "Keputusan",
  "astro.transfers.byManager": "Ikut pengurus",
  "astro.transfers.byHour": "Ikut jam (waktu Malaysia)",
  "astro.transfers.answeredOf": "{{answered}} daripada {{total}} dijawab",
  "astro.transfers.waiting": "{{count}} penghutang menunggu panggilan semula · {{amount}}",
  "astro.transferResult.answered": "Pengurus jawab",
  "astro.transferResult.noAnswer": "Pengurus tidak jawab",
  "astro.transferResult.declined": "Ditolak atau talian sibuk",
  "astro.transferResult.abandoned": "Pelanggan letak telefon semasa menunggu",
  "astro.transferResult.failed": "Pindahan gagal",
  "astro.transferResult.untracked": "Tidak direkod (panggilan lama)",
  "astro.action.managerCallback": "Pengurus perlu hubungi semula",
});
const ASTRO_OUTCOME_COLOR = Object.fromEntries(ASTRO_OUTCOME_DEFS.map(d => [d.key, d.color]));

// Payment status + reason come from the post-call AI review
// (callAnalysisService.js collectionStatus/collectionReason), which runs a few
// minutes after each call ends — so fresh calls show as "not yet analysed".
const ASTRO_COLLECTION_DEFS = [
  { key: "promised", color: "var(--ok)" },
  { key: "already_paid", color: "var(--info)" },
  { key: "cannot_pay", color: "var(--warn)" },
  { key: "refused", color: "var(--err)" },
  { key: "dispute", color: "var(--err)" },
  { key: "callback", color: "var(--a)" },
  { key: "wrong_number", color: "var(--c-text-4)" },
  { key: "not_available", color: "var(--a)" },
  { key: "wrong_person", color: "var(--c-text-4)" },
  { key: "no_conversation", color: "var(--c-line-2)" },
  { key: "other", color: "var(--c-line-2)" },
];

// Same set outbound.js isRetryableEndedReason() treats as "not connected",
// so connection rate here matches the Blast page's Connected column.
const ASTRO_NOT_CONNECTED = new Set([
  "voicemail",
  "customer-did-not-answer",
  "customer-busy",
  "silence-timed-out",
  "failed-to-connect",
  "call.error-processing-failed",
]);

const ASTRO_DURATION_BUCKETS = [
  { key: "lt30", max: 30 },
  { key: "30to60", max: 60 },
  { key: "1to2m", max: 120 },
  { key: "2to5m", max: 300 },
  { key: "gt5m", max: Infinity },
];

function astroEndedReason(log) {
  return String(log?.endedReason || "").trim().toLowerCase();
}

function astroIsConnected(log) {
  const reason = astroEndedReason(log);
  return Boolean(reason) && !ASTRO_NOT_CONNECTED.has(reason) && !reason.includes("fail") && !reason.includes("error");
}

function astroExpectedPaymentDate(log) {
  return log?.expected_payment_date || log?.structuredOutput?.expected_payment_date || "";
}

// How a transfer to a manager ended. The agent writes marker turns and a
// humanTakeover record; nothing is written when the customer hangs up during
// the ring, and older calls have no record at all.
const ASTRO_TRANSFER_RESULTS = [
  { key: "answered", color: "var(--ok)" },
  { key: "noAnswer", color: "var(--err)" },
  { key: "declined", color: "var(--err)" },
  { key: "abandoned", color: "var(--warn)" },
  { key: "failed", color: "var(--err)" },
  { key: "untracked", color: "var(--c-line-2)" },
];
const ASTRO_TRANSFER_MISSED = new Set(["noAnswer", "declined", "abandoned", "failed"]);

function astroTransferResult(log) {
  const markers = (log?.turns || []).map(t => String(t.assistant || ""));
  const has = re => markers.some(m => re.test(m));
  const takeover = log?.humanTakeover;
  if (has(/\[TRANSFER ANSWERED/) || takeover?.answered === true) return "answered";
  if (has(/\[TRANSFER NOT ANSWERED/) || takeover?.notAnswered) return takeover?.notAnswered === "rejected" ? "declined" : "noAnswer";
  if (has(/\[TRANSFER INITIATED/)) return takeover ? "abandoned" : "untracked";
  if (has(/\[TRANSFER FAILED/)) return "failed";
  return log?.transferredTo || astroEndedReason(log).includes("transfer") ? "untracked" : "";
}

function astroTransferAgent(log) {
  const name = String(log?.humanTakeover?.agentName || "").trim();
  if (name) return name;
  for (const t of log?.turns || []) {
    const m = String(t.assistant || "").match(/\[TRANSFER (?:NOT ANSWERED|ANSWERED|FAILED) → ([^:\]]+)/);
    if (m) return m[1].trim();
  }
  return String(log?.transferredTo || "").trim() || "—";
}

function astroOutcomeKey(log) {
  const outcome = String(log?.outcome || log?.structuredOutput?.outcome || "").trim().toLowerCase();
  const reason = astroEndedReason(log);
  // A customer saying they already paid is not a promise to pay, even when a
  // (past) date got captured as expected_payment_date during the call.
  if (log?.aiAnalysis?.collectionStatus === "already_paid") return "alreadyPaid";
  if (outcome === "ptp" || astroExpectedPaymentDate(log)) return "ptp";
  // Ahead of the ended reason: a transfer nobody answered ends as
  // customer-ended-call or assistant-ended-call.
  const transfer = astroTransferResult(log);
  if (ASTRO_TRANSFER_MISSED.has(transfer)) return "transferMissed";
  if (transfer) return "transferred";
  if (outcome === "cbr") return "cbr";
  if (reason.includes("fail") || reason.includes("error")) return "failed";
  if (ASTRO_NOT_CONNECTED.has(reason)) return "noAnswer";
  if (reason === "customer-ended-call") return "customerEnded";
  if (reason === "assistant-ended-call") return "agentEnded";
  return "other";
}

// Balances arrive as whatever the CSV held: "156", "1,316.15", "RM 89.90".
function astroParseBalance(value) {
  if (value == null || value === "") return null;
  const n = Number(String(value).replace(/[^\d.]/g, ""));
  return Number.isFinite(n) ? n : null;
}

function astroLogBalance(log) {
  return astroParseBalance(log?.balance ?? log?.structuredOutput?.balance ?? (csvRowValue(log?.campaignRow, "balance") || null));
}

function astroPhone(log) {
  return callPhoneNumber(log);
}

// One debtor can be dialled several times (retries, reblasts) — balances and
// PTP are counted once per campaign+number, not once per call.
// One debtor = one Astro account number: that is what Astro payment files are
// matched on (promise-kept checks), and it dedupes reblasts across campaigns.
// Rows without an account fall back to campaign + phone.
function astroContactKey(log) {
  const account = astroAccount(log).trim();
  if (account) return `akaun|${account}`;
  // No account number: the same phone in another campaign/reblast is the same
  // debtor (keying on campaign + phone counted their balance once per reblast).
  const digits = String(astroPhone(log) || "").replace(/D/g, "").slice(-9);
  return digits ? `tel|${digits}` : `log|${log?.id || ""}`;
}

function astroFmtMoney(n) {
  if (n == null || !Number.isFinite(n)) return "—";
  return `RM ${n.toLocaleString("en-MY", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
}

function astroFmtTalkTime(seconds) {
  const s = Math.round(seconds || 0);
  const h = Math.floor(s / 3600);
  const m = Math.floor((s % 3600) / 60);
  return h > 0 ? `${h}h ${m}m` : `${m}m ${s % 60}s`;
}

function astroPct(part, whole) {
  return whole > 0 ? Math.round((part / whole) * 100) : 0;
}

function astroDurationBucket(log) {
  const dur = Number(log?.durationSeconds) || 0;
  if (dur <= 0) return null;
  return (ASTRO_DURATION_BUCKETS.find(b => dur < b.max) || ASTRO_DURATION_BUCKETS[ASTRO_DURATION_BUCKETS.length - 1]).key;
}

function astroAttemptBucket(attempts) {
  return attempts >= 4 ? "4+" : String(attempts);
}

// Turns where the agent had to ask the caller to repeat because speech
// recognition returned nothing usable (logged with asrMiss by the voice agent;
// older calls are matched on the repair line itself).
function astroAsrMissTurns(log) {
  return (log?.turns || []).filter(t => t.asrMiss || /tak berapa dengar|didn't quite catch/i.test(String(t.assistant || ""))).length;
}

function astroAccount(log) {
  return String(csvRowValue(log?.campaignRow, "akaun_number") || log?.structuredOutput?.akaun_number || "");
}

function astroCallerTurns(log) {
  return (log?.turns || []).filter(t => String(t.user || "").trim()).length;
}

// "25/10/2026" or "2026-10-25" → Date at local midnight, else null.
function astroParsePtpDate(value) {
  const raw = String(value || "").trim();
  let m = raw.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
  if (m) return new Date(Number(m[3]), Number(m[2]) - 1, Number(m[1]));
  m = raw.match(/^(\d{4})-(\d{2})-(\d{2})/);
  if (m) return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
  return null;
}

// ---------- Insights: calendar, funnel, next actions, timing, bands, compliance ----------

// The AI review's promise date (taken from the agent's confirmed read-back)
// wins over the live extractor's, which can misread relative dates.
function astroPromiseDate(log) {
  return log?.aiAnalysis?.promisedPayDate || astroExpectedPaymentDate(log) || "";
}

function astroIsPromise(log, outcome) {
  return log?.aiAnalysis?.collectionStatus === "promised" || outcome === "ptp";
}

function astroDayKey(d) {
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
}

// Malaysia is UTC+8 all year (no DST): shift and read the UTC fields, so the
// heatmap and the calling-hours check use Malaysian time in any browser.
function astroMySlot(log) {
  const ms = new Date(log?.startedAt || 0).getTime();
  if (!Number.isFinite(ms) || ms <= 0) return null;
  const d = new Date(ms + 8 * 3600000);
  return { dow: d.getUTCDay(), hour: d.getUTCHours() };
}

// Calling window used for the compliance check (Malaysia time).
const ASTRO_CALL_WINDOW = { from: 8, to: 21 };
function astroOutsideHours(log) {
  const s = astroMySlot(log);
  return Boolean(s) && (s.hour < ASTRO_CALL_WINDOW.from || s.hour >= ASTRO_CALL_WINDOW.to);
}

const ASTRO_HEAT_HOURS = Array.from({ length: ASTRO_CALL_WINDOW.to - ASTRO_CALL_WINDOW.from }, (_, i) => i + ASTRO_CALL_WINDOW.from);
const ASTRO_HEAT_DAYS = [1, 2, 3, 4, 5, 6, 0];

// How far each call got. Index = furthest step reached.
const ASTRO_FUNNEL_STEPS = ["dialled", "answered", "spoke", "heardBalance", "replied", "outcome", "promised"];
const ASTRO_CLEAR_OUTCOMES = new Set(["promised", "already_paid", "cannot_pay", "refused", "dispute", "callback", "wrong_person", "wrong_number", "not_available"]);
const ASTRO_DISCLOSURE = /tunggakan|bagi pihak astro|outstanding|arrears/i;
function astroFunnelStage(log, outcome) {
  if (!astroIsConnected(log)) return 0;
  const turns = log?.turns || [];
  if (!turns.some(t => String(t.user || "").trim())) return 1;
  const di = turns.findIndex(t => ASTRO_DISCLOSURE.test(String(t.assistant || "")));
  if (di < 0) return 2;
  // Within a turn record the caller's line comes before the agent's reply,
  // so replies to the disclosure live in the later records.
  if (!turns.slice(di + 1).some(t => String(t.user || "").trim())) return 3;
  const promised = astroIsPromise(log, outcome);
  if (!promised && !ASTRO_CLEAR_OUTCOMES.has(log?.aiAnalysis?.collectionStatus)) return 4;
  return promised ? 6 : 5;
}

const ASTRO_BALANCE_BANDS = [
  { key: "lt100", max: 100 },
  { key: "100to500", max: 500 },
  { key: "500to1000", max: 1000 },
  { key: "gt1000", max: Infinity },
];
function astroBandKey(balance) {
  return balance == null ? null : ASTRO_BALANCE_BANDS.find(b => balance < b.max).key;
}

const ASTRO_SIGNALS = ["hardship", "service_complaint", "wants_cancel", "moved", "legal_threat", "vulnerable", "deceased"];
const ASTRO_LANGUAGES = ["ms", "en", "zh", "ta", "mixed"];

// One suggested next step per debtor, from their latest clear call outcome.
// Order = priority in the action list.
const ASTRO_ACTION_DEFS = [
  { key: "managerCallback", color: "var(--err)" },
  { key: "escalateDispute", color: "var(--err)" },
  { key: "verifyPayment", color: "var(--info)" },
  { key: "promiseDue", color: "var(--warn)" },
  { key: "callBack", color: "var(--a)" },
  { key: "awaitPromise", color: "var(--ok)" },
  { key: "hardship", color: "var(--warn)" },
  { key: "refused", color: "var(--err)" },
  { key: "callAgain", color: "var(--info)" },
  { key: "retry", color: "var(--c-line-2)" },
  { key: "unreachable", color: "var(--c-text-4)" },
  { key: "wrongPerson", color: "var(--c-text-4)" },
  { key: "invalidNumber", color: "var(--c-text-4)" },
];
const ASTRO_ACTION_COLOR = Object.fromEntries(ASTRO_ACTION_DEFS.map(d => [d.key, d.color]));
const ASTRO_BAD_NUMBER_ACTIONS = new Set(["wrongPerson", "invalidNumber", "unreachable"]);

function astroNextAction(c, todayMs) {
  // The customer asked for a person (the AI said we'd call back) and got
  // nobody. A later promise, payment or wrong number settles it instead.
  if (c.transferMissed && !["promised", "already_paid", "wrong_person", "wrong_number"].includes(c.status)) return "managerCallback";
  switch (c.status) {
    case "dispute": return "escalateDispute";
    case "already_paid": return "verifyPayment";
    case "promised": {
      const d = astroParsePtpDate(c.ptpDate);
      return d && d.getTime() < todayMs ? "promiseDue" : "awaitPromise";
    }
    case "callback": return "callBack";
    case "cannot_pay": return "hardship";
    case "refused": return "refused";
    case "wrong_person": return "wrongPerson";
    case "wrong_number": return "wrongPerson";
    case "not_available": return "callBack";
    default: break;
  }
  if (c.transferred) return "escalateDispute";
  if (c.ptp) return "awaitPromise";
  if (c.connected) return "callAgain";
  if (c.allFailedToConnect && c.attempts >= 2) return "invalidNumber";
  return c.attempts >= 3 ? "unreachable" : "retry";
}

function astroDownloadCsv(filename, header, rows) {
  const esc = v => `"${String(v ?? "").replace(/"/g, '""')}"`;
  const body = [header.map(esc).join(","), ...rows.map(r => r.map(esc).join(","))].join("\n");
  const a = document.createElement("a");
  a.href = URL.createObjectURL(new Blob(["﻿" + body], { type: "text/csv;charset=utf-8" }));
  a.download = filename;
  a.click();
  const href = a.href;
  setTimeout(() => URL.revokeObjectURL(href), 1000);
}

const ASTRO_RANGE_PRESETS = ["today", "7d", "30d", "90d", "all"];
const ASTRO_TABS = ["overview", "actions", "unreachable", "ai", "quality", "reach"];

function AstroLegendRow({ color, label, count, total, active, onClick }) {
  return (
    <div
      className="row"
      onClick={onClick}
      style={{
        justifyContent: "space-between", fontSize: 12, cursor: onClick ? "pointer" : undefined,
        padding: "4px 6px", margin: "-4px -6px", borderRadius: 6,
        background: active ? "rgba(141,139,255,0.09)" : "transparent",
      }}
    >
      <span className="row gap-2 muted">
        <span style={{ width: 8, height: 8, borderRadius: 2, background: color }} />
        <span style={active ? { fontWeight: 600, color: "var(--c-text)" } : undefined}>{label}</span>
      </span>
      <span className="mono">{count.toLocaleString()} · {astroPct(count, total)}%</span>
    </div>
  );
}

// Small (i) next to a title: hover (desktop) or tap (mobile) to read what the
// analytic means. Stops the click so it never triggers a tile's drill-down.
function AstroInfo({ text }) {
  const [open, setOpen] = React.useState(false);
  if (!text) return null;
  return (
    <span
      style={{ position: "relative", display: "inline-flex", marginLeft: 6, verticalAlign: "middle" }}
      onMouseEnter={() => setOpen(true)}
      onMouseLeave={() => setOpen(false)}
      onClick={e => { e.stopPropagation(); setOpen(o => !o); }}
    >
      <span aria-label={text} style={{
        width: 14, height: 14, borderRadius: "50%", border: "1px solid var(--c-text-4)", color: "var(--c-text-4)",
        fontSize: 9, lineHeight: "12px", textAlign: "center", cursor: "help", fontWeight: 700, fontStyle: "normal",
        textTransform: "none", letterSpacing: "normal",
      }}>i</span>
      {open && (
        <span role="tooltip" style={{
          position: "absolute", top: 20, left: -10, zIndex: 60, width: 280, padding: "8px 10px", borderRadius: 8,
          background: "#1f2330", color: "#fff", fontSize: 11, fontWeight: 400, lineHeight: 1.45, whiteSpace: "normal",
          boxShadow: "0 6px 20px rgba(0,0,0,0.28)", textTransform: "none", letterSpacing: "normal", textAlign: "left",
        }}>{text}</span>
      )}
    </span>
  );
}

const AstroAnalyticsView = ({ mode, callLogs = [], assistants = [], clients = [], lang = "en", testCallsToggle = null }) => {
  const t = makeT(lang);
  const isClient = mode === "client";
  const [rangePreset, setRangePreset] = React.useState("30d");
  const [customFrom, setCustomFrom] = React.useState("");
  const [customTo, setCustomTo] = React.useState("");
  const [filterClientId, setFilterClientId] = React.useState("");
  const [filterCampaign, setFilterCampaign] = React.useState("");
  const [filterOutcome, setFilterOutcome] = React.useState("");
  const [filterConnected, setFilterConnected] = React.useState("");
  const [filterCollection, setFilterCollection] = React.useState("");
  const [filterReason, setFilterReason] = React.useState("");
  const [searchQuery, setSearchQuery] = React.useState("");
  const [showAllRows, setShowAllRows] = React.useState(false);
  const [selected, setSelected] = React.useState(null);

  const astroOrgClients = React.useMemo(
    () => clients.filter(c => Array.isArray(c.customDashboards) && c.customDashboards.includes("astro")),
    [clients]
  );
  const astroClientIds = React.useMemo(() => new Set(astroOrgClients.map(c => c.id)), [astroOrgClients]);
  const astroAssistantIds = React.useMemo(() => new Set(
    astroOrgClients.flatMap(c => [c.assistantId, ...(Array.isArray(c.assignedAssistantIds) ? c.assignedAssistantIds : [])]).filter(Boolean)
  ), [astroOrgClients]);

  const astroLogsAll = React.useMemo(() => {
    // Client logins get server-scoped logs, but an admin previewing "Client"
    // mode gets every call in the system — so scope whenever we know the
    // Astro client(s), and only trust the list as-is when we don't.
    if (isClient && astroClientIds.size === 0) return callLogs;
    return callLogs.filter(l =>
      (l.clientId && astroClientIds.has(l.clientId))
      || (l.assistantId && astroAssistantIds.has(l.assistantId))
    );
  }, [callLogs, isClient, astroClientIds, astroAssistantIds]);

  const astroClients = React.useMemo(() => {
    const ids = new Set(astroLogsAll.map(l => l.clientId).filter(Boolean));
    return clients.filter(c => ids.has(c.id) || astroClientIds.has(c.id));
  }, [clients, astroLogsAll, astroClientIds]);

  const campaignNames = React.useMemo(
    () => [...new Set(astroLogsAll.map(l => l.campaignName).filter(Boolean))].sort(),
    [astroLogsAll]
  );

  const { rangeStart, rangeEnd, rangeLabel } = React.useMemo(() => {
    const now = new Date();
    if (rangePreset === "custom" && customFrom) {
      return {
        rangeStart: new Date(`${customFrom}T00:00:00`).getTime(),
        rangeEnd: customTo ? new Date(`${customTo}T23:59:59`).getTime() : now.getTime(),
        rangeLabel: `${customFrom} – ${customTo || t("angkasa.range.now")}`,
      };
    }
    if (rangePreset === "all") return { rangeStart: 0, rangeEnd: now.getTime(), rangeLabel: t("angkasa.range.all") };
    if (rangePreset === "today") {
      return { rangeStart: new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime(), rangeEnd: now.getTime(), rangeLabel: t("angkasa.range.today") };
    }
    const days = { "7d": 7, "30d": 30, "90d": 90 }[rangePreset] || 30;
    return { rangeStart: now.getTime() - days * 86400000, rangeEnd: now.getTime(), rangeLabel: t("angkasa.range.lastNDays", { days }) };
  }, [rangePreset, customFrom, customTo, lang]);

  // Scope (org/campaign/date) — everything on the page is computed from this.
  const scopedLogs = React.useMemo(() => astroLogsAll.filter(l => {
    if (filterClientId && l.clientId !== filterClientId) return false;
    if (filterCampaign && l.campaignName !== filterCampaign) return false;
    const ts = new Date(l.startedAt || l.updatedAt || 0).getTime();
    return ts >= rangeStart && ts <= rangeEnd;
  }), [astroLogsAll, filterClientId, filterCampaign, rangeStart, rangeEnd]);

  const outcomeById = React.useMemo(() => {
    const map = {};
    scopedLogs.forEach(l => { map[l.id] = astroOutcomeKey(l); });
    return map;
  }, [scopedLogs]);

  const stats = React.useMemo(() => {
    const outcomeCounts = Object.fromEntries(ASTRO_OUTCOME_DEFS.map(d => [d.key, 0]));
    const endedReasons = {};
    const campaignVolume = {};
    const durationBuckets = Object.fromEntries(ASTRO_DURATION_BUCKETS.map(b => [b.key, 0]));
    const contacts = new Map();
    let connected = 0;
    let talkSeconds = 0;
    let callsWithDuration = 0;
    let spent = 0;

    scopedLogs.forEach(l => {
      const outcome = outcomeById[l.id];
      outcomeCounts[outcome] += 1;
      const reason = astroEndedReason(l) || "unknown";
      endedReasons[reason] = (endedReasons[reason] || 0) + 1;
      const campaign = l.campaignName || t("astro.noCampaign");
      campaignVolume[campaign] = (campaignVolume[campaign] || 0) + 1;
      spent += Number(l.sellingMyr) || 0;

      const isConn = astroIsConnected(l);
      if (isConn) connected += 1;
      const dur = Number(l.durationSeconds) || 0;
      if (dur > 0) {
        callsWithDuration += 1;
        talkSeconds += dur;
        const bucket = ASTRO_DURATION_BUCKETS.find(b => dur < b.max) || ASTRO_DURATION_BUCKETS[ASTRO_DURATION_BUCKETS.length - 1];
        durationBuckets[bucket.key] += 1;
      }

      const key = astroContactKey(l);
      const c = contacts.get(key) || {
        attempts: 0, ptp: false, connected: false, balance: null, balanceAt: 0, ptpDate: "", ptpAt: 0, name: "",
        phone: astroPhone(l), account: "", status: "", statusAt: 0, statusReason: "", lastLogId: "", lastAt: 0,
        transferred: false, allFailedToConnect: true,
      };
      c.attempts += 1;
      if (isConn) c.connected = true;
      const startedMs = new Date(l.startedAt || l.updatedAt || 0).getTime();
      if (isConn && startedMs > (c.lastConnAt || 0)) c.lastConnAt = startedMs;
      const transferResult = astroTransferResult(l);
      if (transferResult && startedMs >= (c.transferAt || 0)) {
        c.transferAt = startedMs;
        c.transferResult = transferResult;
      }
      if (astroIsPromise(l, outcome) && startedMs >= c.ptpAt) {
        c.ptp = true;
        c.ptpAt = startedMs;
        // Amount promised = balance when they promised (Oct RM 80), not a later
        // re-sent balance (Nov RM 300).
        c.ptpAmount = astroLogBalance(l);
        c.ptpDate = astroPromiseDate(l) || c.ptpDate;
      }
      const bal = astroLogBalance(l);
      const ts = new Date(l.updatedAt || l.startedAt || 0).getTime();
      if (bal != null && ts >= c.balanceAt) { c.balance = bal; c.balanceAt = ts; }
      c.name = c.name || l.customerName || "";
      c.account = c.account || astroAccount(l);
      if (startedMs >= c.lastAt) { c.lastAt = startedMs; c.lastLogId = l.id; }
      // Latest clear outcome decides the next action (a later "no answer"
      // doesn't erase an earlier promise or dispute).
      const status = l.aiAnalysis?.collectionStatus;
      if (ASTRO_CLEAR_OUTCOMES.has(status) && startedMs >= c.statusAt) {
        c.status = status;
        c.statusAt = startedMs;
        c.statusReason = aiReasonText(l.aiAnalysis, lang) || "";
      }
      if (outcome === "transferred") c.transferred = true;
      if (astroEndedReason(l) !== "failed-to-connect") c.allFailedToConnect = false;
      contacts.set(key, c);
    });

    // A promise followed by "cannot pay" or "already paid" on a LATER call no
    // longer counts. The AI review of the promising call itself doesn't cancel
    // it, so these tiles always agree with the PTP outcome on the row.
    contacts.forEach(c => {
      if (!c.status) return;
      if (c.ptp && c.statusAt <= c.ptpAt) c.status = "promised";
      else c.ptp = c.status === "promised";
    });
    // Still waiting unless we reached them again after the missed transfer.
    contacts.forEach(c => { c.transferMissed = ASTRO_TRANSFER_MISSED.has(c.transferResult) && (c.lastConnAt || 0) <= c.transferAt; });
    const contactList = [...contacts.values()];
    const attemptDist = { 1: 0, 2: 0, 3: 0, "4+": 0 };
    let owed = 0;
    let ptpBalance = 0;
    let ptpContacts = 0;
    let reachedContacts = 0;
    contactList.forEach(c => {
      attemptDist[c.attempts >= 4 ? "4+" : c.attempts] += 1;
      if (c.balance != null) owed += c.balance;
      if (c.connected) reachedContacts += 1;
      if (c.ptp) {
        ptpContacts += 1;
        const promised = c.ptpAmount != null ? c.ptpAmount : c.balance;
        if (promised != null) ptpBalance += promised;
      }
    });

    // PTP dates falling due from today onward — Bizsuria's follow-up list.
    const today = new Date();
    today.setHours(0, 0, 0, 0);
    const dueSoon = { overdue: 0, next7: 0, later: 0 };
    contactList.filter(c => c.ptp).forEach(c => {
      const d = astroParsePtpDate(c.ptpDate);
      if (!d) return;
      const diffDays = Math.round((d - today) / 86400000);
      c.ptpDue = diffDays < 0 ? "overdue" : diffDays <= 7 ? "next7" : "later";
      dueSoon[c.ptpDue] += 1;
      c.ptpDayKey = diffDays < 0 ? "overdue" : diffDays < 14 ? astroDayKey(d) : "later";
    });

    // Expected collections: promised RM per due day (overdue / next 14 days / later).
    const calendarMap = new Map();
    contactList.forEach(c => {
      c.band = astroBandKey(c.balance);
      c.action = astroNextAction(c, today.getTime());
      if (!c.ptp || !c.ptpDayKey) return;
      const slot = calendarMap.get(c.ptpDayKey) || { key: c.ptpDayKey, count: 0, amount: 0 };
      slot.count += 1;
      slot.amount += Number(c.ptpAmount != null ? c.ptpAmount : c.balance) || 0;
      calendarMap.set(c.ptpDayKey, slot);
    });
    const calendarDays = [];
    if (calendarMap.has("overdue")) calendarDays.push(calendarMap.get("overdue"));
    for (let i = 0; i < 14; i += 1) {
      const d = new Date(today.getFullYear(), today.getMonth(), today.getDate() + i);
      const slot = calendarMap.get(astroDayKey(d));
      if (slot) calendarDays.push({ ...slot, date: d });
    }
    if (calendarMap.has("later")) calendarDays.push(calendarMap.get("later"));
    const weekAhead = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 7).getTime();
    const dueNext7Amount = calendarDays.filter(s => s.date && s.date.getTime() < weekAhead).reduce((sum, s) => sum + s.amount, 0);

    return {
      total: scopedLogs.length,
      outcomeCounts,
      endedReasons: Object.entries(endedReasons).sort((a, b) => b[1] - a[1]),
      campaignVolume: Object.entries(campaignVolume).sort((a, b) => b[1] - a[1]),
      durationBuckets,
      connected,
      talkSeconds,
      callsWithDuration,
      spent,
      contactCount: contactList.length,
      reachedContacts,
      retriedContacts: contactList.filter(c => c.attempts > 1).length,
      attemptDist,
      owed,
      ptpBalance,
      ptpContacts,
      dueSoon,
      contacts,
      contactList,
      calendarDays,
      dueNext7Amount,
    };
  }, [scopedLogs, outcomeById, lang]);

  // Manager transfers: every call that tried one, whatever its outcome.
  const transferStats = React.useMemo(() => {
    const results = Object.fromEntries(ASTRO_TRANSFER_RESULTS.map(d => [d.key, 0]));
    const agents = new Map();
    const hours = new Map();
    let total = 0;
    scopedLogs.forEach(l => {
      const result = astroTransferResult(l);
      if (!result) return;
      total += 1;
      results[result] += 1;
      const answered = result === "answered" ? 1 : 0;
      const agent = astroTransferAgent(l);
      const a = agents.get(agent) || { key: agent, total: 0, answered: 0 };
      a.total += 1;
      a.answered += answered;
      agents.set(agent, a);
      const slot = astroMySlot(l);
      if (slot) {
        const h = hours.get(slot.hour) || { key: slot.hour, total: 0, answered: 0 };
        h.total += 1;
        h.answered += answered;
        hours.set(slot.hour, h);
      }
    });
    const waiting = stats.contactList.filter(c => c.action === "managerCallback");
    return {
      total,
      results,
      tracked: total - results.untracked,
      missed: [...ASTRO_TRANSFER_MISSED].reduce((sum, key) => sum + results[key], 0),
      agents: [...agents.values()].sort((a, b) => b.total - a.total),
      hours: [...hours.values()].sort((a, b) => a.key - b.key),
      waitingCount: waiting.length,
      waitingAmount: waiting.reduce((sum, c) => sum + (Number(c.balance) || 0), 0),
    };
  }, [scopedLogs, stats]);

  // Per-call insight facts used by the funnel, heatmap and compliance cards.
  const funnelStageById = React.useMemo(() => {
    const map = {};
    scopedLogs.forEach(l => { map[l.id] = astroFunnelStage(l, outcomeById[l.id]); });
    return map;
  }, [scopedLogs, outcomeById]);

  const [heatMetric, setHeatMetric] = React.useState("answer");
  const [actionFilter, setActionFilter] = React.useState("");
  const [showAllActions, setShowAllActions] = React.useState(false);

  const insights = React.useMemo(() => {
    // Funnel: calls reaching each step.
    const reached = ASTRO_FUNNEL_STEPS.map(() => 0);
    scopedLogs.forEach(l => {
      const stage = funnelStageById[l.id] || 0;
      for (let i = 0; i <= stage; i += 1) reached[i] += 1;
    });
    let biggestDrop = null;
    for (let i = 1; i < reached.length; i += 1) {
      if (!reached[i - 1]) continue;
      const lost = 1 - reached[i] / reached[i - 1];
      if (!biggestDrop || lost > biggestDrop.lost) biggestDrop = { step: i, lost };
    }

    // Best time to call (Malaysia time).
    const heat = {};
    scopedLogs.forEach(l => {
      const s = astroMySlot(l);
      if (!s) return;
      const key = `${s.dow}-${s.hour}`;
      const cell = heat[key] || { dialled: 0, connected: 0, promised: 0 };
      cell.dialled += 1;
      if (astroIsConnected(l)) cell.connected += 1;
      if (astroIsPromise(l, outcomeById[l.id])) cell.promised += 1;
      heat[key] = cell;
    });
    const heatRate = cell => !cell ? null
      : heatMetric === "answer" ? (cell.dialled ? cell.connected / cell.dialled : null)
        : (cell.connected ? cell.promised / cell.connected : null);
    const bestSlots = Object.entries(heat)
      .filter(([, cell]) => (heatMetric === "answer" ? cell.dialled : cell.connected) >= 3)
      .map(([key, cell]) => ({ key, cell, rate: heatRate(cell) }))
      .sort((a, b) => b.rate - a.rate)
      .slice(0, 3);

    // Outcome by balance band (per debtor).
    const bands = Object.fromEntries(ASTRO_BALANCE_BANDS.map(b => [b.key, { contacts: 0, reached: 0, ptp: 0, owed: 0, promised: 0 }]));
    stats.contactList.forEach(c => {
      if (!c.band) return;
      const b = bands[c.band];
      b.contacts += 1;
      if (c.connected) b.reached += 1;
      b.owed += Number(c.balance) || 0;
      if (c.ptp) { b.ptp += 1; b.promised += Number(c.ptpAmount != null ? c.ptpAmount : c.balance) || 0; }
    });

    // Compliance + customer signals + language (AI review fields, Oct 2026+).
    let reviewed = 0;
    let clean = 0;
    let identityChecked = 0;
    let identityFail = 0;
    let pushy = 0;
    let outsideHours = 0;
    const signals = Object.fromEntries(ASTRO_SIGNALS.map(s => [s, 0]));
    const languages = Object.fromEntries(ASTRO_LANGUAGES.map(s => [s, 0]));
    scopedLogs.forEach(l => {
      const late = astroOutsideHours(l);
      if (late) outsideHours += 1;
      const a = l.aiAnalysis;
      if (!a || !("agentPushy" in a)) return;
      reviewed += 1;
      if (a.identityVerifiedFirst != null) identityChecked += 1;
      if (a.identityVerifiedFirst === false) identityFail += 1;
      if (a.agentPushy) pushy += 1;
      if (a.identityVerifiedFirst !== false && !a.agentPushy && !late) clean += 1;
      (a.signals || []).forEach(s => { if (signals[s] != null) signals[s] += 1; });
      if (languages[a.customerLanguage] != null) languages[a.customerLanguage] += 1;
    });

    // Next actions + bad numbers.
    const actionCounts = Object.fromEntries(ASTRO_ACTION_DEFS.map(d => [d.key, 0]));
    stats.contactList.forEach(c => { actionCounts[c.action] += 1; });
    const order = Object.fromEntries(ASTRO_ACTION_DEFS.map((d, i) => [d.key, i]));
    const actionRows = stats.contactList
      .filter(c => !actionFilter || c.action === actionFilter)
      .sort((a, b) => (order[a.action] - order[b.action]) || (b.lastAt - a.lastAt));
    const badNumbers = stats.contactList
      .filter(c => ASTRO_BAD_NUMBER_ACTIONS.has(c.action))
      .sort((a, b) => b.attempts - a.attempts);

    return {
      reached, biggestDrop, heat, heatRate, bestSlots, bands,
      reviewed, clean, identityChecked, identityFail, pushy, outsideHours, signals, languages,
      actionCounts, actionRows, badNumbers,
    };
  }, [scopedLogs, funnelStageById, outcomeById, stats, heatMetric, actionFilter]);

  // One drill filter for every chart/tile that has no dedicated dropdown:
  // { type, value, label }. Clicking the same segment again clears it.
  const [drill, setDrill] = React.useState(null);
  const sq = useSpeechQuality();
  const uc = useUnreachable(scopedLogs, { defaultClientId: isClient ? "" : (filterClientId || (astroClients.length === 1 ? astroClients[0].id : "")) });
  // Clicking any card segment opens the filtered call table in a pop-up, so
  // nobody has to scroll down to the call log to see the result.
  const [popupOpen, setPopupOpen] = React.useState(false);
  // A card click REPLACES any earlier row filters (they used to stack, e.g.
  // reason "driving" + "overdue" → an empty pop-up); the pop-up's own
  // filters can then narrow it further.
  function toggleDrill(type, value, label) {
    if (drill && drill.type === type && drill.value === value) { setDrill(null); return; }
    clearRowFilters();
    setDrill({ type, value, label });
    setPopupOpen(true);
  }
  function cardFilter(setter, current, value) {
    if (current === value) { setter(""); return; }
    clearRowFilters();
    setter(value);
    setPopupOpen(true);
  }

  const [astroTab, setAstroTab] = React.useState(() => {
    try { const v = localStorage.getItem("voxpro.astroTab"); return ASTRO_TABS.includes(v) ? v : "overview"; } catch { return "overview"; }
  });
  function selectAstroTab(id) {
    setAstroTab(id);
    try { localStorage.setItem("voxpro.astroTab", id); } catch { /* storage unavailable */ }
  }
  React.useEffect(() => {
    if (!popupOpen) return undefined;
    const onKey = e => { if (e.key === "Escape") setPopupOpen(false); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [popupOpen]);
  function matchesDrill(l) {
    if (!drill) return true;
    const contact = stats.contacts.get(astroContactKey(l)) || {};
    switch (drill.type) {
      case "ended": return (astroEndedReason(l) || "unknown") === drill.value;
      case "attempts": return astroAttemptBucket(contact.attempts || 0) === drill.value;
      case "duration": return astroDurationBucket(l) === drill.value;
      case "ptpDue": return contact.ptp && contact.ptpDue === drill.value;
      case "balance": return drill.value === "promised" ? Boolean(contact.ptp) : (!contact.ptp && Number(contact.balance) > 0);
      case "sentiment": return (l.aiAnalysis?.sentiment || "unanalyzed") === drill.value;
      case "qa": return Number(l.aiAnalysis?.qaScore) === Number(drill.value);
      case "asrMiss": return astroAsrMissTurns(l) > 0;
      case "csat": return Number.isFinite(l.csat?.score);
      case "ptpDay": return contact.ptp && contact.ptpDayKey === drill.value;
      // Funnel: calls that got as far as this step and no further.
      // Funnel: calls that reached this step (matches the number shown on the bar).
      case "funnel": return (funnelStageById[l.id] || 0) >= Number(drill.value);
      case "slot": { const s = astroMySlot(l); return Boolean(s) && `${s.dow}-${s.hour}` === drill.value; }
      case "transfer": { const r = astroTransferResult(l); return drill.value === "all" ? Boolean(r) : r === drill.value; }
      case "transferAgent": return Boolean(astroTransferResult(l)) && astroTransferAgent(l) === drill.value;
      case "transferHour": { const s = astroMySlot(l); return Boolean(astroTransferResult(l)) && Boolean(s) && s.hour === drill.value; }
      case "band": return contact.band === drill.value;
      case "action": return contact.action === drill.value;
      case "comp":
        if (drill.value === "identity") return l.aiAnalysis?.identityVerifiedFirst === false;
        if (drill.value === "pushy") return l.aiAnalysis?.agentPushy === true;
        if (drill.value === "hours") return astroOutsideHours(l);
        return true;
      case "signal": return (l.aiAnalysis?.signals || []).includes(drill.value);
      case "lang": return l.aiAnalysis?.customerLanguage === drill.value;
      case "sq": return speechQualityMatches(l, drill, sq);
      case "uc": return unreachableMatches(l, drill, uc);
      default: return true;
    }
  }

  const ai = React.useMemo(() => {
    const statusCounts = Object.fromEntries(ASTRO_COLLECTION_DEFS.map(d => [d.key, 0]));
    const reasonCounts = {};
    const reasonLabelsMs = {};
    let analyzed = 0;
    let qaSum = 0;
    let qaCount = 0;
    let negative = 0;
    const sentimentCounts = { positive: 0, neutral: 0, negative: 0, unanalyzed: 0 };
    const qaDist = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
    let asrMissTurns = 0;
    let callerTurns = 0;
    let callsWithMiss = 0;
    let latencySum = 0;
    let latencyCount = 0;
    let csatSum = 0;
    let csatCount = 0;
    scopedLogs.forEach(l => {
      const misses = astroAsrMissTurns(l);
      asrMissTurns += misses;
      callerTurns += astroCallerTurns(l);
      if (misses > 0) callsWithMiss += 1;
      (l.turns || []).forEach(t => {
        const ms = Number(t.latencyMs);
        if (String(t.user || "").trim() && String(t.assistant || "").trim() && ms > 0 && ms < 30000) { latencySum += ms; latencyCount += 1; }
      });
      if (Number.isFinite(l.csat?.score)) { csatSum += l.csat.score; csatCount += 1; }
      const a = l.aiAnalysis;
      if (!a) { sentimentCounts.unanalyzed += 1; return; }
      sentimentCounts[sentimentCounts[a.sentiment] != null ? a.sentiment : "neutral"] += 1;
      if (qaDist[a.qaScore] != null) qaDist[a.qaScore] += 1;
      analyzed += 1;
      if (Number.isFinite(a.qaScore)) { qaSum += a.qaScore; qaCount += 1; }
      if (a.sentiment === "negative") negative += 1;
      if (a.collectionStatus && statusCounts[a.collectionStatus] != null) statusCounts[a.collectionStatus] += 1;
      const reason = String(a.collectionReason || "").trim().toLowerCase();
      if (reason) {
        reasonCounts[reason] = (reasonCounts[reason] || 0) + 1;
        if (a.collectionReasonMs && !reasonLabelsMs[reason]) reasonLabelsMs[reason] = a.collectionReasonMs;
      }
    });
    return {
      analyzed,
      avgQa: qaCount ? qaSum / qaCount : null,
      negative,
      statusCounts,
      statusTotal: Object.values(statusCounts).reduce((s, n) => s + n, 0),
      topReasons: Object.entries(reasonCounts).sort((a, b) => b[1] - a[1]).slice(0, 8),
      reasonLabelsMs,
      sentimentCounts,
      qaDist,
      asrMissTurns,
      // Share of caller replies speech recognition couldn't read — the
      // closest measure we have to WER without human reference transcripts.
      asrMissRate: (callerTurns + asrMissTurns) ? asrMissTurns / (callerTurns + asrMissTurns) : null,
      callsWithMiss,
      avgResponseMs: latencyCount ? latencySum / latencyCount : null,
      csatAvg: csatCount ? csatSum / csatCount : null,
      csatCount,
    };
  }, [scopedLogs]);

  // Customers who said they already paid — Bizsuria checks these against Astro.
  const claims = React.useMemo(() => {
    const rows = scopedLogs
      .filter(l => l.aiAnalysis?.collectionStatus === "already_paid")
      .sort((a, b) => new Date(b.startedAt || 0) - new Date(a.startedAt || 0));
    const total = rows.reduce((s, l) => s + (Number(l.aiAnalysis.claimedPaidAmount) || 0), 0);
    return { rows, total };
  }, [scopedLogs]);

  const walletLeft = React.useMemo(() => {
    const relevant = filterClientId ? clients.filter(c => c.id === filterClientId) : astroClients;
    const withBalance = relevant.filter(c => c.walletBalanceMyr != null);
    return withBalance.length ? withBalance.reduce((s, c) => s + (Number(c.walletBalanceMyr) || 0), 0) : null;
  }, [clients, astroClients, filterClientId]);

  const tableLogs = React.useMemo(() => scopedLogs.filter(l => {
    if (filterOutcome && outcomeById[l.id] !== filterOutcome) return false;
    if (filterConnected === "yes" && !astroIsConnected(l)) return false;
    if (filterConnected === "no" && astroIsConnected(l)) return false;
    if (filterCollection && l.aiAnalysis?.collectionStatus !== filterCollection) return false;
    if (filterReason && String(l.aiAnalysis?.collectionReason || "").trim().toLowerCase() !== filterReason) return false;
    if (!matchesDrill(l)) return false;
    if (searchQuery.trim()) {
      const q = searchQuery.trim().toLowerCase();
      const hay = `${l.customerName || ""} ${astroPhone(l)} ${astroAccount(l)} ${l.campaignName || ""} ${l.id || ""}`.toLowerCase();
      if (!hay.includes(q)) return false;
    }
    return true;
  }).sort((a, b) => new Date(b.startedAt || 0) - new Date(a.startedAt || 0)), [scopedLogs, outcomeById, filterOutcome, filterConnected, filterCollection, filterReason, searchQuery, drill, stats, funnelStageById]);

  const visibleRows = showAllRows ? tableLogs : tableLogs.slice(0, 50);

  // Every AI reason in scope (the Reasons card only shows the top 8).
  const reasonOptions = React.useMemo(() => {
    const map = new Map();
    scopedLogs.forEach(l => {
      const reason = String(l.aiAnalysis?.collectionReason || "").trim().toLowerCase();
      if (reason && !map.has(reason)) map.set(reason, lang === "ms" ? (l.aiAnalysis.collectionReasonMs || reason) : reason);
    });
    return [...map.entries()].sort((a, b) => a[1].localeCompare(b[1]));
  }, [scopedLogs, lang]);
  const selectedLogObj = React.useMemo(() => (selected ? astroLogsAll.find(l => l.id === selected) || null : null), [selected, astroLogsAll]);

  const hasRowFilter = Boolean(filterOutcome || filterConnected || filterCollection || filterReason || drill || searchQuery.trim());
  function clearRowFilters() {
    setDrill(null);
    setFilterOutcome("");
    setFilterConnected("");
    setFilterCollection("");
    setFilterReason("");
    setSearchQuery("");
  }

  function exportClaimsCsv() {
    const claimsUploadCols = uploadColumns(claims.rows.map((l) => l.campaignRow || {}));
    const header = ["call_id", "called_at", "customer", "number", "akaun_number", "claimed_paid_date", "claimed_paid_amount", "claimed_paid_method", "balance", "campaign", "ai_summary", ...claimsUploadCols];
    const esc = v => `"${String(v ?? "").replace(/"/g, '""')}"`;
    const lines = claims.rows.map(l => [
      l.id, l.startedAt, l.customerName, csvTextValue(astroPhone(l)), csvTextValue(astroAccount(l)),
      l.aiAnalysis.claimedPaidDate, l.aiAnalysis.claimedPaidAmount, l.aiAnalysis.claimedPaidMethod,
      astroLogBalance(l), l.campaignName, aiSummaryText(l.aiAnalysis, lang), ...uploadCells(l.campaignRow || {}, claimsUploadCols),
    ].map(esc).join(","));
    const blob = new Blob(["\ufeff" + [header.join(","), ...lines].join("\n")], { type: "text/csv;charset=utf-8" });
    const a = document.createElement("a");
    a.href = URL.createObjectURL(blob);
    a.download = `astro-paid-claims-${new Date().toISOString().slice(0, 10)}.csv`;
    a.click();
    const href = a.href;
    setTimeout(() => URL.revokeObjectURL(href), 1000);
  }

  function exportCsv() {
    const callsUploadCols = uploadColumns(tableLogs.map((l) => l.campaignRow || {}));
    const header = ["call_id", "started_at", "customer", "number", "campaign", "outcome", "ended_reason", "transfer_result", "connected", "balance", "expected_payment_date", "duration_s", "cost_myr", "ai_payment_status", "ai_reason", "ai_qa_score", "ai_sentiment", "asr_misses", "ai_summary", "claimed_paid", "claimed_paid_date", "claimed_paid_amount", "claimed_paid_method", "akaun_number", ...callsUploadCols];
    const esc = v => `"${String(v ?? "").replace(/"/g, '""')}"`;
    const lines = tableLogs.map(l => [
      l.id, l.startedAt, l.customerName, csvTextValue(astroPhone(l)), l.campaignName, outcomeById[l.id], l.endedReason, astroTransferResult(l),
      astroIsConnected(l) ? "yes" : "no", astroLogBalance(l), astroExpectedPaymentDate(l), l.durationSeconds, l.sellingMyr,
      l.aiAnalysis?.collectionStatus || "", aiReasonText(l.aiAnalysis, lang), l.aiAnalysis?.qaScore ?? "", l.aiAnalysis?.sentiment || "", astroAsrMissTurns(l), aiSummaryText(l.aiAnalysis, lang),
      l.aiAnalysis?.collectionStatus === "already_paid" ? "yes" : "", l.aiAnalysis?.claimedPaidDate || "", l.aiAnalysis?.claimedPaidAmount ?? "", l.aiAnalysis?.claimedPaidMethod || "", csvTextValue(astroAccount(l)), ...uploadCells(l.campaignRow || {}, callsUploadCols),
    ].map(esc).join(","));
    const blob = new Blob(["\ufeff" + [header.join(","), ...lines].join("\n")], { type: "text/csv;charset=utf-8" });
    const a = document.createElement("a");
    a.href = URL.createObjectURL(blob);
    a.download = `astro-calls-${new Date().toISOString().slice(0, 10)}.csv`;
    a.click();
    const href = a.href;
    setTimeout(() => URL.revokeObjectURL(href), 1000);
  }

  function actionDetail(c) {
    if (c.action === "managerCallback") return t(`astro.transferResult.${c.transferResult}`);
    if (c.action === "promiseDue" || c.action === "awaitPromise") return c.ptpDate ? t("astro.action.dueOn", { date: c.ptpDate }) : "";
    if (c.action === "unreachable" || c.action === "retry" || c.action === "invalidNumber") return t("astro.action.attempts", { n: c.attempts });
    return c.statusReason || "";
  }

  function calendarLabel(slot) {
    if (slot.key === "overdue") return t("astro.calendar.overdue");
    if (slot.key === "later") return t("astro.calendar.later");
    return slot.date.toLocaleDateString(lang === "ms" ? "ms-MY" : "en-GB", { weekday: "short", day: "numeric", month: "short" });
  }

  const today10 = new Date().toISOString().slice(0, 10);
  // A contact's original upload row = the campaign row on its latest call.
  const astroUploadRow = (c) => (callLogs.find((l) => l.id === c.lastLogId)?.campaignRow) || {};
  function exportActionsCsv() {
    const actionUploadCols = uploadColumns(insights.actionRows.map(astroUploadRow));
    astroDownloadCsv(`astro-next-actions-${today10}.csv`,
      ["next_action", "detail", "customer", "number", "akaun_number", "balance", "last_status", "promise_date", "attempts", "last_call", ...actionUploadCols],
      insights.actionRows.map(c => [
        t(`astro.action.${c.action}`), actionDetail(c), c.name, csvTextValue(c.phone), csvTextValue(c.account), c.balance ?? "",
        c.status ? t(`astro.collection.${c.status}`) : "", c.ptpDate, c.attempts, c.lastAt ? new Date(c.lastAt).toISOString() : "", ...uploadCells(astroUploadRow(c), actionUploadCols),
      ]));
  }
  function exportBadNumbersCsv() {
    const badUploadCols = uploadColumns(insights.badNumbers.map(astroUploadRow));
    astroDownloadCsv(`astro-bad-numbers-${today10}.csv`,
      ["number", "customer", "akaun_number", "reason", "attempts", "balance", "last_call", ...badUploadCols],
      insights.badNumbers.map(c => [
        csvTextValue(c.phone), c.name, csvTextValue(c.account), t(`astro.action.${c.action}`), c.attempts, c.balance ?? "",
        c.lastAt ? new Date(c.lastAt).toISOString() : "", ...uploadCells(astroUploadRow(c), badUploadCols),
      ]));
  }

  const total = stats.total;
  const maxEnded = stats.endedReasons.reduce((m, [, c]) => Math.max(m, c), 0);
  const maxCampaign = stats.campaignVolume.reduce((m, [, c]) => Math.max(m, c), 0);
  const maxDuration = Math.max(0, ...Object.values(stats.durationBuckets));
  const maxAttempt = Math.max(0, ...Object.values(stats.attemptDist));
  const uncollected = Math.max(0, stats.owed - stats.ptpBalance);

  return (
    <div style={{ display: "flex", alignItems: "flex-start", gap: 0, minWidth: 0 }}>
    <div className="angkasa-compact" style={{ flex: 1, minWidth: 0, overflow: "hidden" }}>
      <div className="ph">
        <div>
          <h1 className="ph-title">{t("nav.astro")}</h1>
          <div className="ph-subtitle">{t("app.summary.astro")}</div>
        </div>
      </div>

      <div className="row gap-1 mb-4" style={{ justifyContent: "flex-end", alignItems: "center" }}>
        {testCallsToggle && (
          <label className="row gap-2 text-12" style={{ marginRight: "auto", cursor: "pointer" }} title={t("astro.testCalls.hint")}>
            <input type="checkbox" checked={testCallsToggle.value} onChange={e => testCallsToggle.onChange(e.target.checked)} />
            {t("astro.testCalls.label")}
          </label>
        )}
        {ASTRO_RANGE_PRESETS.map(id => (
          <Btn key={id} variant={rangePreset === id ? "primary" : "ghost"} size="sm" onClick={() => setRangePreset(id)}>{t(`angkasa.range.${id}`)}</Btn>
        ))}
      </div>

      <div className="row gap-3 mb-4" style={{ flexWrap: "wrap", alignItems: "flex-end" }}>
        {!isClient && astroClients.length > 0 && (
          <Field label={t("angkasa.field.organization")}>
            <Select value={filterClientId} onChange={e => setFilterClientId(e.target.value)}>
              <option value="">{t("astro.field.allClients")}</option>
              {astroClients.map(c => <option key={c.id} value={c.id}>{c.organizationName || c.company || c.name}</option>)}
            </Select>
          </Field>
        )}
        <Field label={t("astro.field.campaign")}>
          <Select value={filterCampaign} onChange={e => setFilterCampaign(e.target.value)}>
            <option value="">{t("astro.field.allCampaigns", { count: campaignNames.length })}</option>
            {campaignNames.map(n => <option key={n} value={n}>{n}</option>)}
          </Select>
        </Field>
        <Field label={t("angkasa.field.customFrom")}>
          <Input type="date" value={customFrom} onChange={e => { setCustomFrom(e.target.value); setRangePreset("custom"); }} />
        </Field>
        <Field label={t("angkasa.field.customTo")}>
          <Input type="date" value={customTo} onChange={e => { setCustomTo(e.target.value); setRangePreset("custom"); }} />
        </Field>
      </div>

      {astroLogsAll.length === 0 && (
        <div className="card" style={{ padding: 28, textAlign: "center", color: "var(--c-text-4)" }}>
          {t("astro.empty.noCalls")}
        </div>
      )}

      {astroLogsAll.length > 0 && (
        <>
          <div className="g-4 mb-4">
            <div className="stat" style={{ cursor: "pointer" }} onClick={() => toggleDrill("balance", "promised", t("astro.collections.promised"))}>
              <div className="stat-label">{t("astro.stat.ptpBalance")}<AstroInfo text={t("astro.info.stat.ptpBalance")} /></div>
              <div className="stat-value" style={{ color: "var(--ok)" }}>{astroFmtMoney(stats.ptpBalance)}</div>
              <div className="muted text-11 mt-2">{t("astro.stat.ptpBalanceMeta", { count: stats.ptpContacts })}</div>
            </div>
            <div className="stat" style={{ cursor: "pointer" }} onClick={() => toggleDrill("balance", "uncollected", t("astro.collections.uncollected"))}>
              <div className="stat-label">{t("astro.stat.totalOwed")}<AstroInfo text={t("astro.info.stat.totalOwed")} /></div>
              <div className="stat-value" style={{ color: "var(--err)" }}>{astroFmtMoney(stats.owed)}</div>
              <div className="muted text-11 mt-2">{t("astro.stat.totalOwedMeta", { amount: astroFmtMoney(uncollected) })}</div>
            </div>
            <div className="stat">
              <div className="stat-label">{t("astro.stat.totalSpent")}<AstroInfo text={t("astro.info.stat.totalSpent")} /></div>
              <div className="stat-value">{astroFmtMoney(stats.spent)}</div>
              <div className="muted text-11 mt-2">
                {walletLeft != null ? t("astro.stat.creditLeft", { amount: astroFmtMoney(walletLeft) }) : rangeLabel}
              </div>
            </div>
            <div className="stat" style={{ cursor: "pointer" }} onClick={() => { clearRowFilters(); setPopupOpen(true); }}>
              <div className="stat-label">{t("astro.stat.calls")}<AstroInfo text={t("astro.info.stat.calls")} /></div>
              <div className="stat-value">{total.toLocaleString()}</div>
              <div className="muted text-11 mt-2">{t("astro.stat.callsMeta", { contacts: stats.contactCount.toLocaleString(), campaigns: stats.campaignVolume.length })}</div>
            </div>
          </div>

          <div className="g-4 mb-4">
            <div className="stat" style={{ cursor: "pointer" }} onClick={() => cardFilter(setFilterConnected, filterConnected, "yes")}>
              <div className="stat-label">{t("astro.stat.connectionRate")}<AstroInfo text={t("astro.info.stat.connectionRate")} /></div>
              <div className="stat-value">{astroPct(stats.connected, total)}%</div>
              <div className="muted text-11 mt-2">{t("astro.stat.connectionRateMeta", { connected: stats.connected.toLocaleString(), total: total.toLocaleString() })}</div>
            </div>
            <div className="stat" style={{ cursor: "pointer" }} onClick={() => toggleDrill("balance", "promised", t("astro.collections.promised"))}>
              <div className="stat-label">{t("astro.stat.ptpRate")}<AstroInfo text={t("astro.info.stat.ptpRate")} /></div>
              <div className="stat-value" style={{ color: "var(--ok)" }}>{astroPct(stats.ptpContacts, stats.reachedContacts)}%</div>
              <div className="muted text-11 mt-2">{t("astro.stat.ptpRateMeta", { ptp: stats.ptpContacts, reached: stats.reachedContacts })}</div>
            </div>
            <div className="stat" style={{ cursor: "pointer" }} onClick={() => toggleDrill("transfer", "all", t("astro.stat.transferred"))}>
              <div className="stat-label">{t("astro.stat.transferred")}<AstroInfo text={t("astro.info.stat.transferred")} /></div>
              <div className="stat-value" style={{ color: "var(--a)" }}>{transferStats.total.toLocaleString()}</div>
              <div className="muted text-11 mt-2">{t("astro.stat.transferredMeta", { answered: transferStats.results.answered, missed: transferStats.missed })}</div>
            </div>
            <div className="stat">
              <div className="stat-label">{t("astro.stat.avgTalk")}<AstroInfo text={t("astro.info.stat.avgTalk")} /></div>
              <div className="stat-value">{stats.callsWithDuration ? fmtDuration(Math.round(stats.talkSeconds / stats.callsWithDuration)) : "—"}</div>
              <div className="muted text-11 mt-2">{t("astro.stat.avgTalkMeta", { total: astroFmtTalkTime(stats.talkSeconds) })}</div>
            </div>
          </div>

          <div className="row gap-1 mb-4" style={{ flexWrap: "wrap", borderBottom: "1px solid var(--c-line)", paddingBottom: 8 }}>
            {ASTRO_TABS.map(id => (
              <Btn key={id} size="sm" variant={astroTab === id ? "primary" : "ghost"} onClick={() => selectAstroTab(id)}>{t(`astro.tab.${id}`)}</Btn>
            ))}
          </div>

          {astroTab === "overview" && (
            <>
              <div className="g-2 mb-4">
                <Card title={<>{t("astro.card.outcomes.title")}<AstroInfo text={t("astro.info.outcomes")} /></>} subtitle={t("astro.card.clickToFilter")}>
                  <div className="row" style={{ justifyContent: "center", padding: "2px 0" }}>
                    <Donut size={112} label={total.toLocaleString()} sub={t("astro.calls")}
                      segments={total > 0
                        ? ASTRO_OUTCOME_DEFS.filter(d => stats.outcomeCounts[d.key] > 0).map(d => ({ value: stats.outcomeCounts[d.key], color: d.color }))
                        : [{ value: 1, color: "var(--c-line-2)" }]} />
                  </div>
                  <div className="col gap-2 mt-3">
                    {ASTRO_OUTCOME_DEFS.filter(d => stats.outcomeCounts[d.key] > 0).map(d => (
                      <AstroLegendRow
                        key={d.key}
                        color={d.color}
                        label={t(`astro.outcome.${d.key}`)}
                        count={stats.outcomeCounts[d.key]}
                        total={total}
                        active={filterOutcome === d.key}
                        onClick={() => cardFilter(setFilterOutcome, filterOutcome, d.key)}
                      />
                    ))}
                    {total === 0 && <div className="muted text-11">{t("angkasa.table.noCallsWindow")}</div>}
                  </div>
                </Card>
                <Card title={<>{t("astro.card.funnel.title")}<AstroInfo text={t("astro.info.funnel")} /></>} subtitle={t("astro.card.funnel.subtitleReached")}>
                  <div className="col gap-3">
                    {ASTRO_FUNNEL_STEPS.map((step, i) => {
                      const n = insights.reached[i];
                      const prev = i > 0 ? insights.reached[i - 1] : n;
                      const drop = i > 0 && prev ? Math.round((1 - n / prev) * 100) : 0;
                      return (
                        <AngkasaBarRow key={step}
                          label={`${t(`astro.funnel.${step}`)}${i > 0 && drop > 0 ? `  ↓${drop}%` : ""}`}
                          value={`${n.toLocaleString()} · ${astroPct(n, insights.reached[0])}%`}
                          count={n} max={insights.reached[0]} color={i === ASTRO_FUNNEL_STEPS.length - 1 ? "var(--ok)" : "var(--a)"}
                          active={drill?.type === "funnel" && Number(drill.value) === i}
                          onClick={() => toggleDrill("funnel", String(i), t("astro.funnel.reached", { step: t(`astro.funnel.${step}`) }))} />
                      );
                    })}
                  </div>
                  {insights.biggestDrop && insights.biggestDrop.lost > 0 && (
                    <div className="muted text-11 mt-3">
                      {t("astro.funnel.biggestDrop", {
                        from: t(`astro.funnel.${ASTRO_FUNNEL_STEPS[insights.biggestDrop.step - 1]}`),
                        to: t(`astro.funnel.${ASTRO_FUNNEL_STEPS[insights.biggestDrop.step]}`),
                        pct: Math.round(insights.biggestDrop.lost * 100),
                      })}
                    </div>
                  )}
                </Card>
              </div>

              {transferStats.total > 0 && (
                <Card className="mb-4" title={<>{t("astro.card.transfers.title")}<AstroInfo text={t("astro.info.transfers")} /></>}
                  subtitle={t("astro.card.transfers.subtitle", { answered: transferStats.results.answered, tracked: transferStats.tracked, pct: astroPct(transferStats.results.answered, transferStats.tracked) })}>
                  <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))", gap: 24 }}>
                    <div className="col gap-3">
                      <div className="stat-label">{t("astro.transfers.result")}</div>
                      {ASTRO_TRANSFER_RESULTS.filter(d => transferStats.results[d.key] > 0).map(d => (
                        <AngkasaBarRow key={d.key} label={t(`astro.transferResult.${d.key}`)} value={transferStats.results[d.key].toLocaleString()}
                          count={transferStats.results[d.key]} max={transferStats.total} color={d.color}
                          active={drill?.type === "transfer" && drill.value === d.key}
                          onClick={() => toggleDrill("transfer", d.key, t(`astro.transferResult.${d.key}`))} />
                      ))}
                    </div>
                    <div className="col gap-3">
                      <div className="stat-label">{t("astro.transfers.byManager")}</div>
                      {transferStats.agents.map(a => (
                        <AngkasaBarRow key={a.key} label={a.key} value={t("astro.transfers.answeredOf", { answered: a.answered, total: a.total })}
                          count={a.answered} max={a.total} color="var(--ok)"
                          active={drill?.type === "transferAgent" && drill.value === a.key}
                          onClick={() => toggleDrill("transferAgent", a.key, `${t("astro.transfers.byManager")}: ${a.key}`)} />
                      ))}
                    </div>
                    <div className="col gap-3">
                      <div className="stat-label">{t("astro.transfers.byHour")}</div>
                      {transferStats.hours.map(h => (
                        <AngkasaBarRow key={h.key} label={`${String(h.key).padStart(2, "0")}:00`} value={t("astro.transfers.answeredOf", { answered: h.answered, total: h.total })}
                          count={h.answered} max={h.total} color="var(--ok)"
                          active={drill?.type === "transferHour" && drill.value === h.key}
                          onClick={() => toggleDrill("transferHour", h.key, `${t("astro.transfers.byHour")}: ${String(h.key).padStart(2, "0")}:00`)} />
                      ))}
                    </div>
                  </div>
                  {transferStats.waitingCount > 0 && (
                    <div className="mt-4 text-12" style={{ cursor: "pointer", color: "var(--err)" }}
                      onClick={() => toggleDrill("action", "managerCallback", t("astro.action.managerCallback"))}>
                      {t("astro.transfers.waiting", { count: transferStats.waitingCount, amount: astroFmtMoney(transferStats.waitingAmount) })}
                    </div>
                  )}
                </Card>
              )}

              <div className="g-2 mb-4">
                <Card title={<>{t("astro.card.connection.title")}<AstroInfo text={t("astro.info.connection")} /></>} subtitle={t("astro.card.clickToFilter")}>
                  <div className="row" style={{ justifyContent: "center", padding: "2px 0" }}>
                    <Donut size={112} label={`${astroPct(stats.connected, total)}%`} sub={t("astro.connected")}
                      segments={total > 0
                        ? [{ value: stats.connected, color: "var(--ok)" }, { value: total - stats.connected, color: "var(--c-line-2)" }]
                        : [{ value: 1, color: "var(--c-line-2)" }]} />
                  </div>
                  <div className="col gap-2 mt-3">
                    <AstroLegendRow color="var(--ok)" label={t("astro.connected")} count={stats.connected} total={total}
                      active={filterConnected === "yes"} onClick={() => cardFilter(setFilterConnected, filterConnected, "yes")} />
                    <AstroLegendRow color="var(--c-line-2)" label={t("astro.notConnected")} count={total - stats.connected} total={total}
                      active={filterConnected === "no"} onClick={() => cardFilter(setFilterConnected, filterConnected, "no")} />
                  </div>
                </Card>
                <Card title={<>{t("astro.card.collections.title")}<AstroInfo text={t("astro.info.collections")} /></>} subtitle={t("astro.card.collections.subtitle")}>
                  <div className="col gap-3">
                    <AngkasaBarRow label={t("astro.collections.promised")} value={astroFmtMoney(stats.ptpBalance)} count={stats.ptpBalance} max={stats.owed} color="var(--ok)"
                      active={drill?.type === "balance" && drill.value === "promised"} onClick={() => toggleDrill("balance", "promised", t("astro.collections.promised"))} />
                    <AngkasaBarRow label={t("astro.collections.uncollected")} value={astroFmtMoney(uncollected)} count={uncollected} max={stats.owed} color="var(--err)"
                      active={drill?.type === "balance" && drill.value === "uncollected"} onClick={() => toggleDrill("balance", "uncollected", t("astro.collections.uncollected"))} />
                  </div>
                  <div className="stat-label mt-4" style={{ marginBottom: 6 }}>{t("astro.collections.ptpDue")}</div>
                  <div className="col gap-2" style={{ fontSize: 12 }}>
                    {[["overdue", "var(--err)"], ["next7", "var(--warn)"], ["later", "var(--ok)"]].map(([key, color]) => (
                      <div key={key} className="row" onClick={() => toggleDrill("ptpDue", key, `${t("astro.collections.ptpDue")}: ${t(`astro.collections.due.${key}`)}`)}
                        style={{ justifyContent: "space-between", cursor: "pointer", padding: "2px 6px", margin: "-2px -6px", borderRadius: 6,
                          background: drill?.type === "ptpDue" && drill.value === key ? "rgba(141,139,255,0.09)" : "transparent" }}>
                        <span className="row gap-2 muted"><span style={{ width: 8, height: 8, borderRadius: 2, background: color }} />{t(`astro.collections.due.${key}`)}</span>
                        <span className="mono">{stats.dueSoon[key]}</span>
                      </div>
                    ))}
                  </div>
                </Card>
              </div>

              <Card
                title={<>{t("astro.card.logs.title")}<AstroInfo text={t("astro.info.logs")} /></>}
                subtitle={`${t("astro.card.logs.subtitle", { shown: visibleRows.length.toLocaleString(), total: tableLogs.length.toLocaleString() })}${drill ? ` · ${t("astro.filteredBy", { label: drill.label })}` : ""}`}
                action={(
                  <div className="row gap-2">
                    {hasRowFilter && <Btn variant="ghost" size="sm" onClick={clearRowFilters}>{t("angkasa.clearFilters")}</Btn>}
                    <Btn variant="ghost" size="sm" onClick={exportCsv} disabled={!tableLogs.length}>{t("astro.export", { count: tableLogs.length.toLocaleString() })}</Btn>
                  </div>
                )}
              >
                {renderCallTable(false)}
              </Card>
            </>
          )}

          {astroTab === "actions" && (
            <>
              <Card
                title={<>{t("astro.card.actions.title")}<AstroInfo text={t("astro.info.actions")} /></>}
                subtitle={t("astro.card.actions.subtitle", { count: stats.contactList.length.toLocaleString() })}
                className="mb-4"
                action={<Btn variant="ghost" size="sm" onClick={exportActionsCsv} disabled={!insights.actionRows.length}>{t("astro.actions.export", { count: insights.actionRows.length })}</Btn>}
              >
                <div className="row gap-1 mb-3" style={{ flexWrap: "wrap" }}>
                  <Btn size="sm" variant={!actionFilter ? "primary" : "ghost"} onClick={() => setActionFilter("")}>{t("astro.actions.all")}</Btn>
                  {ASTRO_ACTION_DEFS.filter(d => insights.actionCounts[d.key] > 0).map(d => (
                    <Btn key={d.key} size="sm" variant={actionFilter === d.key ? "primary" : "ghost"}
                      onClick={() => setActionFilter(prev => prev === d.key ? "" : d.key)}>
                      <span style={{ display: "inline-block", width: 8, height: 8, borderRadius: 2, background: d.color, marginRight: 6 }} />
                      {t(`astro.action.${d.key}`)} · {insights.actionCounts[d.key]}
                    </Btn>
                  ))}
                </div>
                <div className="tbl-wrap" style={{ margin: "0 -18px -18px" }}>
                  <table className="tbl">
                    <thead>
                      <tr>
                        <th>{t("astro.actions.next")}</th>
                        <th>{t("astro.table.customer")}</th>
                        <th>{t("astro.claims.account")}</th>
                        <th className="right">{t("astro.table.balance")}</th>
                        <th>{t("astro.actions.lastStatus")}</th>
                        <th className="right">{t("astro.actions.attemptsCol")}</th>
                        <th>{t("astro.actions.lastCall")}</th>
                      </tr>
                    </thead>
                    <tbody>
                      {insights.actionRows.length === 0 && (
                        <tr><td colSpan={7} style={{ textAlign: "center", color: "var(--c-text-4)", padding: 20 }}>{t("angkasa.table.noCallsWindow")}</td></tr>
                      )}
                      {(showAllActions ? insights.actionRows : insights.actionRows.slice(0, 25)).map(c => (
                        <tr key={`act-${c.phone}-${c.lastLogId}`} onClick={() => setSelected(prev => prev === c.lastLogId ? null : c.lastLogId)}
                          style={{ cursor: "pointer", background: c.lastLogId === selected ? "rgba(141,139,255,0.07)" : "" }}>
                          <td>
                            <span className="row gap-2 text-12 strong">
                              <span style={{ width: 8, height: 8, borderRadius: 2, background: ASTRO_ACTION_COLOR[c.action] }} />
                              {t(`astro.action.${c.action}`)}
                            </span>
                            {actionDetail(c) && <div className="text-11 muted mt-1">{actionDetail(c)}</div>}
                          </td>
                          <td>
                            <div className="strong">{c.name || t("angkasa.table.unknown")}</div>
                            {c.phone && <div className="mono text-11 muted">{c.phone}</div>}
                          </td>
                          <td className="mono text-11">{c.account || "—"}</td>
                          <td className="mono right">{astroFmtMoney(c.balance)}</td>
                          <td className="text-11">{c.status ? t(`astro.collection.${c.status}`) : (c.connected ? t("astro.connected") : t("astro.notConnected"))}</td>
                          <td className="mono right">{c.attempts}</td>
                          <td className="mono" style={{ fontSize: 11 }}>{c.lastAt ? fmtDate(new Date(c.lastAt).toISOString()) : "—"}</td>
                        </tr>
                      ))}
                    </tbody>
                  </table>
                </div>
                {!showAllActions && insights.actionRows.length > 25 && (
                  <div className="row mt-3" style={{ justifyContent: "center" }}>
                    <Btn variant="ghost" size="sm" onClick={() => setShowAllActions(true)}>{t("astro.showAll", { count: insights.actionRows.length.toLocaleString() })}</Btn>
                  </div>
                )}
              </Card>

              <Card
                title={<>{t("astro.card.claims.title")}<AstroInfo text={t("astro.info.claims")} /></>}
                subtitle={t("astro.card.claims.subtitle", { count: claims.rows.length, amount: astroFmtMoney(claims.total) })}
                className="mb-4"
                action={claims.rows.length > 0 && <Btn variant="ghost" size="sm" onClick={exportClaimsCsv}>{t("astro.claims.export")}</Btn>}
              >
                {claims.rows.length === 0 ? (
                  <div className="muted text-11">{t("astro.claims.empty")}</div>
                ) : (
                  <div className="tbl-wrap" style={{ margin: "0 -18px -18px" }}>
                    <table className="tbl">
                      <thead>
                        <tr>
                          <th>{t("angkasa.table.time")}</th>
                          <th>{t("astro.table.customer")}</th>
                          <th>{t("astro.claims.account")}</th>
                          <th>{t("astro.claims.date")}</th>
                          <th className="right">{t("astro.claims.amount")}</th>
                          <th>{t("astro.claims.method")}</th>
                          <th className="right">{t("astro.table.balance")}</th>
                        </tr>
                      </thead>
                      <tbody>
                        {claims.rows.map(l => (
                          <tr key={`claim-${l.id}`} onClick={() => setSelected(prev => prev === l.id ? null : l.id)}
                            style={{ cursor: "pointer", background: l.id === selected ? "rgba(141,139,255,0.07)" : "" }}>
                            <td className="mono" style={{ fontSize: 11 }}>{fmtDate(l.startedAt)}</td>
                            <td>
                              <div className="strong">{l.customerName || t("angkasa.table.unknown")}</div>
                              {astroPhone(l) && <div className="mono text-11 muted">{astroPhone(l)}</div>}
                            </td>
                            <td className="mono text-11">{astroAccount(l) || "—"}</td>
                            <td className="mono text-11">{l.aiAnalysis.claimedPaidDate || "—"}</td>
                            <td className="mono right">{l.aiAnalysis.claimedPaidAmount != null ? astroFmtMoney(Number(l.aiAnalysis.claimedPaidAmount)) : "—"}</td>
                            <td className="text-11">{l.aiAnalysis.claimedPaidMethod || "—"}</td>
                            <td className="mono right">{astroFmtMoney(astroLogBalance(l))}</td>
                          </tr>
                        ))}
                      </tbody>
                    </table>
                  </div>
                )}
              </Card>

              <Card
                title={<>{t("astro.card.badNumbers.title")}<AstroInfo text={t("astro.info.badNumbers")} /></>}
                subtitle={t("astro.card.badNumbers.subtitle", { count: insights.badNumbers.length })}
                className="mb-4"
                action={insights.badNumbers.length > 0 && <Btn variant="ghost" size="sm" onClick={exportBadNumbersCsv}>{t("astro.badNumbers.export")}</Btn>}
              >
                {insights.badNumbers.length === 0 ? (
                  <div className="muted text-11">{t("astro.badNumbers.empty")}</div>
                ) : (
                  <div className="tbl-wrap" style={{ margin: "0 -18px -18px" }}>
                    <table className="tbl">
                      <thead>
                        <tr>
                          <th>{t("astro.badNumbers.number")}</th>
                          <th>{t("astro.table.customer")}</th>
                          <th>{t("astro.claims.account")}</th>
                          <th>{t("astro.badNumbers.reason")}</th>
                          <th className="right">{t("astro.actions.attemptsCol")}</th>
                          <th>{t("astro.actions.lastCall")}</th>
                        </tr>
                      </thead>
                      <tbody>
                        {insights.badNumbers.slice(0, 50).map(c => (
                          <tr key={`bad-${c.phone}-${c.lastLogId}`} onClick={() => setSelected(prev => prev === c.lastLogId ? null : c.lastLogId)}
                            style={{ cursor: "pointer", background: c.lastLogId === selected ? "rgba(141,139,255,0.07)" : "" }}>
                            <td className="mono">{c.phone || "—"}</td>
                            <td>{c.name || t("angkasa.table.unknown")}</td>
                            <td className="mono text-11">{c.account || "—"}</td>
                            <td className="text-12">{t(`astro.action.${c.action}`)}</td>
                            <td className="mono right">{c.attempts}</td>
                            <td className="mono" style={{ fontSize: 11 }}>{c.lastAt ? fmtDate(new Date(c.lastAt).toISOString()) : "—"}</td>
                          </tr>
                        ))}
                      </tbody>
                    </table>
                  </div>
                )}
              </Card>
            </>
          )}

          {astroTab === "ai" && (
            <>
              <div className="g-3 mb-4">
                <Card
                  title={<>{t("astro.card.aiStatus.title")}<AstroInfo text={t("astro.info.aiStatus")} /></>}
                  subtitle={t("astro.card.aiStatus.subtitle", { analyzed: ai.analyzed.toLocaleString(), total: total.toLocaleString() })}
                >
                  <div className="col gap-3">
                    {ASTRO_COLLECTION_DEFS.filter(d => ai.statusCounts[d.key] > 0).map(d => (
                      <AngkasaBarRow
                        key={d.key}
                        label={t(`astro.collection.${d.key}`)}
                        value={`${ai.statusCounts[d.key]} · ${astroPct(ai.statusCounts[d.key], ai.statusTotal)}%`}
                        count={ai.statusCounts[d.key]}
                        max={Math.max(...Object.values(ai.statusCounts))}
                        color={d.color}
                        active={filterCollection === d.key}
                        onClick={() => cardFilter(setFilterCollection, filterCollection, d.key)}
                      />
                    ))}
                    {ai.statusTotal === 0 && <div className="muted text-11">{t("astro.ai.empty")}</div>}
                  </div>
                </Card>
                <Card title={<>{t("astro.card.aiReasons.title")}<AstroInfo text={t("astro.info.aiReasons")} /></>} subtitle={t("astro.card.aiReasons.subtitle")}>
                  <div className="col gap-3">
                    {ai.topReasons.map(([reason, count]) => (
                      <AngkasaBarRow
                        key={reason}
                        label={lang === "ms" ? (ai.reasonLabelsMs[reason] || reason) : reason}
                        value={count.toLocaleString()}
                        count={count}
                        max={ai.topReasons[0][1]}
                        color="var(--warn)"
                        active={filterReason === reason}
                        onClick={() => cardFilter(setFilterReason, filterReason, reason)}
                      />
                    ))}
                    {ai.topReasons.length === 0 && <div className="muted text-11">{ai.analyzed ? t("astro.ai.noReasons") : t("astro.ai.empty")}</div>}
                  </div>
                </Card>
                <Card title={<>{t("astro.card.signals.title")}<AstroInfo text={t("astro.info.signals")} /></>} subtitle={t("astro.card.signals.subtitle")}>
                  <div className="col gap-3">
                    {ASTRO_SIGNALS.filter(s => insights.signals[s] > 0).map(s => (
                      <AngkasaBarRow key={s} label={t(`astro.signal.${s}`)} value={insights.signals[s].toLocaleString()} count={insights.signals[s]}
                        max={Math.max(...Object.values(insights.signals))} color={s === "legal_threat" || s === "deceased" ? "var(--err)" : "var(--warn)"}
                        active={drill?.type === "signal" && drill.value === s} onClick={() => toggleDrill("signal", s, t(`astro.signal.${s}`))} />
                    ))}
                    {ASTRO_SIGNALS.every(s => !insights.signals[s]) && <div className="muted text-11">{insights.reviewed ? t("astro.signals.none") : t("astro.ai.empty")}</div>}
                  </div>
                </Card>
              </div>

              <div className="g-3 mb-4">
                <Card title={<>{t("astro.card.sentiment.title")}<AstroInfo text={t("astro.info.sentiment")} /></>} subtitle={t("astro.card.clickToFilter")}>
                  <div className="row" style={{ justifyContent: "center", padding: "2px 0" }}>
                    <Donut size={112} label={(ai.sentimentCounts.positive + ai.sentimentCounts.neutral + ai.sentimentCounts.negative).toLocaleString()} sub={t("astro.ai.analyzed")}
                      segments={ai.analyzed > 0
                        ? [{ value: ai.sentimentCounts.positive, color: "var(--ok)" }, { value: ai.sentimentCounts.neutral, color: "var(--info)" }, { value: ai.sentimentCounts.negative, color: "var(--err)" }]
                        : [{ value: 1, color: "var(--c-line-2)" }]} />
                  </div>
                  <div className="col gap-2 mt-3">
                    {[["positive", "var(--ok)"], ["neutral", "var(--info)"], ["negative", "var(--err)"], ["unanalyzed", "var(--c-line-2)"]].map(([key, color]) => (
                      <AstroLegendRow key={key} color={color} label={t(`astro.sentiment.${key}`)} count={ai.sentimentCounts[key]} total={total}
                        active={drill?.type === "sentiment" && drill.value === key}
                        onClick={() => toggleDrill("sentiment", key, t(`astro.sentiment.${key}`))} />
                    ))}
                  </div>
                </Card>
                <Card title={<>{t("astro.card.language.title")}<AstroInfo text={t("astro.info.language")} /></>} subtitle={t("astro.card.clickToFilter")}>
                  <div className="col gap-3">
                    {ASTRO_LANGUAGES.filter(s => insights.languages[s] > 0).map(s => (
                      <AngkasaBarRow key={s} label={t(`astro.lang.${s}`)} value={`${insights.languages[s]} · ${astroPct(insights.languages[s], insights.reviewed)}%`} count={insights.languages[s]}
                        max={Math.max(...Object.values(insights.languages))} color="var(--info)"
                        active={drill?.type === "lang" && drill.value === s} onClick={() => toggleDrill("lang", s, t(`astro.lang.${s}`))} />
                    ))}
                    {ASTRO_LANGUAGES.every(s => !insights.languages[s]) && <div className="muted text-11">{t("astro.ai.empty")}</div>}
                  </div>
                </Card>
                <Card title={<>{t("astro.card.aiQuality.title")}<AstroInfo text={t("astro.info.aiQuality")} /></>} subtitle={t("astro.card.aiQuality.subtitle")}>
                  <div className="g-2">
                    <div className="stat">
                      <div className="stat-label">{t("angkasa.qa.avgScore")}</div>
                      <div className="stat-value" style={{ color: qaScoreColor(ai.avgQa) }}>{ai.avgQa != null ? `${ai.avgQa.toFixed(1)} / 5` : "—"}</div>
                    </div>
                    <div className="stat" style={{ cursor: ai.statusCounts.already_paid ? "pointer" : undefined }}
                      onClick={() => ai.statusCounts.already_paid && cardFilter(setFilterCollection, filterCollection, "already_paid")}>
                      <div className="stat-label">{t("astro.collection.already_paid")}</div>
                      <div className="stat-value" style={{ color: "var(--info)" }}>{ai.statusCounts.already_paid}</div>
                    </div>
                    <div className="stat">
                      <div className="stat-label">{t("astro.ai.negative")}</div>
                      <div className="stat-value" style={{ color: ai.negative ? "var(--err)" : undefined }}>{ai.negative}</div>
                    </div>
                    <div className="stat">
                      <div className="stat-label">{t("astro.ai.analyzed")}</div>
                      <div className="stat-value">{ai.analyzed.toLocaleString()}</div>
                    </div>
                  </div>
                  <div className="muted text-11 mt-3">{t("astro.ai.footnote")}</div>
                </Card>
              </div>

            </>
          )}

          {astroTab === "quality" && (
            <>
              <div className="mb-4">
                <SpeechQualityCard logs={scopedLogs} sq={sq} t={t} lang={lang} Info={AstroInfo} drill={drill} onDrill={toggleDrill} />
              </div>
              <div className="g-3 mb-4">
                <Card title={<>{t("astro.card.compliance.title")}<AstroInfo text={t("astro.info.compliance")} /></>} subtitle={t("astro.card.compliance.subtitle", { reviewed: insights.reviewed.toLocaleString() })}>
                  <div className="stat" style={{ marginBottom: 10 }}>
                    <div className="stat-label">{t("astro.compliance.score")}</div>
                    <div className="stat-value" style={{ color: insights.reviewed ? (insights.clean / insights.reviewed >= 0.95 ? "var(--ok)" : "var(--warn)") : undefined }}>
                      {insights.reviewed ? `${astroPct(insights.clean, insights.reviewed)}%` : "—"}
                    </div>
                  </div>
                  <div className="col gap-2">
                    <AstroLegendRow color="var(--err)" label={t("astro.compliance.identity")} count={insights.identityFail} total={insights.identityChecked}
                      active={drill?.type === "comp" && drill.value === "identity"} onClick={() => toggleDrill("comp", "identity", t("astro.compliance.identity"))} />
                    <AstroLegendRow color="var(--warn)" label={t("astro.compliance.pushy")} count={insights.pushy} total={insights.reviewed}
                      active={drill?.type === "comp" && drill.value === "pushy"} onClick={() => toggleDrill("comp", "pushy", t("astro.compliance.pushy"))} />
                    <AstroLegendRow color="var(--a)" label={t("astro.compliance.hours", { from: ASTRO_CALL_WINDOW.from, to: ASTRO_CALL_WINDOW.to })} count={insights.outsideHours} total={total}
                      active={drill?.type === "comp" && drill.value === "hours"} onClick={() => toggleDrill("comp", "hours", t("astro.compliance.hours", { from: ASTRO_CALL_WINDOW.from, to: ASTRO_CALL_WINDOW.to }))} />
                  </div>
                  <div className="muted text-11 mt-3">{t("astro.compliance.footnote")}</div>
                </Card>
                <Card title={<>{t("astro.card.qaDist.title")}<AstroInfo text={t("astro.info.qaDist")} /></>} subtitle={t("astro.card.qaDist.subtitle")}>
                  <div className="col gap-3">
                    {[5, 4, 3, 2, 1].map(score => (
                      <AngkasaBarRow key={score} label={`${score} / 5`} value={ai.qaDist[score].toLocaleString()} count={ai.qaDist[score]}
                        max={Math.max(1, ...Object.values(ai.qaDist))} color={score >= 4 ? "var(--ok)" : score === 3 ? "var(--warn)" : "var(--err)"}
                        active={drill?.type === "qa" && Number(drill.value) === score}
                        onClick={() => toggleDrill("qa", String(score), `QA ${score}/5`)} />
                    ))}
                  </div>
                </Card>
                <Card title={<>{t("astro.card.speech.title")}<AstroInfo text={t("astro.info.speech")} /></>} subtitle={t("astro.card.speech.subtitle")}>
                  <div className="g-2">
                    <div className="stat" style={{ cursor: ai.callsWithMiss ? "pointer" : undefined }}
                      onClick={() => ai.callsWithMiss && toggleDrill("asrMiss", "yes", t("astro.speech.callsWithMiss"))}>
                      <div className="stat-label">{t("astro.speech.missRate")}</div>
                      <div className="stat-value" style={{ color: ai.asrMissRate > 0.2 ? "var(--err)" : ai.asrMissRate > 0.1 ? "var(--warn)" : undefined }}>
                        {ai.asrMissRate != null ? `${Math.round(ai.asrMissRate * 100)}%` : "—"}
                      </div>
                      <div className="muted text-11 mt-2">{t("astro.speech.missMeta", { misses: ai.asrMissTurns, calls: ai.callsWithMiss })}</div>
                    </div>
                    <div className="stat">
                      <div className="stat-label">{t("astro.speech.responseTime")}</div>
                      <div className="stat-value">{ai.avgResponseMs != null ? `${(ai.avgResponseMs / 1000).toFixed(1)}s` : "—"}</div>
                      <div className="muted text-11 mt-2">{t("astro.speech.responseMeta")}</div>
                    </div>
                    <div className="stat" style={{ cursor: ai.csatCount ? "pointer" : undefined }}
                      onClick={() => ai.csatCount && toggleDrill("csat", "yes", "CSAT")}>
                      <div className="stat-label">CSAT</div>
                      <div className="stat-value">{ai.csatAvg != null ? `${ai.csatAvg.toFixed(1)} / 5` : "—"}</div>
                      <div className="muted text-11 mt-2">{ai.csatCount ? t("astro.speech.csatMeta", { count: ai.csatCount }) : t("astro.speech.csatNone")}</div>
                    </div>
                  </div>
                  <div className="muted text-11 mt-3">{t("astro.speech.footnote")}</div>
                </Card>
              </div>

            </>
          )}

          {astroTab === "unreachable" && (
            <div className="mb-4">
              <UnreachableTab model={uc} t={t} lang={lang} Info={AstroInfo} drill={drill} onDrill={toggleDrill} rangeLabel={rangeLabel} />
            </div>
          )}

          {astroTab === "reach" && (
            <>
              <div className="g-2 mb-4">
                <Card title={<>{t("astro.card.heat.title")}<AstroInfo text={t("astro.info.heat")} /></>} subtitle={t("astro.card.heat.subtitle")}
                  action={(
                    <div className="row gap-1">
                      {["answer", "promise"].map(m => (
                        <Btn key={m} size="sm" variant={heatMetric === m ? "primary" : "ghost"} onClick={() => setHeatMetric(m)}>{t(`astro.heat.${m}`)}</Btn>
                      ))}
                    </div>
                  )}>
                  <div style={{ overflowX: "auto" }}>
                    <table style={{ borderCollapse: "separate", borderSpacing: 2, fontSize: 10, width: "100%" }}>
                      <thead>
                        <tr>
                          <th />
                          {ASTRO_HEAT_HOURS.map(h => <th key={h} className="muted mono" style={{ fontWeight: 400 }}>{h}</th>)}
                        </tr>
                      </thead>
                      <tbody>
                        {ASTRO_HEAT_DAYS.map(dow => (
                          <tr key={dow}>
                            <td className="muted" style={{ paddingRight: 4, whiteSpace: "nowrap" }}>{t(`astro.day.${dow}`)}</td>
                            {ASTRO_HEAT_HOURS.map(h => {
                              const key = `${dow}-${h}`;
                              const cell = insights.heat[key];
                              const rate = insights.heatRate(cell);
                              const isActive = drill?.type === "slot" && drill.value === key;
                              return (
                                <td key={key}
                                  title={cell ? t("astro.heat.tooltip", { day: t(`astro.day.${dow}`), hour: h, calls: cell.dialled, answered: cell.connected, promised: cell.promised }) : ""}
                                  onClick={() => cell && toggleDrill("slot", key, `${t(`astro.day.${dow}`)} ${h}:00`)}
                                  style={{
                                    height: 22, minWidth: 22, textAlign: "center", borderRadius: 4, cursor: cell ? "pointer" : undefined,
                                    background: rate == null ? "var(--c-surface-2)" : `color-mix(in srgb, var(--ok) ${Math.round(15 + rate * 85)}%, var(--c-surface-2))`,
                                    outline: isActive ? "2px solid var(--a)" : undefined,
                                    color: rate != null && rate > 0.55 ? "#fff" : undefined,
                                  }}>
                                  {rate != null ? Math.round(rate * 100) : ""}
                                </td>
                              );
                            })}
                          </tr>
                        ))}
                      </tbody>
                    </table>
                  </div>
                  <div className="muted text-11 mt-3">
                    {insights.bestSlots.length > 0
                      ? t("astro.heat.best", { slots: insights.bestSlots.map(s => { const [dow, h] = s.key.split("-"); return `${t(`astro.day.${dow}`)} ${h}:00 (${Math.round(s.rate * 100)}%)`; }).join(", ") })
                      : t("astro.heat.needMore")}
                  </div>
                </Card>
                <Card title={<>{t("astro.card.bands.title")}<AstroInfo text={t("astro.info.bands")} /></>} subtitle={t("astro.card.clickToFilter")}>
                  <div className="tbl-wrap" style={{ margin: "0 -18px -18px" }}>
                    <table className="tbl">
                      <thead>
                        <tr>
                          <th>{t("astro.bands.band")}</th>
                          <th className="right">{t("astro.bands.debtors")}</th>
                          <th className="right">{t("astro.bands.reached")}</th>
                          <th className="right">{t("astro.bands.promiseRate")}</th>
                          <th className="right">{t("astro.bands.owed")}</th>
                          <th className="right">{t("astro.bands.promised")}</th>
                        </tr>
                      </thead>
                      <tbody>
                        {ASTRO_BALANCE_BANDS.map(b => {
                          const row = insights.bands[b.key];
                          return (
                            <tr key={b.key} onClick={() => toggleDrill("band", b.key, t(`astro.band.${b.key}`))}
                              style={{ cursor: "pointer", background: drill?.type === "band" && drill.value === b.key ? "rgba(141,139,255,0.07)" : "" }}>
                              <td className="strong">{t(`astro.band.${b.key}`)}</td>
                              <td className="mono right">{row.contacts}</td>
                              <td className="mono right">{astroPct(row.reached, row.contacts)}%</td>
                              <td className="mono right" style={{ color: row.reached && astroPct(row.ptp, row.reached) >= 50 ? "var(--ok)" : undefined }}>{row.reached ? `${astroPct(row.ptp, row.reached)}%` : "—"}</td>
                              <td className="mono right">{astroFmtMoney(row.owed)}</td>
                              <td className="mono right">{astroFmtMoney(row.promised)}</td>
                            </tr>
                          );
                        })}
                      </tbody>
                    </table>
                  </div>
                </Card>
              </div>

              <div className="g-2 mb-4">
                <Card title={<>{t("astro.card.endedReasons.title")}<AstroInfo text={t("astro.info.endedReasons")} /></>} subtitle={rangeLabel}>
                  <div className="col gap-3">
                    {stats.endedReasons.slice(0, 10).map(([reason, count]) => (
                      <AngkasaBarRow key={reason} label={reason.replace(/[-_.]/g, " ")} value={count.toLocaleString()} count={count} max={maxEnded} color="var(--info)"
                        active={drill?.type === "ended" && drill.value === reason} onClick={() => toggleDrill("ended", reason, reason.replace(/[-_.]/g, " "))} />
                    ))}
                    {stats.endedReasons.length === 0 && <div className="muted text-11">{t("angkasa.table.noCallsWindow")}</div>}
                  </div>
                </Card>
                <Card title={<>{t("astro.card.campaignVolume.title")}<AstroInfo text={t("astro.info.campaignVolume")} /></>} subtitle={t("astro.card.campaignVolume.subtitle")}>
                  <div className="col gap-3">
                    {stats.campaignVolume.slice(0, 10).map(([name, count]) => (
                      <AngkasaBarRow
                        key={name}
                        label={name}
                        value={count.toLocaleString()}
                        count={count}
                        max={maxCampaign}
                        active={filterCampaign === name}
                        onClick={() => { if (filterCampaign === name) { setFilterCampaign(""); return; } clearRowFilters(); setFilterCampaign(name); setPopupOpen(true); }}
                      />
                    ))}
                    {stats.campaignVolume.length === 0 && <div className="muted text-11">{t("angkasa.table.noCallsWindow")}</div>}
                  </div>
                </Card>
              </div>

              <div className="g-2 mb-4">
                <Card title={<>{t("astro.card.attempts.title")}<AstroInfo text={t("astro.info.attempts")} /></>} subtitle={t("astro.card.attempts.subtitle", { retried: stats.retriedContacts, pct: astroPct(stats.retriedContacts, stats.contactCount) })}>
                  <div className="col gap-3">
                    {Object.entries(stats.attemptDist).map(([bucket, count]) => (
                      <AngkasaBarRow key={bucket} label={t("astro.attempts.n", { n: bucket })} value={count.toLocaleString()} count={count} max={maxAttempt} color="var(--a)"
                        active={drill?.type === "attempts" && drill.value === String(bucket)} onClick={() => toggleDrill("attempts", String(bucket), t("astro.attempts.n", { n: bucket }))} />
                    ))}
                  </div>
                </Card>
                <Card title={<>{t("astro.card.duration.title")}<AstroInfo text={t("astro.info.duration")} /></>} subtitle={t("astro.card.duration.subtitle", { count: stats.callsWithDuration.toLocaleString() })}>
                  <div className="col gap-3">
                    {ASTRO_DURATION_BUCKETS.map(b => (
                      <AngkasaBarRow key={b.key} label={t(`astro.duration.${b.key}`)} value={stats.durationBuckets[b.key].toLocaleString()} count={stats.durationBuckets[b.key]} max={maxDuration} color="var(--warn)"
                        active={drill?.type === "duration" && drill.value === b.key} onClick={() => toggleDrill("duration", b.key, t(`astro.duration.${b.key}`))} />
                    ))}
                  </div>
                </Card>
              </div>

            </>
          )}

        </>
      )}
    </div>
    {!popupOpen && <SessionDetail log={selectedLogObj} mode={mode} onClose={() => setSelected(null)} lang={lang} allLogs={astroLogsAll} onSelectLog={setSelected} />}
    {popupOpen && (
      <div onClick={e => { if (e.target === e.currentTarget) setPopupOpen(false); }}
        style={{ position: "fixed", inset: 0, zIndex: 1000, background: "rgba(0,0,0,0.45)", display: "flex", alignItems: "center", justifyContent: "center", padding: 24 }}>
        <div className="card" role="dialog" aria-modal="true"
          style={{ width: "min(1400px, 100%)", maxHeight: "calc(100vh - 48px)", display: "flex", flexDirection: "column", overflow: "hidden", padding: 0 }}>
          <div className="row" style={{ justifyContent: "space-between", alignItems: "flex-start", gap: 12, padding: "16px 18px", borderBottom: "1px solid var(--c-line)" }}>
            <div>
              <div className="strong" style={{ fontSize: 15 }}>{t("astro.card.logs.title")}</div>
              <div className="muted text-11 mt-1">{t("astro.card.logs.subtitle", { shown: visibleRows.length.toLocaleString(), total: tableLogs.length.toLocaleString() })}</div>
            </div>
            <div className="row gap-2">
              {hasRowFilter && <Btn variant="ghost" size="sm" onClick={clearRowFilters}>{t("angkasa.clearFilters")}</Btn>}
              <Btn variant="ghost" size="sm" onClick={exportCsv} disabled={!tableLogs.length}>{t("astro.export", { count: tableLogs.length.toLocaleString() })}</Btn>
              <Btn variant="primary" size="sm" onClick={() => setPopupOpen(false)}>{t("astro.popup.close")}</Btn>
            </div>
          </div>
          <div style={{ display: "flex", minHeight: 0, flex: 1 }}>
            <div style={{ flex: 1, minWidth: 0, overflow: "auto", padding: "14px 18px 18px" }}>
              {renderCallTable(true)}
            </div>
            <SessionDetail log={selectedLogObj} mode={mode} onClose={() => setSelected(null)} lang={lang} allLogs={astroLogsAll} onSelectLog={setSelected} />
          </div>
        </div>
      </div>
    )}
    </div>
  );

  // Filters + call table — shared by the page's call log card and the pop-up.
  function renderCallTable(inPopup) {
    return (
      <>
            <div className="row gap-2 mb-3" style={{ flexWrap: "wrap", alignItems: "center" }}>
              <Input placeholder={t("astro.search.placeholder")} value={searchQuery} onChange={e => setSearchQuery(e.target.value)} style={{ width: 240 }} />
              <Select value={filterOutcome} onChange={e => setFilterOutcome(e.target.value)} style={{ width: 180 }}>
                <option value="">{t("angkasa.filter.allOutcomes")}</option>
                {ASTRO_OUTCOME_DEFS.map(d => <option key={d.key} value={d.key}>{t(`astro.outcome.${d.key}`)}</option>)}
              </Select>
              <Select value={filterConnected} onChange={e => setFilterConnected(e.target.value)} style={{ width: 160 }}>
                <option value="">{t("astro.filter.anyConnection")}</option>
                <option value="yes">{t("astro.connected")}</option>
                <option value="no">{t("astro.notConnected")}</option>
              </Select>
              <Select value={filterCollection} onChange={e => setFilterCollection(e.target.value)} style={{ width: 180 }}>
                <option value="">{t("astro.filter.anyPaymentStatus")}</option>
                {ASTRO_COLLECTION_DEFS.map(d => <option key={d.key} value={d.key}>{t(`astro.collection.${d.key}`)}</option>)}
              </Select>
              <Select value={filterReason} onChange={e => setFilterReason(e.target.value)} style={{ width: 200 }}>
                <option value="">{t("astro.filter.anyReason")}</option>
                {reasonOptions.map(([reason, label]) => <option key={reason} value={reason}>{label}</option>)}
              </Select>
              {drill && (
                <span className="row gap-1 text-12" style={{ padding: "4px 8px", borderRadius: 6, background: "rgba(141,139,255,0.12)" }}>
                  {t("astro.filteredBy", { label: drill.label })}
                  <span role="button" title={t("angkasa.clearFilters")} onClick={() => setDrill(null)} style={{ cursor: "pointer", marginLeft: 4, fontWeight: 600 }}>×</span>
                </span>
              )}
            </div>
            <div className="tbl-wrap" style={{ margin: inPopup ? 0 : "0 -18px -18px" }}>
              <table className="tbl">
                <thead>
                  <tr>
                    <th>{t("angkasa.table.time")}</th>
                    <th>{t("astro.table.customer")}</th>
                    <th>{t("astro.claims.account")}</th>
                    <th>{t("astro.field.campaign")}</th>
                    <th>{t("angkasa.table.outcome")}</th>
                    <th>{t("astro.table.endedReason")}</th>
                    <th className="right">{t("astro.table.balance")}</th>
                    <th>{t("astro.table.expectedPayment")}</th>
                    <th className="right">{t("angkasa.table.duration")}</th>
                    <th className="right">{t("angkasa.table.cost")}</th>
                  </tr>
                </thead>
                <tbody>
                  {visibleRows.length === 0 && (
                    <tr><td colSpan={10} style={{ textAlign: "center", color: "var(--c-text-4)", padding: 20 }}>{t("angkasa.table.noCallsWindow")}</td></tr>
                  )}
                  {visibleRows.map(l => {
                    const outcome = outcomeById[l.id];
                    const isSelected = l.id === selected;
                    return (
                      <tr key={l.id} onClick={() => setSelected(prev => prev === l.id ? null : l.id)}
                        style={{ cursor: "pointer", background: isSelected ? "rgba(141,139,255,0.07)" : "" }}>
                        <td className="mono" style={{ fontSize: 11 }}>{fmtDate(l.startedAt)}</td>
                        <td>
                          <div className="strong">{l.customerName || t("angkasa.table.unknown")}</div>
                          {astroPhone(l) && <div className="mono text-11 muted">{astroPhone(l)}</div>}
                        </td>
                        <td className="mono text-11">{astroAccount(l) || "—"}</td>
                        <td className="text-11">{l.campaignName || "—"}</td>
                        <td>
                          <span className="row gap-2 text-12">
                            <span style={{ width: 8, height: 8, borderRadius: 2, background: ASTRO_OUTCOME_COLOR[outcome] }} />
                            {t(`astro.outcome.${outcome}`)}
                          </span>
                          {l.aiAnalysis?.collectionStatus && (
                            <div className="text-11 muted mt-1">
                              {t(`astro.collection.${l.aiAnalysis.collectionStatus}`)}{aiReasonText(l.aiAnalysis, lang) ? ` · ${aiReasonText(l.aiAnalysis, lang)}` : ""}
                            </div>
                          )}
                          {aiSummaryText(l.aiAnalysis, lang) && (
                            <div className="text-11 muted mt-1" style={{ maxWidth: 320, whiteSpace: "normal" }} title={aiSummaryText(l.aiAnalysis, lang)}>
                              {aiSummaryText(l.aiAnalysis, lang)}
                            </div>
                          )}
                        </td>
                        <td><Badge tone={outcomeTone(l.endedReason)} dot>{l.endedReason || "—"}</Badge></td>
                        <td className="mono right">{astroFmtMoney(astroLogBalance(l))}</td>
                        <td className="mono text-11">{astroIsPromise(l, outcome) ? (astroPromiseDate(l) || "—") : "—"}</td>
                        <td className="mono right">{fmtDuration(l.durationSeconds)}</td>
                        <td className="mono right">{fmtRM(l.sellingMyr)}</td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>
            {!showAllRows && tableLogs.length > visibleRows.length && (
              <div className="row mt-3" style={{ justifyContent: "center" }}>
                <Btn variant="ghost" size="sm" onClick={() => setShowAllRows(true)}>{t("astro.showAll", { count: tableLogs.length.toLocaleString() })}</Btn>
              </div>
            )}
      </>
    );
  }
};
