// マルロウ 漏水案件OS — UI atoms. Global-scoped, prefixed names to avoid collision.

const afBtnBase = {
  display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
  height: 28, padding: '0 12px', borderRadius: 'var(--radius-md)',
  fontSize: 'var(--text-body-md)', fontWeight: 600,
  // border(一括)と borderColor(個別)を混ぜると、React が再描画のたびに
  //   Removing borderColor border …don't mix shorthand and non-shorthand properties
  // を出す。variant が borderColor だけを差し替えるので、ここは3つに分けて持つ。
  // (見た目は同じ。警告が出るとE2Eの「コンソールにエラーが出ない」が落ちる。)
  borderWidth: 1, borderStyle: 'solid', borderColor: 'var(--border)',
  background: 'var(--secondary)',
  color: 'var(--secondary-foreground)', cursor: 'pointer',
  transition: 'all var(--duration-fast) var(--easing)', whiteSpace: 'nowrap',
  fontFamily: 'var(--font-sans)', gap: 6
};
function AfButton({ variant = 'secondary', size = 'md', children, style = {}, ...rest }) {
  const variants = {
    secondary: {},
    primary: { background: 'var(--primary)', color: 'var(--primary-foreground)', borderColor: 'var(--primary)' },
    danger: { background: 'var(--destructive-muted)', color: 'var(--destructive)', borderColor: 'var(--destructive-border)' },
    ghost: { background: 'transparent', borderColor: 'transparent' },
    link: { background: 'transparent', borderColor: 'transparent', color: 'var(--brand)', textDecoration: 'underline', textUnderlineOffset: 3, padding: 0, height: 'auto' }
  };
  const sizes = { sm: { height: 22, padding: '0 8px', fontSize: 'var(--text-body-sm)' }, md: {}, lg: { height: 38, padding: '0 16px' } };
  const [hover, setHover] = React.useState(false);
  const hoverStyle = hover ?
  variant === 'primary' ? { background: 'var(--primary-hover)', borderColor: 'var(--primary-hover)' } :
  variant === 'ghost' ? { background: 'var(--muted)' } :
  variant === 'link' ? {} :
  { background: 'var(--muted)', borderColor: 'var(--border-strong)', boxShadow: 'var(--shadow-sm)' } :
  {};
  return <button onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
  style={{ ...afBtnBase, ...variants[variant], ...sizes[size], ...hoverStyle, ...style }} {...rest}>{children}</button>;
}

const CHIP_TONES = {
  neutral: { background: 'var(--muted)', color: 'var(--foreground)', borderColor: 'var(--border-strong)' },
  brand: { background: 'var(--brand-muted)', color: 'var(--brand)', borderColor: 'var(--brand-border)' },
  success: { background: 'var(--success-muted)', color: 'var(--success)', borderColor: 'var(--success-border)' },
  warning: { background: 'var(--warning-muted)', color: 'var(--warning)', borderColor: 'var(--warning-border)' },
  destructive: { background: 'var(--destructive-muted)', color: 'var(--destructive)', borderColor: 'var(--destructive-border)' },
  'st-intake': { background: 'var(--status-intake-muted)', color: 'var(--status-intake)', borderColor: 'var(--status-intake-border)' },
  'st-survey': { background: 'var(--status-survey-muted)', color: 'var(--status-survey)', borderColor: 'var(--status-survey-border)' },
  'st-estimate': { background: 'var(--status-estimate-muted)', color: 'var(--status-estimate)', borderColor: 'var(--status-estimate-border)' },
  'st-progress': { background: 'var(--status-progress-muted)', color: 'var(--status-progress)', borderColor: 'var(--status-progress-border)' },
  'st-done': { background: 'var(--status-done-muted)', color: 'var(--status-done)', borderColor: 'var(--status-done-border)' },
  'st-hold': { background: 'var(--status-hold-muted)', color: 'var(--status-hold)', borderColor: 'var(--status-hold-border)' },
  'st-canceled': { background: 'var(--status-canceled-muted)', color: 'var(--status-canceled)', borderColor: 'var(--status-canceled-border)' },
  'w-supply': { background: 'var(--work-supply-muted)', color: 'var(--work-supply)', borderColor: 'var(--work-supply-border)' },
  'w-drainage': { background: 'var(--work-drainage-muted)', color: 'var(--work-drainage)', borderColor: 'var(--work-drainage-border)' },
  'w-waterproof': { background: 'var(--work-waterproof-muted)', color: 'var(--work-waterproof)', borderColor: 'var(--work-waterproof-border)' },
  'w-fixture': { background: 'var(--work-fixture-muted)', color: 'var(--work-fixture)', borderColor: 'var(--work-fixture-border)' },
  emergency: { background: 'var(--urgency-emergency)', color: '#fff', borderColor: 'var(--urgency-emergency)' }
};
function AfChip({ tone = 'neutral', dot = false, children, style = {} }) {
  const t = CHIP_TONES[tone] || CHIP_TONES.neutral;
  return <span style={{
    display: 'inline-flex', alignItems: 'center', gap: dot ? 5 : 0, height: 20, padding: '0 6px',
    borderRadius: 'var(--radius-md)', fontSize: 'var(--text-body-sm)', fontWeight: 600,
    borderWidth: 1, borderStyle: 'solid', whiteSpace: 'nowrap', ...t, ...style
  }}>{dot && <span style={{ fontSize: 7, lineHeight: 1 }}>●</span>}{children}</span>;
}

function AfInput({ style = {}, mono = false, ...rest }) {
  const [focus, setFocus] = React.useState(false);
  return <input onFocus={() => setFocus(true)} onBlur={() => setFocus(false)} style={{
    height: 28, padding: '0 8px', borderRadius: 'var(--radius-md)',
    border: `1px solid ${focus ? 'var(--brand)' : 'var(--input)'}`,
    boxShadow: focus ? 'var(--ring-focus)' : 'none',
    background: 'var(--card)', fontSize: 'var(--text-body-md)', color: 'var(--foreground)',
    fontFamily: mono ? 'var(--font-mono)' : 'var(--font-sans)', outline: 'none',
    transition: 'border-color var(--duration-fast) var(--easing), box-shadow var(--duration-fast) var(--easing)', ...style
  }} {...rest} />;
}

function AfKbd({ children }) {
  return <kbd style={{
    display: 'inline-flex', alignItems: 'center', height: 18, padding: '0 5px',
    borderRadius: 'var(--radius-sm)', background: 'var(--muted)', border: '1px solid var(--border)',
    fontSize: 'var(--text-caption)', fontFamily: 'var(--font-mono)', color: 'var(--muted-foreground)'
  }}>{children}</kbd>;
}

function AfAlert({ variant = 'info', title, children, style = {} }) {
  const v = {
    info: { bg: 'var(--muted)', border: 'var(--border-strong)', fg: 'var(--foreground)', dot: 'var(--foreground-subtle)', label: 'i', italic: true },
    notice: { bg: 'var(--warning-muted)', border: 'var(--warning-border)', fg: 'var(--warning)', dot: 'var(--warning)', label: '!' },
    success: { bg: 'var(--success-muted)', border: 'var(--success-border)', fg: 'var(--success)', dot: 'var(--success)', label: '✓' },
    error: { bg: 'var(--destructive-muted)', border: 'var(--destructive-border)', fg: 'var(--destructive)', dot: 'var(--destructive)', label: '!' }
  }[variant];
  return <div style={{ display: 'flex', gap: 10, padding: '10px 12px', background: v.bg, border: `1px solid ${v.border}`, borderRadius: 'var(--radius-md)', alignItems: 'flex-start', ...style }}>
    <div style={{ width: 16, height: 16, borderRadius: 999, background: v.dot, color: 'white', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: 10, fontWeight: 700, fontStyle: v.italic ? 'italic' : 'normal', flexShrink: 0, marginTop: 1 }}>{v.label}</div>
    <div style={{ fontSize: 'var(--text-body-md)', lineHeight: 1.5, color: v.fg }}>
      {title && <b>{title}{children ? ' · ' : ''}</b>}{children}
    </div>
  </div>;
}

/* ㉝: 保存失敗の共通見え方。赤の帯 + 理由 + 「もう一度」。
   ContactModal / 案件詳細(書き込み層) / 精算 / 管理画面で同じ形にする。 */
function AfSaveFail({ reason, onRetry, onDismiss, title, style = {} }) {
  const head = title || '保存できませんでした';
  const body = reason || 'この変更はサーバに残っていません。';
  return (
    <div role="alert" data-af-save-fail style={{
      display: 'flex', flexDirection: 'column', gap: 8, padding: '10px 12px',
      background: 'var(--destructive-muted)', border: '1px solid var(--destructive-border)',
      borderRadius: 'var(--radius-md)', color: 'var(--destructive)', ...style,
    }}>
      <div style={{ display: 'flex', gap: 10, alignItems: 'flex-start' }}>
        <div style={{ width: 16, height: 16, borderRadius: 999, background: 'var(--destructive)', color: 'var(--destructive-foreground)',
          display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: 10, fontWeight: 700, flexShrink: 0, marginTop: 1 }}>!</div>
        <div style={{ flex: 1, minWidth: 0, fontSize: 'var(--text-body-md)', lineHeight: 1.55 }}>
          <b>{head}</b>
          <div style={{ marginTop: 4, whiteSpace: 'pre-wrap', wordBreak: 'break-word', color: 'var(--foreground)' }}>{body}</div>
        </div>
        {onDismiss && (
          <button type="button" aria-label="閉じる" onClick={onDismiss}
            style={{ all: 'unset', cursor: 'pointer', color: 'var(--muted-foreground)', fontSize: 'var(--text-caption)', fontWeight: 700, padding: '0 2px' }}>×</button>
        )}
      </div>
      {onRetry && (
        <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
          <button type="button" onClick={onRetry}
            style={{ all: 'unset', cursor: 'pointer', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-body-sm)', fontWeight: 700,
              color: 'var(--destructive-foreground)', background: 'var(--destructive)', borderRadius: 'var(--radius-sm)',
              padding: '5px 12px' }}>もう一度</button>
        </div>
      )}
    </div>
  );
}

/** 書き込み層・管理画面など JSX 外から同じ形で出す(㉝)。 */
function showSaveFail({ reason, title, onRetry } = {}) {
  let host = document.getElementById('marurou-save-fail');
  if (!host) {
    host = document.createElement('div');
    host.id = 'marurou-save-fail';
    host.style.cssText = 'position:fixed;right:12px;bottom:12px;z-index:99999;max-width:min(420px,calc(100vw - 24px));';
    document.body.appendChild(host);
  }
  const retry = onRetry;
  const dismiss = () => { host.innerHTML = ''; host.style.display = 'none'; };
  host.style.display = 'block';
  host.innerHTML = '';
  const wrap = document.createElement('div');
  wrap.setAttribute('role', 'alert');
  wrap.setAttribute('data-af-save-fail', '1');
  wrap.style.cssText = 'display:flex;flex-direction:column;gap:8px;padding:10px 12px;' +
    'background:var(--destructive-muted);border:1px solid var(--destructive-border);' +
    'border-radius:var(--radius-md);color:var(--destructive);' +
    'box-shadow:var(--shadow-md);font-family:var(--font-sans);';
  const row = document.createElement('div');
  row.style.cssText = 'display:flex;gap:10px;align-items:flex-start;';
  const mark = document.createElement('div');
  mark.style.cssText = 'width:16px;height:16px;border-radius:999px;background:var(--destructive);' +
    'color:var(--destructive-foreground);display:inline-flex;align-items:center;justify-content:center;' +
    'font-size:10px;font-weight:700;flex-shrink:0;margin-top:1px;';
  mark.textContent = '!';
  const text = document.createElement('div');
  text.style.cssText = 'flex:1;min-width:0;font-size:var(--text-body-md);line-height:1.55;';
  const head = document.createElement('b');
  head.textContent = title || '保存できませんでした';
  const body = document.createElement('div');
  body.style.cssText = 'margin-top:4px;white-space:pre-wrap;word-break:break-word;color:var(--foreground);';
  body.textContent = reason || 'この変更はサーバに残っていません。';
  text.appendChild(head);
  text.appendChild(body);
  const close = document.createElement('button');
  close.type = 'button';
  close.setAttribute('aria-label', '閉じる');
  close.textContent = '×';
  close.style.cssText = 'all:unset;cursor:pointer;color:var(--muted-foreground);font-size:var(--text-caption);font-weight:700;padding:0 2px;';
  close.onclick = dismiss;
  row.appendChild(mark);
  row.appendChild(text);
  row.appendChild(close);
  wrap.appendChild(row);
  if (typeof retry === 'function') {
    const foot = document.createElement('div');
    foot.style.cssText = 'display:flex;justify-content:flex-end;';
    const btn = document.createElement('button');
    btn.type = 'button';
    btn.textContent = 'もう一度';
    btn.style.cssText = 'all:unset;cursor:pointer;font-family:var(--font-sans);font-size:var(--text-body-sm);font-weight:700;' +
      'color:var(--destructive-foreground);background:var(--destructive);border-radius:var(--radius-sm);padding:5px 12px;';
    btn.onclick = () => { dismiss(); try { retry(); } catch (_) {} };
    foot.appendChild(btn);
    wrap.appendChild(foot);
  }
  host.appendChild(wrap);
  return dismiss;
}

function AfCard({ children, style = {}, pad = 16, onClick }) {
  return <div onClick={onClick} style={{ background: 'var(--card)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', padding: pad, ...style }}>{children}</div>;
}

function SectionTitle({ children, right, sub }) {
  return <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 12 }}>
    <h3 style={{ margin: 0, fontSize: 'var(--text-display-sm)', fontWeight: 600, letterSpacing: 'var(--tracking-tight)' }}>{children}</h3>
    {sub && <span style={{ fontSize: 'var(--text-body-sm)', color: 'var(--muted-foreground)' }}>{sub}</span>}
    <div style={{ flex: 1 }} />
    {right}
  </div>;
}

// ── Domain atoms ──────────────────────────────────────────────────────

// 動態 (track dynamics): active / waiting / stalled / done / not-started
const DYN = {
  active: { color: 'var(--brand)', label: '動作中', glyph: '●' },
  wait: { color: 'var(--foreground-subtle)', label: '待機', glyph: '○' },
  stall: { color: 'var(--warning)', label: '滞り', glyph: '●' },
  stall2: { color: 'var(--destructive)', label: '重大滞り', glyph: '●' },
  done: { color: 'var(--success)', label: '完了', glyph: '✓' },
  idle: { color: 'var(--foreground-subtle)', label: '未着手', glyph: '◍' }
};
function DynDot({ kind = 'wait', style = {} }) {
  const d = DYN[kind] || DYN.wait;
  return <span style={{ display: 'inline-flex', width: 14, justifyContent: 'center', color: d.color, fontSize: kind === 'done' ? 11 : 10, lineHeight: 1, ...style }}>{d.glyph}</span>;
}

// 進め方 P1/P2/P3 の名前(決-40・池田さんの仕様 v1.20 骨子 7-7)。値('P1' 等)は DB の approach_type のまま。
// 旧定義(〜v1.19)は P1 と P3 が逆だったので、画面では値だけを出さず必ずこの名前を添える。
//   P1 先行工事 … 事故受付後すぐ着手(認定・入金を待たない。住めない等の緊急)
//   P2 認定後着工 … 認定金額が判明したら着手
//   P3 入金後着工 … 保険金の入金を確かめてから着手(最も慎重)
const AF_APPROACH_NAMES = { P1: '先行工事', P2: '認定後着工', P3: '入金後着工' };
function afApproachLabel(v) {
  if (!v) return '';
  return AF_APPROACH_NAMES[v] ? v + ' ' + AF_APPROACH_NAMES[v] : String(v);
}

// ボール保持者 (who holds the ball)
const BALL = {
  self: { label: '自社', ja: '自', bg: 'var(--brand-muted)', fg: 'var(--brand)', bd: 'var(--brand-border)' },
  partner: { label: '業者', ja: '工', bg: 'var(--work-supply-muted)', fg: 'var(--work-supply)', bd: 'var(--work-supply-border)' },
  insurer: { label: '保険会社', ja: '保', bg: 'var(--work-waterproof-muted)', fg: 'var(--work-waterproof)', bd: 'var(--work-waterproof-border)' },
  client: { label: '依頼者', ja: '依', bg: 'var(--work-fixture-muted)', fg: 'var(--work-fixture)', bd: 'var(--work-fixture-border)' },
  none: { label: '—', ja: '—', bg: 'var(--muted)', fg: 'var(--foreground-subtle)', bd: 'var(--border)' }
};
function BallChip({ who = 'self', compact = false, style = {} }) {
  const b = BALL[who] || BALL.none;
  return <span title={'ボール保持者: ' + b.label} style={{
    display: 'inline-flex', alignItems: 'center', gap: 4, height: 18, padding: compact ? '0 4px' : '0 6px',
    borderRadius: 'var(--radius-sm)', fontSize: 'var(--text-caption)', fontWeight: 600,
    background: b.bg, color: b.fg, border: '1px solid ' + b.bd, whiteSpace: 'nowrap'
  }}>
    <span style={{ width: 13, height: 13, borderRadius: 999, background: b.fg, color: '#fff', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: 8, fontWeight: 700, flexShrink: 0 }}>{b.ja}</span>
    {!compact && b.label}
  </span>;
}

// 方針の目処 — ✓ 確定 / ⚠️ 未確定 (mark, not color — per design rule)
function ConfirmMark({ ok = true, style = {} }) {
  return <span style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 15, height: 15, borderRadius: 999, fontSize: 9, fontWeight: 700, flexShrink: 0,
    background: ok ? 'var(--success-muted)' : 'var(--warning-muted)', color: ok ? 'var(--success)' : 'var(--warning)', border: '1px solid ' + (ok ? 'var(--success-border)' : 'var(--warning-border)'), ...style }}>{ok ? '✓' : '!'}</span>;
}

// 滞り severity badge 🟠 / 🔴
function SeverityBadge({ level = 'normal' }) {
  const m = level === 'critical' ?
  { c: 'var(--destructive)', bg: 'var(--destructive-muted)', bd: 'var(--destructive-border)', t: '重大' } :
  { c: 'var(--warning)', bg: 'var(--warning-muted)', bd: 'var(--warning-border)', t: '滞り' };
  return <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, height: 18, padding: '0 6px', borderRadius: 'var(--radius-full)', fontSize: 'var(--text-caption)', fontWeight: 700, background: m.bg, color: m.c, border: '1px solid ' + m.bd, whiteSpace: 'nowrap' }}>
    <span style={{ fontSize: 7 }}>●</span>{m.t}
  </span>;
}

function MiniMeter({ done, total }) {
  const pct = total ? Math.round(done / total * 100) : 0;
  return <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
    <div style={{ width: 44, height: 4, borderRadius: 999, background: 'var(--bg-pressed)', overflow: 'hidden' }}>
      <div style={{ width: pct + '%', height: '100%', background: pct === 100 ? 'var(--success)' : 'var(--brand)' }} />
    </div>
    <span className="mono" style={{ fontSize: 'var(--text-caption)', color: 'var(--muted-foreground)', fontWeight: 600 }}>{done}/{total}</span>
  </div>;
}

// ============================================================
// PhoneLink — 通知・連携-2(決-33・0071)。電話番号をワンクリック発信リンクにする共通部品。
// ============================================================
// なぜ: 決-33「(A) ワンクリック起動は今すぐ」。Zoom Phone のデスクトップ/モバイルアプリを
//   カスタムURLスキーム(zoomphonecall://)で開く。アプリが入っていない端末・まだZoom Phone
//   の展開が済んでいない間はtel:(OS標準の電話・発信アプリ)へフォールバックする ─
//   ブラウザには「アプリが実際に開けたか」を確かめる手段が無い(独自スキームの起動失敗を
//   検知するAPIは無い)ので、フォールバックは実行時の自動判定ではなく、
//   notify_settings.phone_link_scheme(tenant単位。既定'tel')を管理者がSQLで切り替える
//   運用にした(docs/15-notify.md §9)。値はrpc_get_phone_link_scheme()経由で取得し、
//   モジュール内でキャッシュする(電話番号ごとに毎回RPCを呼ばない)。
//
// 記録(連絡の記録・contact.log op)はPhoneLink自身が作らない ─ 書き込み層を増やさない
// 方針(依頼票どおり)。呼び出し側(CaseDetailV25.jsx の logContact、Screens.jsx の
// PartnerDetail の onSent と同型)が既に持っている「案件のparty.logへ1行足す」処理を
// onCallで呼んでもらう。PhoneLink自体は表示とリンクの組み立てだけに徹する。
//
// 電話番号の正規化(normalizePhoneE164)は marurou/phone.js に切り出してある
// (JSXを含まないので tests/ui/phone-link.test.js がNodeのvmで直接評価できる)。
let _phoneLinkSchemeCache = null;   // 'zoomphonecall' | 'tel' | null(未取得)
function usePhoneLinkScheme() {
  const [scheme, setScheme] = React.useState(_phoneLinkSchemeCache || "tel");
  React.useEffect(() => {
    if (_phoneLinkSchemeCache) return;
    if (!window.MarurouCloud || typeof window.MarurouCloud.getPhoneLinkScheme !== "function") return;
    window.MarurouCloud.getPhoneLinkScheme().then(s => {
      _phoneLinkSchemeCache = s === "zoomphonecall" ? "zoomphonecall" : "tel";
      setScheme(_phoneLinkSchemeCache);
    }).catch(() => { /* 取得できなければ既定のtel:のまま(安全側に倒す) */ });
  }, []);
  return scheme;
}

/**
 * PhoneLink — 電話番号(表示文字列)を受け取り、ワンクリック発信リンクにする。
 * props:
 *   phone   表示・発信する電話番号(必須。無ければ何も描かない)
 *   name    相手の名前(onCallに渡すだけ。表示には使わない)
 *   onCall  クリックした瞬間に呼ぶコールバック({ phone, e164, name })。
 *           連絡の記録を作るかどうか・どう作るかは呼び出し側の責務。
 *   children 表示文字列を上書きしたいとき(既定は phone そのまま)
 */
function PhoneLink({ phone, name, onCall, className, style, children }) {
  const scheme = usePhoneLinkScheme();
  if (!phone) return null;
  const e164 = window.normalizePhoneE164 ? window.normalizePhoneE164(phone) : null;
  const href = e164 ? (scheme + "://" + e164) : ("tel:" + phone);
  return (
    <a href={href} className={className}
      title={"Zoom Phoneで発信: " + phone}
      style={{ color: "var(--brand)", textDecoration: "none", fontWeight: 600, ...style }}
      onClick={() => {
        if (typeof onCall !== "function") return;
        try { onCall({ phone, e164, name: name || null }); }
        catch (e) { console.error("PhoneLink onCall failed:", e); }
      }}>
      {children || phone}
    </a>
  );
}

/* 報告先の語彙 — ここから
   報告先(取引先の既定 organizations.default_report_to / 案件の periodic_report_to)は
   自由入力のタグ。画面に並べる候補だけをここで決める。語は**現場の言葉**に合わせる
   (K-27。9/23 池田さんの規定値シート 27 社: 依頼担当者・居住者が全社に入り、残り 3 語が
   組み合わさる)。旧候補(9/3 開発側の仮置き)は REPORT_ROLE_ALIASES で読み替える。
   仕様書(04 情報整理 v1.24「報告先」)の立場 建物管理／原因管理／被害管理／貸しオーナー／
   居住オーナー／賃借人 は抽象名で、現場の言葉とは一致しないので候補には載せない(docs/03 §3.1)。
   このブロックは JSX を含まない素の JS にしておく(tests/ui/report-roles.test.js が切り出して評価する)。 */
const REPORT_ROLES = ['依頼担当者', '居住者', 'オーナー', '建物管理会社', '上下階管理会社'];
// 「依頼担当者」= この取引先自身。受付では取引先名に置き換える(Intake.jsx / PartnersV6.jsx pvReportTo)。
const REPORT_ROLE_SELF = '依頼担当者';
// 旧語 → 新語(既存データに残りうる。表示と受付の初期値はこの表で読み替える。DB は書き換えない)。
const REPORT_ROLE_ALIASES = {
  '依頼者': '依頼担当者',
  '入居者': '居住者',
  '建物オーナー': 'オーナー',
  '被害箇所の管理会社': '建物管理会社',
};
// 旧語を新語に読み替え、重複と空を落とす。知らない語(社名など)はそのまま通す。
const normReportRoles = (list) => {
  const out = [];
  for (const raw of Array.isArray(list) ? list : []) {
    const s = raw == null ? '' : String(raw).trim();
    if (!s) continue;
    const v = Object.prototype.hasOwnProperty.call(REPORT_ROLE_ALIASES, s) ? REPORT_ROLE_ALIASES[s] : s;
    if (out.indexOf(v) < 0) out.push(v);
  }
  return out;
};
// 受付の初期値: 読み替えたうえで「依頼担当者」(旧「依頼者」も)を取引先名に置き換える。取引先名が無ければ語のまま。
const resolveReportTo = (list, client) => {
  const name = client == null ? '' : String(client).trim();
  const out = [];
  for (const v of normReportRoles(list)) {
    const w = (v === REPORT_ROLE_SELF && name) ? name : v;
    if (out.indexOf(w) < 0) out.push(w);
  }
  return out;
};
/* 報告先の語彙 — ここまで */

Object.assign(window, { AfButton, AfChip, AfInput, AfKbd, AfAlert, AfSaveFail, showSaveFail, AfCard, SectionTitle, DynDot, DYN, BallChip, BALL, AF_APPROACH_NAMES, afApproachLabel, ConfirmMark, SeverityBadge, MiniMeter, PhoneLink,
  REPORT_ROLES, REPORT_ROLE_SELF, REPORT_ROLE_ALIASES, normReportRoles, resolveReportTo });
