// DashboardV6 — ホーム：TODOを束ねて一覧（ファネル別／種類別）＋「今すぐ」band＋行内アクション
// クリック最小化：一覧の行から直接 電話／完了 ができ、詳細を開かずに処理できる。
// 3・4段目：案件段階別件数・進行中トラック件数（タップで絞込一覧へ）。
// Codex(自分優先・0件明示・役割名に電話しない) + Cursor(今すぐ理由・自分チップ・件数サマリ) を統合。

const DV_FUNNELS = [
  { key:"intake", label:"受付",  tone:"var(--status-intake)",   nav:"cases",  note:"初動" },
  { key:"survey", label:"調査",  tone:"var(--status-survey)",   nav:"survey", note:"原因特定まで" },
  { key:"work",   label:"工事",  tone:"var(--status-progress)", nav:"work",   note:"号室ごとの復旧" },
  { key:"ins",    label:"保険",  tone:"var(--warning)",         nav:"ins",    note:"申請〜認定〜入金" },
];
const DV_TYPES = ["報告","社内承認","社外承認","連絡","方針確認","見積","入金精算","その他"];
const DV_STAGES = [
  { key:"受付", tone:"var(--status-intake)" },
  { key:"調査", tone:"var(--status-survey)" },
  { key:"工事", tone:"var(--status-progress)" },
  { key:"完了", tone:"var(--success)" },
];
const DV_TRACK_KINDS = [
  { key:"survey", label:"調査", nav:"survey", tone:"var(--status-survey)" },
  { key:"work",   label:"工事", nav:"work",   tone:"var(--status-progress)" },
  { key:"ins",    label:"保険", nav:"ins",    tone:"var(--warning)" },
];
const dvTypeOf = r => r.btype!=="承認" ? r.btype : (r.step.indexOf("社内")>=0 ? "社内承認" : "社外承認");
/* ファネル＝進行があればその種類、なければ 種類・工程名・ボールから導出（受付＝初動のみ） */
const dvFunnelOf = r => {
  const trk = r.trkId ? (window.TR_ROWS||[]).find(x=>x.id===r.trkId) : null;
  const k = trk ? trk.kind : null;
  if(k && k!=="settle") return k;
  /* 精算（請求・入金・支払）は独立ファネルを持たず、元の進行の種類に寄せる */
  if(/保険|認定|鑑定|加入者/.test(r.step) || /保険会社|加入者/.test(r.ball) || /保険|海上|火災/.test(r.dunning||"")) return "ins";
  if(/調査費|調査/.test(r.step)) return "survey";
  if(r.btype==="入金精算" || /請求|入金|支払|精算|復旧|施工|工事|防水|補修|交換|張替|着工/.test(r.step)) return "work";
  if(/応急|緊急|止水|受付|初動/.test(r.step)) return "intake";
  if(/報告書|現地|見積/.test(r.step)) return "survey";
  return "intake";
};
const dvDue = s => window.bvDueState ? window.bvDueState(s) : "future";
const dvDueColor = s => { const st = dvDue(s); return st==="over"?"var(--destructive)" : st==="today"?"var(--warning)" : st==="soon"?"var(--status-survey)" : "var(--muted-foreground)"; };
const dvDueLabel = s => window.bvDueLabel ? window.bvDueLabel(s) : s;
/* 「今すぐ」帯向け: なぜ今すぐかを略さず出す（何日超過／本日締め） */
const dvWhyNow = s => {
  const st = dvDue(s);
  if(st==="over"){
    const m = /(\d+)日/.exec(dvDueLabel(s));
    return m ? ("期限超過 "+m[1]+"日") : "期限超過";
  }
  if(st==="today") return "本日締め";
  return dvDueLabel(s);
};
/* 列幅を食わない短縮表記（超過＝−n日／本日／あとn日／日付） */
const dvShortDue = s => { const l = dvDueLabel(s); const m = /(\d+)日/.exec(l);
  if(/超過/.test(l)) return "−"+(m?m[1]:"")+"日";
  if(/本日|今日/.test(l)) return "本日";
  if(/あと/.test(l)) return m?"+"+m[1]+"日":l;
  return l; };
const dvSort = (a,b) => (window.bvDueNum ? window.bvDueNum(a.due)-window.bvDueNum(b.due) : 0);
const dvPersonName = value => String(value || "").split("（")[0].trim();
// 決-21(追-4-3):「自分の案件」の絞り込みに副担当(subAssignee)・営業担当(salesAssignee)
// も含める(store.jsx boardRows()が3つとも持つ)。本担当(assignee)だけの一致で
// 済ませていたのを、いずれか一致すれば「自分」に広げる。
const dvIsMine = (r, meName) => {
  const me = dvPersonName(meName);
  if(!me) return false;
  return [r.assignee, r.subAssignee, r.salesAssignee].some(n => n && dvPersonName(n)===me);
};
const dvSortRows = (rows, meName) => [...rows].sort((a,b) => {
  if(dvPersonName(meName)) {
    const mine = Number(dvIsMine(b, meName)) - Number(dvIsMine(a, meName));
    if(mine) return mine;
  }
  return dvSort(a,b);
});
const dvChip = { display:"inline-flex", alignItems:"center", height:15, padding:"0 6px", borderRadius:"var(--radius-sm)", fontSize:"var(--text-caption)", fontWeight:600, whiteSpace:"nowrap", flexShrink:0, lineHeight:1 };
const DV_FLAGS = {
  urgent:["緊急","var(--urgency-emergency)","var(--destructive-muted)","var(--destructive-border)"],
  delay: ["滞留","var(--warning)","var(--warning-muted)","var(--warning-border)"],
  warn:  ["予兆","var(--status-survey)","var(--status-survey-muted)","var(--status-survey-border)"],
  watch: ["要注意","var(--warning)","var(--warning-muted)","var(--warning-border)"],
};
const dvFlagOf = r => r.urgent?"urgent" : r.marker==="delay"?"delay" : r.marker==="warn"?"warn" : r.watch?"watch" : null;
/* ball は「誰が持つか」の役割であって連絡先名ではない。役割だけの実データから電話記録を作らない。 */
const DV_CONTACT_ROLES = new Set(["自社", "他者", "依頼者", "業者", "保険会社", "加入者"]);
const dvContactName = value => {
  const name = String(value || "").trim();
  return name && !DV_CONTACT_ROLES.has(name) ? name : null;
};
const dvWho = r => dvContactName(r.dunning) || dvContactName(r.ball);
function DvFlag({ r }) {
  const k = dvFlagOf(r);
  if(!k) return null;
  const [l,c,bg,bd] = DV_FLAGS[k];
  return <span style={{ ...dvChip, color:c, background:bg, border:"1px solid "+bd }}>{l}</span>;
}
/* K18-13(0105): 漏水が続いている案件(漏水状況が 常時/時々)の印「継続中」。規則は store.jsx の
   isLeakContinuing の 1 か所(boardRows の leakContinuing)。盤面(BoardV4 BvLeakChip)と同じ語・同じ赤。 */
function DvLeak({ r }) {
  if(!r || !r.leakContinuing) return null;
  return <span data-leak-continuing title="漏水状況が 常時/時々(まだ漏れています)" style={{ ...dvChip, color:"var(--destructive)", background:"var(--destructive-muted)", border:"1px solid var(--destructive-border)" }}>継続中</span>;
}

/* 盤面・ダッシュボード-12(0049・決-20): 「遅れている」一覧。
   決-20は文字どおり「赤/黄」を求めているので、上のDV_FLAGS(滞留/予兆とも
   var(--warning)系で色分けは文言だけ)とは別に、超過=赤(destructive)・
   予兆=黄(warning)の2色をここだけで使う。理由(予定日/報告リズムのどちらで
   引っかかったか)はreasonKindで出し分ける。 */
const SLA_REASON_LABELS = {
  plan:   { delay:"予定日 超過",     warn:"予定日 まもなく" },
  report: { delay:"報告リズム 超過", warn:"報告リズム そろそろ" },
};
/* ㉘: rpc_sla_queue の語彙(予定/報告・赤/黄) → 画面の reasonKind / marker。 */
function mapSlaRpcRow(r) {
  if (!r) return null;
  const reasonKind = r.reasonKind === "報告" || r.reasonKind === "report" ? "report" : "plan";
  const marker = r.level === "赤" || r.marker === "delay" || r.level === "delay" ? "delay" : "warn";
  return {
    caseId: r.caseId,
    dbId: r.caseId,
    prop: r.title || r.prop || r.caseNo || "（名称未設定）",
    caseNo: r.caseNo || null,
    marker,
    reasonKind,
    overDays: typeof r.overDays === "number" ? r.overDays : Number(r.overDays) || 0,
    ownerName: r.ownerName || null,
  };
}
const slaDayText = (overDays) => overDays >= 0 ? overDays + "日超過" : "あと" + (-overDays) + "日";
function SlaRow({ r, onOpen, last }) {
  const isDelay = r.marker === "delay";
  const color = isDelay ? "var(--destructive)" : "var(--warning)";
  const bg = isDelay ? "var(--destructive-muted)" : "var(--warning-muted)";
  const bd = isDelay ? "var(--destructive-border)" : "var(--warning-border)";
  const label = (SLA_REASON_LABELS[r.reasonKind] || SLA_REASON_LABELS.plan)[isDelay ? "delay" : "warn"];
  return (
    <div role="button" tabIndex={0} onClick={()=>onOpen&&onOpen(r)}
      onKeyDown={e=>{ if(e.key==="Enter"||e.key===" "){ e.preventDefault(); onOpen&&onOpen(r); } }}
      style={{ boxSizing:"border-box", width:"100%",
        display:"flex", alignItems:"center", gap:8, padding:"8px 12px", cursor:"pointer", fontFamily:"var(--font-sans)",
        borderBottom:last?"none":"1px solid var(--border)", transition:"background var(--duration-fast) var(--easing)" }}
      onMouseEnter={e=>e.currentTarget.style.background="var(--accent)"}
      onMouseLeave={e=>e.currentTarget.style.background="transparent"}>
      <span style={{ ...dvChip, color, background:bg, border:"1px solid "+bd, flexShrink:0 }}>{label}</span>
      <span style={{ flex:1, minWidth:0, overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap",
        fontSize:"var(--text-body-md)", fontWeight:600 }}>
        {r.caseNo ? <span className="mono" style={{ fontWeight:600, color:"var(--muted-foreground)", marginRight:6 }}>{r.caseNo}</span> : null}
        {r.prop}{r.surveyLabel ? <span style={{ fontWeight:600, color:"var(--status-survey)", marginLeft:6 }}>{r.surveyLabel}</span> : null}
      </span>
      <span className="mono" style={{ fontSize:"var(--text-caption)", fontWeight:700, color, flexShrink:0 }}>{slaDayText(r.overDays)}</span>
      <span style={{ fontSize:"var(--text-caption)", fontWeight:600, color:"var(--brand)", flexShrink:0 }}>›</span>
    </div>
  );
}
function DvSlaQueue({ rows, onOpenCase, source }) {
  if(!rows || !rows.length) return null;
  const shown = rows.slice(0, 30);
  return (
    <div style={{ marginTop:14, border:"1px solid var(--border)", borderRadius:"var(--radius-lg)", overflow:"hidden", background:"var(--card)" }}
      data-dv-sla-queue>
      <div style={{ display:"flex", alignItems:"center", gap:8, padding:"8px 12px",
        background:"var(--muted)", borderBottom:"1px solid var(--border)" }}>
        <span style={{ fontSize:"var(--text-body-md)", fontWeight:700 }}>遅れている</span>
        <span className="mono" style={{ fontSize:"var(--text-body-lg)", fontWeight:700, color:"var(--destructive)" }}>{rows.length}</span>
        <span style={{ fontSize:"var(--text-caption)", color:"var(--foreground-subtle)" }}>件</span>
        <span style={{ marginLeft:"auto", fontSize:"var(--text-caption)", color:"var(--muted-foreground)" }}>
          {source === "rpc"
            ? "全件から予定日・報告リズムの超過/予兆(決-20・rpc_sla_queue)。超過日数が大きい順"
            : "読み込み済みの案件から(接続時は全件)。超過日数が大きい順"}
        </span>
      </div>
      {shown.map((r,i)=><SlaRow key={r.caseId || r.prop + i} r={r} onOpen={onOpenCase} last={i===shown.length-1} />)}
      {rows.length > shown.length &&
        <div style={{ padding:"6px 12px", fontSize:"var(--text-caption)", color:"var(--foreground-subtle)", borderTop:"1px solid var(--border)" }}>
          ほか{rows.length - shown.length}件(超過日数が小さい)
        </div>}
    </div>
  );
}

function DvItem({ r, onOpenTodo, onContact, onToast, last, meName, now }) {
  const who = dvWho(r);
  const mine = dvIsMine(r, meName);
  const call = e => {
    e.stopPropagation();
    if(!who || !onContact) return;
    // ㉔: ContactModal 送信後に parties[].log → contact.log へ。caseId が無い行は正直に断る。
    onContact({
      name: who,
      ch: "電話",
      onSent: (label, body) => {
        if (!r.caseId || !window.MStore) {
          onToast && onToast("案件へ保存できる経路がありません");
          return;
        }
        const when = ((d)=>(d.getMonth()+1)+"/"+d.getDate())(new Date());
        window.MStore.updateCase(r.caseId, c => {
          let party = (c.parties||[]).find(x => x && (x.name === who || String(x.name).indexOf(who) >= 0));
          if (!party) {
            party = { role: "関係者", name: who, wait: null, log: [] };
            c.parties = [...(c.parties||[]), party];
          }
          party.log = [{ d: when, dir: "out", t: label + "：" + (body || "連絡") }, ...(party.log || [])];
          party.wait = null;
        });
        onToast && onToast(who + " への記録を案件に残しました");
      },
    });
  };
  const finish = e => {
    e.stopPropagation();
    if(!window.MStore || !r.caseId || !r.todoId) return;
    window.MStore.toggleTodo(r.caseId, r.todoId);
    onToast && onToast(r.step+" を完了にしました");
  };
  const dueText = now ? dvWhyNow(r.due) : dvShortDue(r.due);
  return (
    <div role="button" tabIndex={0} onClick={()=>onOpenTodo&&onOpenTodo(r)}
      onKeyDown={e=>{ if(e.key==="Enter"||e.key===" "){ e.preventDefault(); onOpenTodo&&onOpenTodo(r); } }}
      style={{ boxSizing:"border-box", width:"100%",
        display:"flex", alignItems:"center", gap:8, padding:"8px 12px", cursor:"pointer", fontFamily:"var(--font-sans)",
        borderBottom:last?"none":"1px solid var(--border)", transition:"background var(--duration-fast) var(--easing)",
        background:mine?"var(--brand-muted)":"transparent" }}
      onMouseEnter={e=>e.currentTarget.style.background=mine?"var(--brand-muted)":"var(--accent)"}
      onMouseLeave={e=>e.currentTarget.style.background=mine?"var(--brand-muted)":"transparent"}>
      <span className="mono" title={dvDueLabel(r.due)} style={{ fontSize:"var(--text-caption)", fontWeight:700, color:dvDueColor(r.due),
        width:now?72:46, flexShrink:0, whiteSpace:"nowrap" }}>{dueText}</span>
      <span style={{ minWidth:0, flex:1 }}>
        <span style={{ display:"block", fontSize:"var(--text-body-md)", fontWeight:600, overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap" }}>{r.step}</span>
        <span style={{ display:"block", fontSize:"var(--text-caption)", color:"var(--foreground-subtle)", overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap" }}>
          {r.prop}
          {r.surveyLabel && <span style={{ color:"var(--status-survey)", fontWeight:700 }}> · {r.surveyLabel}</span>}
          <span style={{ color:"var(--border-strong)" }}> · </span>{r.dunning || r.ball}
        </span>
      </span>
      {mine && <span style={{ ...dvChip, color:"var(--brand)", background:"var(--card)", border:"1px solid var(--brand-border)" }}>自分</span>}
      <DvFlag r={r} />
      <DvLeak r={r} />
      <span style={{ display:"inline-flex", gap:4, flexShrink:0 }} onClick={e=>e.stopPropagation()}>
        {who
          ? <button type="button" onClick={call} title={"電話："+who}
              style={{ all:"unset", cursor:"pointer", fontFamily:"var(--font-sans)", fontSize:"var(--text-caption)", fontWeight:600,
                padding:"3px 8px", borderRadius:"var(--radius-sm)", whiteSpace:"nowrap",
                color:"var(--brand)", background:"var(--brand-muted)", border:"1px solid var(--brand-border)" }}>電話</button>
          : <span data-dv-contact-missing="true" title="TODO詳細で連絡先を確認してください"
              style={{ fontSize:"var(--text-caption)", color:"var(--foreground-subtle)", padding:"3px 0", whiteSpace:"nowrap" }}>連絡先未登録</span>}
        <button type="button" onClick={finish} title="完了にする"
          style={{ all:"unset", cursor:"pointer", fontFamily:"var(--font-sans)", fontSize:"var(--text-caption)", fontWeight:600,
            padding:"3px 8px", borderRadius:"var(--radius-sm)", whiteSpace:"nowrap",
            color:"var(--success)", background:"var(--success-muted)", border:"1px solid var(--success-border)" }}>完了</button>
      </span>
    </div>
  );
}

function DvGroup({ label, tone, note, rows, onOpenTodo, onContact, onToast, onHeader, headerLabel, meName }) {
  const [hov, setHov] = React.useState(false);
  const [open, setOpen] = React.useState(false);
  const today = rows.filter(r=>{ const s=dvDue(r.due); return s==="over"||s==="today"; });
  const later = rows.filter(r=>{ const s=dvDue(r.due); return s!=="over"&&s!=="today"; });
  const on = rows.length>0;
  const itemProps = { onOpenTodo, onContact, onToast, meName };
  return (
    <div style={{ border:"1px solid var(--border)", borderRadius:"var(--radius-lg)", background:on?"var(--card)":"transparent",
      opacity:on?1:0.55, overflow:"hidden" }}>
      <button onClick={onHeader} onMouseEnter={()=>setHov(true)} onMouseLeave={()=>setHov(false)}
        style={{ all:"unset", boxSizing:"border-box", width:"100%", display:"flex", alignItems:"center", gap:7, cursor:"pointer",
          padding:"8px 12px", background:hov?"var(--accent)":"var(--muted)", borderBottom:"1px solid var(--border)",
          fontFamily:"var(--font-sans)", transition:"background var(--duration-fast) var(--easing)" }}>
        <span style={{ width:3, height:14, borderRadius:2, background:tone, flexShrink:0 }} />
        <span style={{ fontSize:"var(--text-body-md)", fontWeight:700, whiteSpace:"nowrap" }}>{label}</span>
        <span className="mono" style={{ fontSize:"var(--text-body-lg)", fontWeight:700, whiteSpace:"nowrap", color:today.length?tone:"var(--foreground-subtle)" }}>{today.length}</span>
        {note && <span style={{ fontSize:"var(--text-caption)", color:"var(--foreground-subtle)", overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap" }}>{note}</span>}
        <span style={{ marginLeft:"auto", fontSize:"var(--text-caption)", fontWeight:600, color:"var(--brand)", whiteSpace:"nowrap" }}>{headerLabel} ›</span>
      </button>
      {today.length===0
        ? <div style={{ padding:"10px 12px", fontSize:"var(--text-body-sm)", color:"var(--foreground-subtle)" }}>やることはありません</div>
        : (
          <React.Fragment>
            <div style={{ padding:"5px 12px 2px", fontSize:"var(--text-caption)", fontWeight:700, letterSpacing:"0.04em", color:"var(--muted-foreground)" }}>今すぐ（期限超過・本日締め）</div>
            {today.map((r,i)=><DvItem key={(r.todoId||r.trk)+i} r={r} {...itemProps} now last={i===today.length-1 && later.length===0} />)}
          </React.Fragment>
        )}
      {later.length>0 && (
        <React.Fragment>
          <button onClick={()=>setOpen(!open)} style={{ all:"unset", boxSizing:"border-box", width:"100%", display:"flex", alignItems:"center", gap:7,
            padding:"7px 12px", cursor:"pointer", fontFamily:"var(--font-sans)", borderTop:today.length?"1px solid var(--border)":"none",
            background:open?"var(--muted)":"transparent", transition:"background var(--duration-fast) var(--easing)" }}>
            <span style={{ fontSize:8, color:"var(--foreground-subtle)", display:"inline-block", transform:open?"rotate(90deg)":"none",
              transition:"transform var(--duration-fast) var(--easing)" }}>▶</span>
            <span style={{ fontSize:"var(--text-caption)", fontWeight:600, color:"var(--muted-foreground)" }}>数日内以降</span>
            <span className="mono" style={{ fontSize:"var(--text-caption)", fontWeight:700, color:"var(--foreground-subtle)" }}>{later.length}</span>
          </button>
          {open && later.map((r,i)=><DvItem key={(r.todoId||r.trk)+i} r={r} {...itemProps} last={i===later.length-1} />)}
        </React.Fragment>
      )}
    </div>
  );
}

function DvStatRow({ title, items, testId }) {
  return (
    <div style={{ marginTop:16 }} data-dv-stat={testId||undefined}>
      <div style={{ fontSize:"var(--text-caption)", fontWeight:700, letterSpacing:"0.04em", color:"var(--muted-foreground)", marginBottom:7 }}>{title}</div>
      <div style={{ display:"flex", gap:8, flexWrap:"wrap" }}>
        {items.map(it => (
          <button key={it.key} type="button" onClick={it.onClick} disabled={!it.n}
            style={{ all:"unset", cursor:it.n?"pointer":"default", fontFamily:"var(--font-sans)",
              display:"inline-flex", alignItems:"center", gap:8, padding:"8px 12px", borderRadius:"var(--radius-md)",
              border:"1px solid var(--border)", background:it.n?"var(--card)":"transparent", opacity:it.n?1:0.5,
              transition:"background var(--duration-fast) var(--easing)" }}>
            <span style={{ width:3, height:14, borderRadius:2, background:it.tone, flexShrink:0 }} />
            <span style={{ fontSize:"var(--text-body-sm)", fontWeight:600, color:"var(--foreground)" }}>{it.label}</span>
            <span className="mono" style={{ fontSize:"var(--text-body-md)", fontWeight:700, color:it.n?it.tone:"var(--foreground-subtle)" }}>{it.n}</span>
            {it.n>0 && <span style={{ fontSize:"var(--text-caption)", fontWeight:600, color:"var(--brand)" }}>›</span>}
          </button>
        ))}
      </div>
    </div>
  );
}

function DashboardV6({ go, openTodo, onContact, onToast, onOpenCase }) {
  const store = window.useMStore ? window.useMStore() : null;
  const rows = window.BV_ROWS || [];
  // ㉘(0080): 接続時は rpc_sla_queue で全件。未接続・失敗時は読み込み済みの MStore.slaQueue。
  const localSla = React.useMemo(()=> window.MStore && window.MStore.slaQueue ? window.MStore.slaQueue() : [], [store]);
  const [slaRemote, setSlaRemote] = React.useState(null); // null=未取得 / []=空 / [...]
  const [slaSource, setSlaSource] = React.useState("local");
  React.useEffect(() => {
    const cloud = window.MarurouCloud;
    if (!cloud || typeof cloud.slaQueue !== "function") {
      setSlaRemote(null);
      setSlaSource("local");
      return undefined;
    }
    let alive = true;
    cloud.slaQueue({ limit: 100 }).then(raw => {
      if (!alive) return;
      const list = Array.isArray(raw) ? raw.map(mapSlaRpcRow).filter(Boolean) : [];
      setSlaRemote(list);
      setSlaSource("rpc");
    }).catch(err => {
      console.warn("[ダッシュボード] rpc_sla_queue を読めませんでした。読み込み済み分を出します。", err);
      if (!alive) return;
      setSlaRemote(null);
      setSlaSource("local");
    });
    return () => { alive = false; };
  }, [store]);
  const slaRows = slaSource === "rpc" && Array.isArray(slaRemote) ? slaRemote : localSla;
  const me = window.MStore && window.MStore.me ? window.MStore.me() : null;
  const meName = dvPersonName(me && (me.displayName || me.name));
  const [mode, setMode] = React.useState("funnel");
  const [flag, setFlag] = React.useState(null);
  // ページング2段目(決-1・0033)。緊急・滞留・期限注意の見出しチップは
  // 「件数バッジ」に当たる ─ rpc_open_counts が数えたサーバの全件数を出す
  // (読み込んだ分(rows)だけで数えると、直近N件に入らない古い案件の緊急/滞留を
  //  見落として実際より少なく出る)。まだ届いていなければ、読み込んだ分から
  // 数えた暫定値を出す(0件のまま固まって見えるより実用的なため)。
  // 絞り込み(flag)自体は読み込んだ分(rows)の中でしか効かない ─
  // ここは一覧を絞る操作で、件数バッジとは役割が違う。
  const srvCounts = window.MStore ? window.MStore.counts() : null;
  const counts = srvCounts
    ? { urgent:srvCounts.urgent, delay:srvCounts.delay, warn:srvCounts.warn }
    : { urgent:rows.filter(r=>r.urgent).length, delay:rows.filter(r=>r.marker==="delay").length, warn:rows.filter(r=>r.marker==="warn"||r.watch).length };
  const base = React.useMemo(()=>{
    const f = flag ? rows.filter(r=>flag==="warn" ? (r.marker==="warn"||r.watch) : flag==="urgent" ? r.urgent : r.marker==="delay") : rows;
    return dvSortRows(f, meName);
  }, [rows, flag, store, meName]);
  const nowCount = base.filter(r=>{ const s=dvDue(r.due); return s==="over"||s==="today"; }).length;
  // ㉑: 「うち自分」は rpc_open_counts の mine.todosDue(0077)。未到着・旧応答は
  // 読み込み分から数えた暫定値(従来どおり)に落とす。
  const mineSrv = window.MStore && window.MStore.mine ? window.MStore.mine() : null;
  const myNow = (mineSrv && typeof mineSrv.todosDue === "number")
    ? mineSrv.todosDue
    : base.filter(r=>{ const s=dvDue(r.due); return (s==="over"||s==="today") && dvIsMine(r, meName); }).length;
  const stageCounts = React.useMemo(()=>{
    const meta = window.BV_CASEMETA || {};
    const tally = { "受付":0, "調査":0, "工事":0, "完了":0 };
    Object.keys(meta).forEach(id => {
      const s = meta[id].stage || "受付";
      if(tally[s]!=null) tally[s]++; else tally["受付"]++;
    });
    return tally;
  }, [store]);
  const trackCounts = React.useMemo(()=>{
    const tr = window.TR_ROWS || [];
    const tally = { survey:0, work:0, ins:0 };
    tr.forEach(r => { if(r.state==="now" && tally[r.kind]!=null) tally[r.kind]++; });
    return tally;
  }, [store]);
  const groups = mode==="funnel"
    ? DV_FUNNELS.map(f=>({ key:f.key, label:f.label, tone:f.tone, note:f.note, headerLabel:f.label+"一覧",
        onHeader:()=>go&&go(f.nav,{}), rows:base.filter(r=>dvFunnelOf(r)===f.key) }))
    : DV_TYPES.map(t=>({ key:t, label:t, tone:"var(--brand)", note:null, headerLabel:"TODO一覧",
        onHeader:()=>go&&go("tasks",{ tab:(t==="社内承認"||t==="社外承認")?"承認":t }), rows:base.filter(r=>dvTypeOf(r)===t) }));
  const live = mode==="funnel" ? groups : groups.filter(g=>g.rows.length>0);
  const empty = mode==="funnel" ? [] : groups.filter(g=>g.rows.length===0);
  return (
    <div style={{ padding:"18px 24px 32px 20px" }} data-dv-root>
      {/* 1行サマリ＋表示切替＋絞込 */}
      <div style={{ display:"flex", alignItems:"center", gap:10, flexWrap:"wrap", marginBottom:12 }}>
        <span className="mono" style={{ fontSize:"var(--text-body-md)", color:"var(--muted-foreground)" }}>{window.bvTodayLabel ? window.bvTodayLabel() : ""}</span>
        <span style={{ display:"inline-flex", padding:2, borderRadius:"var(--radius-md)", background:"var(--muted)", border:"1px solid var(--border)" }}>
          {[["funnel","ファネル別"],["type","種類別"]].map(([k,l])=>(
            <button key={k} onClick={()=>setMode(k)} style={{ all:"unset", cursor:"pointer", fontFamily:"var(--font-sans)",
              fontSize:"var(--text-body-sm)", fontWeight:mode===k?700:500, padding:"4px 13px", borderRadius:"var(--radius-sm)", whiteSpace:"nowrap",
              background:mode===k?"var(--card)":"transparent", color:mode===k?"var(--foreground)":"var(--muted-foreground)",
              boxShadow:mode===k?"var(--shadow-sm)":"none", transition:"all var(--duration-fast) var(--easing)" }}>{l}</button>
          ))}
        </span>
        <span style={{ display:"flex", gap:6, flexWrap:"wrap", marginLeft:"auto" }}>
          {Object.keys(counts).map(k=>{
            const [l,c,bg,bd] = DV_FLAGS[k==="warn"?"warn":k];
            const label = k==="warn"?"期限注意":l, on = flag===k, n = counts[k];
            return (
              <button key={k} onClick={()=>setFlag(on?null:k)} disabled={!n} style={{ all:"unset", cursor:n?"pointer":"default",
                fontFamily:"var(--font-sans)", fontSize:"var(--text-body-sm)", fontWeight:600, padding:"4px 11px", borderRadius:"var(--radius-full)",
                whiteSpace:"nowrap", opacity:n?1:0.45, color:on?"#fff":c, background:on?c:bg, border:"1px solid "+(on?c:bd) }}>
                {label} <span className="mono" style={{ fontWeight:700 }}>{n}</span>{on && <span style={{ marginLeft:5 }}>✕</span>}
              </button>
            );
          })}
        </span>
      </div>

      {/* Codex: AfAlert で空状態を明示 / Cursor: 件数サマリも出す */}
      {base.length===0
        ? <AfAlert variant="notice" title="現在、やることはありません">選んだ条件に当てはまる未完了TODOはありません。</AfAlert>
        : nowCount===0
          ? <AfAlert variant="notice" title="今すぐやることはありません">先の期限は各列の「数日内以降」で確認できます。</AfAlert>
          : <div data-dv-now-summary style={{ marginBottom:12, padding:"10px 12px", borderRadius:"var(--radius-md)",
              border:"1px solid var(--warning-border)", background:"var(--warning-muted)" }}>
              <span style={{ fontSize:"var(--text-body-md)", fontWeight:600, color:"var(--foreground)" }}>
                今すぐ <span className="mono">{nowCount}</span> 件
                {myNow>0 && <span style={{ marginLeft:8, fontSize:"var(--text-body-sm)", color:"var(--brand)" }}>うち自分 {myNow}</span>}
                <span style={{ marginLeft:8, fontSize:"var(--text-caption)", fontWeight:500, color:"var(--muted-foreground)" }}>期限超過または本日締め</span>
              </span>
            </div>}

      <DvSlaQueue rows={slaRows} onOpenCase={onOpenCase} source={slaSource} />

      <div style={{ overflowX:"auto", paddingBottom:4 }}>
      <div style={{ display:"grid", gap:12, alignItems:"start",
        gridTemplateColumns:mode==="funnel" ? "repeat(4,minmax(236px,1fr))" : "repeat(8,minmax(212px,1fr))" }}>
        {live.map(g=><DvGroup key={g.key} {...g} meName={meName} onOpenTodo={openTodo} onContact={onContact} onToast={onToast} />)}
      </div>
      </div>
      {empty.length>0 && (
        <div style={{ display:"flex", gap:6, flexWrap:"wrap", alignItems:"center", marginTop:12 }}>
          <span style={{ fontSize:"var(--text-caption)", color:"var(--foreground-subtle)" }}>0件</span>
          {empty.map(g=>(
            <button key={g.key} onClick={g.onHeader} style={{ all:"unset", cursor:"pointer", fontFamily:"var(--font-sans)",
              fontSize:"var(--text-body-sm)", color:"var(--foreground-subtle)", padding:"3px 10px", borderRadius:"var(--radius-full)",
              border:"1px solid var(--border)" }}>{g.label} <span className="mono">0</span></button>
          ))}
        </div>
      )}

      <DvStatRow title="案件の段階" testId="stage" items={DV_STAGES.map(s=>({
        key:s.key, label:s.key, tone:s.tone, n:stageCounts[s.key]||0,
        onClick:()=>go&&go("cases",{ stage:s.key }),
      }))} />
      <DvStatRow title="進行中のトラック" testId="track" items={DV_TRACK_KINDS.map(t=>({
        key:t.key, label:t.label, tone:t.tone, n:trackCounts[t.key]||0,
        onClick:()=>go&&go(t.nav,{ filter:"進行中" }),
      }))} />

      <div style={{ fontSize:"var(--text-caption)", color:"var(--muted-foreground)", marginTop:14, lineHeight:1.7 }}>
今すぐには期限超過・本日締めだけを出し、理由（何日超過／本日締め）を行に表示します。自分担当を先に並べます。0件のときは「やることはありません」と明示します。役割名だけのTODOには電話を出しません。段階・進行中トラックはタップで絞込一覧へ。
        {srvCounts && rows.length < srvCounts.todos &&
          <React.Fragment>上の緊急・滞留・期限注意の件数は全{srvCounts.cases}件から数えたもの。列の中身・段階・進行中トラックは画面が読み込んでいる分（{rows.length}件のTODO）から。他の案件は検索（⌘K）で探せる。</React.Fragment>}
      </div>
    </div>
  );
}

Object.assign(window, { DashboardV6, DV_FUNNELS, DV_TYPES, DV_STAGES, DV_TRACK_KINDS, dvTypeOf, dvFunnelOf,
  dvWhyNow, dvIsMine, dvDashboardContactName:dvWho, dvDashboardSortRows:dvSortRows,
  // 盤面・ダッシュボード-12(0049・決-20) / ㉘ rpc_sla_queue。
  DvSlaQueue, SlaRow, SLA_REASON_LABELS, mapSlaRpcRow });
