// Intake — 受付フロー: チャネル受信 → 緊急度判定 → 初動方針 → 案件登録(K18-47。左にウィザード・右に受信トレイ)。
// 4チャネル取り込みはまだ無い。受信トレイは空が正しい。

function ChannelGlyph({ ch }) {
  const map = { phone: '☎', mail: '✉', slack: '#', hp: '⌂' };
  const tone = { phone: 'var(--brand)', mail: 'var(--work-fixture)', slack: 'var(--work-waterproof)', hp: 'var(--work-drainage)' };
  return <span style={{ width: 22, height: 22, borderRadius: 'var(--radius-sm)', flexShrink: 0, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, fontWeight: 700, background: 'var(--muted)', color: tone[ch], border: '1px solid var(--border)' }}>{map[ch]}</span>;
}

function Radio({ checked, label, sub, onClick }) {
  return (
    <button onClick={onClick} style={{
      all: 'unset', cursor: 'pointer', boxSizing: 'border-box', display: 'flex', alignItems: 'flex-start', gap: 9, width: '100%',
      padding: '10px 12px', borderRadius: 'var(--radius-md)', border: '1px solid ' + (checked ? 'var(--brand)' : 'var(--border)'),
      background: checked ? 'var(--brand-muted)' : 'var(--card)', transition: 'all var(--duration-fast) var(--easing)',
    }}>
      <span style={{ width: 15, height: 15, borderRadius: 999, flexShrink: 0, marginTop: 1, border: '2px solid ' + (checked ? 'var(--brand)' : 'var(--border-strong)'), background: 'var(--card)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
        {checked && <span style={{ width: 7, height: 7, borderRadius: 999, background: 'var(--brand)' }} />}
      </span>
      <span style={{ minWidth: 0 }}>
        <span style={{ fontSize: 'var(--text-body-md)', fontWeight: 600, color: checked ? 'var(--brand)' : 'var(--foreground)' }}>{label}</span>
        {sub && <span style={{ display: 'block', fontSize: 'var(--text-body-sm)', color: 'var(--muted-foreground)', marginTop: 2 }}>{sub}</span>}
      </span>
    </button>
  );
}

function FieldLabel({ children, hint }) {
  return <div style={{ display: 'flex', alignItems: 'baseline', gap: 6, marginBottom: 5 }}>
    <span style={{ fontSize: 'var(--text-caption)', fontWeight: 600, letterSpacing: '0.06em', color: 'var(--muted-foreground)', textTransform: 'uppercase' }}>{children}</span>
    {hint && <span style={{ fontSize: 'var(--text-caption)', color: 'var(--foreground-subtle)' }}>{hint}</span>}
  </div>;
}

// K18-47: 段の名前は池田さんの画面編集版(screens/marurou/Intake.jsx:3)に合わせる。中身は前と同じ順
// (チャネル受信=受付チャネル・依頼先・聞き取り / 緊急度判定=依頼内容・緊急度・居住制限 /
//  初動方針=方針の要約・本担当・報告先 / 案件登録=建物・対象部屋・依頼者規定・写真)。
const INTAKE_STEPS = ['チャネル受信', '緊急度判定', '初動方針', '案件登録'];

// K18-17: 受付チャネル(cases.intake_channel)。語彙は DB の comm_channel そのもの
// (supabase/migrations/0001_enums.sql:46。接触記録の channel と同じ型)。
// 書き込み層(app/write/mstore-diff.js の COMM_CHANNELS)と同じ並びにしておく ─
// ここに enum に無い言葉を足すと、保存が「選択肢のどれでもない」で止まる。
// 既定は「電話」(受付はほぼ受電。受信トレイの取り込みが入れば、その受信のチャネルが初期値になる)。
const INTAKE_CHANNELS = ['電話', 'メール', 'LINE', 'SMS', 'Slack', 'HP問合せ', '来訪', 'その他'];

// 受付の日付。以前は '2026/06/16' / '06/17' が焼き込まれており、
// いつ受け付けても同じ日付で登録され、TODOの期限も動かなかった。
const msPad = n => String(n).padStart(2, '0');
const msToday = () => { const d = new Date(); return `${d.getFullYear()}/${msPad(d.getMonth()+1)}/${msPad(d.getDate())}`; };
const msTodayMd = () => { const d = new Date(); return `${d.getMonth()+1}/${d.getDate()}`; };
const msTomorrowMd = () => { const d = new Date(Date.now() + 86400000); return `${msPad(d.getMonth()+1)}/${msPad(d.getDate())}`; };

function intakeGuessKind(file) {
  const isImg = file && file.type && String(file.type).startsWith('image');
  return isImg ? '写真' : /請求/.test(file.name || '') ? '請求書' : /見積/.test(file.name || '') ? '見積書'
    : /発注/.test(file.name || '') ? '発注書' : /報告/.test(file.name || '') ? '報告書' : 'その他';
}

function IntakeWizard({ msg, onDone, onCancel, onCreate, onToast }) {
  const [step, setStep] = React.useState(0);
  const [urgent, setUrgent] = React.useState(msg.urgent ? 'rush' : 'normal');
  // K18-14(0107): 居住制限(被害側 3 値)。既定は未選択('')= 保存しない。選択肢は store.jsx の
  // LIVING_RESTRICTION_OPTIONS(書き込み層 mstore-diff.js LIVING_RESTRICTIONS と同じ並び)。
  const [livingRestriction, setLivingRestriction] = React.useState('');
  const unlivable = livingRestriction === '生活できず避難';
  const [order, setOrder] = React.useState('both');
  // ㉚: 案件登録のあと attachment.add。失敗しても案件は残す。
  const [pendingFiles, setPendingFiles] = React.useState([]);
  const [submitting, setSubmitting] = React.useState(false);
  const [attachNote, setAttachNote] = React.useState('');
  const ORDER_LABEL = { survey: '調査のみ', work: '工事のみ', both: '調査＋工事' };
  // 受付-7(0057): 物件種別。仮の4値(池田さん確定待ち・docs/18 2-7)。
  const PROPERTY_KINDS = ['賃貸マンション', '分譲マンション', 'アパート', '戸建て', 'その他'];
  const [propKind, setPropKind] = React.useState('賃貸マンション');
  // K18-17: 受付チャネル。受信トレイの受信(msg.channel は phone/mail/slack/hp の内部キー)なら
  // その表示名(data.jsx の CHANNEL_LABEL。4つとも comm_channel にある)を初期値に、手入力なら「電話」。
  const [intakeChannel, setIntakeChannel] = React.useState(() => {
    const fromMsg = (typeof CHANNEL_LABEL !== 'undefined' && msg.channel) ? CHANNEL_LABEL[msg.channel] : '';
    return INTAKE_CHANNELS.includes(fromMsg) ? fromMsg : '電話';
  });
  const [issued, setIssued] = React.useState(null);
  // 依頼先・担当者は **DBのIDまで持つ**。
  // 名前だけだと書き込み層が取引先にも利用者にも紐づけられず、
  // 依頼先は備考へ落ち、担当者は丸ごと捨てられていた。
  // 名簿は seed が返す(MStore.masters)。読めていなければ空 ─ その場合は
  // 従来どおり自由入力に落ちるが、**紐づかないことを画面に出す**。
  const masters = (window.MStore && window.MStore.masters) ? window.MStore.masters() : { orgs: [], users: [] };
  // 手入力のときは差出人名('手入力')を依頼先に入れない ─ そのまま登録されると
  // 取引先が「手入力」という名前で備考に残る。
  const [client, setClient] = React.useState(msg.manual ? '' : (msg.matched || msg.from || ''));
  const [clientDbId, setClientDbId] = React.useState(null);
  // 受付-10(決-22): 依頼先の検索(名前・電話番号)とその場登録。
  // clientPhone は検索にも、その場登録(名前+電話)にも使う。
  const [clientPhone, setClientPhone] = React.useState('');
  // 検索結果に無かったとき「この名前と電話で登録」を選んだ印。true のときだけ
  // buildCreate が org.create(asRequester:true)を送る(app/write/mstore-diff.js)。
  const [clientRegisterNew, setClientRegisterNew] = React.useState(false);
  // 取引先の検索結果。null=まだ検索していない、[]=0件、[...]=候補(最大20件)。
  // 「重複登録を防ぐため検索結果を先に見せる」(決-22)ため、その場登録の
  // ボタンは partnerHits が確定する(null でなくなる)まで出さない。
  const [partnerHits, setPartnerHits] = React.useState(null);
  const clientDbIdRef = React.useRef(null);
  clientDbIdRef.current = clientDbId;
  const [partnerErr, setPartnerErr] = React.useState('');
  // 依頼先の入力(名前 or 電話)から取引先マスタを検索する。件数は20件+truncated。
  // window.MarurouCloud が無い(本番接続の設定が無い開発)ときは、seedが返す
  // masters.orgs(依頼元223件だけ・電話番号は持たない)から名前一致で絞り込む
  // ─ PartnersV6.jsx/Payment.jsx の pvFetch と同じ「無ければ空」の構え。
  React.useEffect(() => {
    if (msg.matched) return;   // 差出人と一致済みなら検索しない(既存の経路)
    const q = client.trim() || clientPhone.trim();
    if (!q) { setPartnerHits(null); setPartnerErr(''); return; }
    let alive = true;
    const t = setTimeout(() => {
      const cloud = window.MarurouCloud;
      const done = (rows, err) => {
        if (!alive) return;
        setPartnerHits(rows); setPartnerErr(err || '');
        // 名前が完全一致する候補がちょうど 1 件なら自動で選ぶ(旧UIの「名前一致で確定」と同じ振る舞い。
        // 電話番号だけの検索や部分一致では選ばない)。
        const needleName = client.trim().toLowerCase();
        if (needleName && !clientDbIdRef.current) {
          const exact = (rows || []).filter(r => (r.name || '').trim().toLowerCase() === needleName);
          if (exact.length === 1) pickPartner(exact[0]);
        }
      };
      if (cloud && typeof cloud.listPartners === 'function') {
        cloud.listPartners({ query: q, limit: 20 })
          .then(r => done((r && r.rows) || []))
          .catch(e => done([], (e && e.message) || '取引先を検索できませんでした'));
      } else {
        // オフライン代替: 名前だけの部分一致(大文字小文字を無視)。
        const needle = q.toLowerCase();
        done(masters.orgs.filter(o => o.name && o.name.toLowerCase().includes(needle)).slice(0, 20));
      }
    }, 250);
    return () => { alive = false; clearTimeout(t); };
  }, [client, clientPhone, msg.matched]);
  // K18-18: 選んだ候補の行(rpc_list_partners。0106 で firstResponseNotes ほか依頼者規定の 4 キーが載る)。
  // 初動対応注意点の帯はこの行を読むだけ。依頼先の入力を変えると clientDbId が外れるので帯も消える
  // (下の描画で picked.id と clientDbId が一致するときだけ渡す)。
  const [picked, setPicked] = React.useState(null);
  const pickPartner = (p) => {
    setClientDbId(p.id); setClient(p.name); setClientPhone(p.tel || '');
    setClientRegisterNew(false); setPicked(p);
  };
  const [owner, setOwner] = React.useState('');
  const [ownerDbId, setOwnerDbId] = React.useState(null);
  const [ownerAuto, setOwnerAuto] = React.useState(false);   // 担当固定で自動に入ったか
  // 選んだ取引先(規定を持つ)。名前だけの入力なら null。
  const selOrg = clientDbId ? (masters.orgs.find(o => o.id === clientDbId) || null) : null;
  // 担当固定(管理 › 取引先)。取引先を選んだとき担当が空なら自動で入れる。
  // 人が選び直した後は上書きしない ─ 自動は「空を埋める」だけ。
  React.useEffect(() => {
    if (!selOrg || !selOrg.defaultLeadUserId || ownerDbId) return;
    const hit = masters.users.find(u => u.id === selOrg.defaultLeadUserId);
    if (hit) { setOwnerDbId(hit.id); setOwner(hit.name); setOwnerAuto(true); }
  }, [clientDbId]);
  const [bldg, setBldg] = React.useState('');
  const [addr, setAddr] = React.useState('');
  const [rooms, setRooms] = React.useState([{ stance:'原因', room:'', detail:'' }, { stance:'被害', room:'', detail:'' }]);
  const [reportTo, setReportTo] = React.useState(() => (window.pvReportTo && !msg.manual ? window.pvReportTo(msg.matched || msg.from || '') : []));
  // 既定の報告先: 管理 › 取引先 に設定があればそれ。語彙は atoms.jsx の REPORT_ROLES(K-27・現場の言葉)。
  // 「依頼担当者」(旧「依頼者」も。既存データに残りうるので読み替える)は取引先名に置き換える(resolveReportTo)。
  // 無ければ従来の推定(pvReportTo)。
  const resolveRoles = window.resolveReportTo || ((l, c) => (Array.isArray(l) ? l : []).map(x => x === '依頼担当者' && c ? c : x));
  React.useEffect(() => {
    const d = selOrg && Array.isArray(selOrg.defaultReportTo) ? resolveRoles(selOrg.defaultReportTo, client) : [];
    if (d.length) setReportTo(d);
    else if (window.pvReportTo) setReportTo(window.pvReportTo(client));
  }, [client, clientDbId]);
  // 候補: 取引先名 → いま選ばれている報告先 → 候補の 5 語(「依頼担当者」は取引先名に置き換わるので重複しない)。
  // 以前あった「○○箇所 301 の持ち主」「保険会社」は候補から外した(現場の言葉に無い。docs/03 §3.1)。
  const reportCands = () => {
    const out = [];
    if(client) out.push(client);
    reportTo.forEach(x => out.push(x));
    resolveRoles(window.REPORT_ROLES || ['依頼担当者','居住者','オーナー','建物管理会社','上下階管理会社'], client).forEach(x=>out.push(x));
    return [...new Set(out)];
  };
  const setRoom = (i, patch) => setRooms(l => l.map((r,j)=> j===i ? { ...r, ...patch } : r));
  // 居住制限(livingRestriction)は住所と被害箇所の対象部屋がそろって初めて保存できる
  // (app/write/mstore-diff.js buildCreate / supabase/migrations/0015_write_ops.sql)。
  // 選んだのに保存できないときは、黙って捨てず画面に出す(下の対象部屋のセクション)。
  const hasAddr = !!addr.trim();
  const hasDamageSpot = rooms.some(r => (r.stance === '被害' || r.stance === '両方') && (r.room.trim() || r.detail.trim()));
  // 聞き取りメモ。受信した本文を初期値にして、受付が電話中に足した分を上書きする。
  // ここは defaultValue の textarea で、**打った内容が一度も読まれていなかった**。
  // 登録される「初期報」は常に受信本文のままで、聞き取りが丸ごと消えていた。
  const [memo, setMemo] = React.useState(msg.body || '');
  const initial = memo.trim() || msg.body || msg.summary || '受電の聞き取り';
  const submit = async () => {
    if (submitting) return;
    setSubmitting(true);
    setAttachNote('');
    const cause = rooms.find(r=>r.stance==='原因'), dmg = rooms.find(r=>r.stance==='被害');
    const room = (dmg && dmg.room) || (cause && cause.room) || (rooms.find(r=>r.room)||{}).room || '';
    const name = (bldg || '新規物件') + room;
    const payload = {
      // 受付日は**今日**。'2026/06/16' が焼き込まれていたため、
      // いつ受け付けても同じ日付で登録されていた。
      name, type: propKind.startsWith('分譲') ? '分譲' : (propKind.startsWith('戸建') ? '戸建' : '賃貸'),
      propKind, order:ORDER_LABEL[order], accepted:msToday(),
      // K18-17: 受付チャネル(cases.intake_channel)。mstore-diff.js の INTAKE_MAP が翻訳する。
      intakeChannel,
      owner:owner || '', ownerDbId, client, clientDbId, clientPhone: clientPhone.trim() || '',
      clientRegisterNew, addr, bldg, initial, mode,
      // 居住制限(K18-14・0107。3 値: 制限なし/一部制限あり/生活できず避難。仕様 04 v1.24:265)。
      // 「生活できず避難」= 住めない → 進め方 P1(先行工事)の判定値(骨子 10-6・決-40)。進め方が空の保険精算の
      // 復旧工事には DB が P1 を入れ、依頼者の規定などで入っている進め方は変えない(K18-16・0111)。住所・被害箇所の対象部屋があるときだけ
      // 被害/両方の部屋に保存される(app/write/mstore-diff.js buildCreate)。無ければ画面にも出す
      // (下の対象部屋のセクションと、保存後に出る警告バナーの両方)。未選択('')は送っても保存しない。
      // **ここで1回だけ載せる**(管-5 の前、同じ値を下で別の形に上書きして黙って消えていた経緯がある)。
      livingRestriction,
      spots:rooms.filter(r=>r.room||r.detail).map(r=>({ kind:r.stance, room:r.room||'—', detail:r.detail||'未特定（調査前）', owner:client||'—', isClient:r.stance==='被害' })),
      surveyTasks:[{ name:'応急止水', state:urgent==='rush'?'done':'todo', date:urgent==='rush'?msTodayMd():null, memo:urgent==='rush'?'駆けつけで止水':'' },
        { name:'原因特定', state:'todo', date:null, memo:'' }, { name:'被害対応', state:'todo', date:null, memo:'' }],
      todos:[{ id:'TDN'+Date.now(), step:'現地調査 日程調整', ball:'自社', category:'自社', btype:'連絡', marker:'on', sev:null,
        // 期限も固定値('06/17')だった。受け付けた翌日にする。
        urgent:urgent==='rush', watch:false, due:msTomorrowMd(), dunning:null, trkId:null, railStep:null, done:false }],
      dueDone:'—',
      reportTo: reportTo.length ? reportTo : (window.pvReportTo ? window.pvReportTo(client) : (client ? [client] : [])),
    };
    // 案件登録を先に完了させる。写真の失敗で案件を消さない(㉚)。
    const id = onCreate ? onCreate(payload) : null;
    setIssued(id || 'CASE-NEW');

    if (id && pendingFiles.length) {
      const c = window.MStore && window.MStore.get().cases.find(x => x.id === id);
      const caseDbId = c && c.dbId;
      const cloud = window.MarurouCloud;
      if (cloud && cloud.uploadAttachment && caseDbId && window.MStore.addAttachment) {
        let ok = 0, fail = 0;
        for (const file of pendingFiles) {
          try {
            const meta = await cloud.uploadAttachment(file, { caseId: caseDbId });
            window.MStore.addAttachment(id, { ...meta, kind: intakeGuessKind(file) });
            ok += 1;
          } catch (err) {
            fail += 1;
            console.warn('[受付] 写真の添付に失敗しました。案件は残しています。', err);
          }
        }
        const note = fail === 0
          ? (ok + '件の写真を添付しました')
          : (ok > 0
            ? (ok + '件を添付、' + fail + '件は失敗しました（案件は登録済み。案件詳細のファイルから付け直せます）')
            : '写真の添付に失敗しました（案件は登録済み。案件詳細のファイルから付け直せます）');
        setAttachNote(note);
        onToast && onToast(note);
      } else {
        const note = '案件は登録しました。写真のアップロードには接続が必要です（案件詳細のファイルから付けられます）';
        setAttachNote(note);
        onToast && onToast(note);
      }
    }
    setPendingFiles([]);
    setSubmitting(false);
  };
  const mode = urgent === 'rush' ? '即時駆けつけ' : '日程調整';

  if (issued) {
    return (
      <div style={{ padding: 24, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', textAlign: 'center', gap: 4 }}>
        <div style={{ width: 40, height: 40, borderRadius: 999, background: 'var(--success-muted)', color: 'var(--success)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 20, fontWeight: 700, border: '1px solid var(--success-border)', marginBottom: 8 }}>✓</div>
        <div style={{ fontSize: 'var(--text-display-sm)', fontWeight: 700 }}>案件を登録しました</div>
        <div className="code" style={{ fontSize: 'var(--text-body-lg)', color: 'var(--brand)', fontWeight: 600 }}>{issued}</div>
        <div style={{ fontSize: 'var(--text-body-sm)', color: 'var(--muted-foreground)', maxWidth: 320, marginTop: 4 }}>
          {/* 起きていないことを言わない。通知も SLA 監視もまだ無い(モック時代の文言を削除)。
              連携が入ったら、ここは実際に送った結果で書く。 */}
          対応モード「{mode}」・依頼内容「{ORDER_LABEL[order]}」で登録しました。担当者への連絡は別途行ってください。
        </div>
        {attachNote && (
          <div style={{ fontSize: 'var(--text-body-sm)', color: 'var(--muted-foreground)', maxWidth: 360, marginTop: 8, lineHeight: 1.6 }}>{attachNote}</div>
        )}
        <div style={{ display: 'flex', gap: 8, marginTop: 16 }}>
          <AfButton onClick={onCancel}>受信トレイへ</AfButton>
          <AfButton variant="primary" onClick={() => onDone && onDone(issued)}>案件を開く</AfButton>
        </div>
      </div>
    );
  }

  return (
    <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
      {/* step indicator(K18-47: 段の名前が長くなったので、狭い画面では 2 段に折り返す。index.html の .mr-intake-steps) */}
      <div className="mr-intake-steps" style={{ display: 'flex', gap: 0, padding: '12px 16px', borderBottom: '1px solid var(--border)' }}>
        {INTAKE_STEPS.map((s, i) => (
          <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 6, flex: i < INTAKE_STEPS.length - 1 ? 1 : 'none' }}>
            <span style={{ width: 20, height: 20, borderRadius: 999, flexShrink: 0, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: 'var(--text-caption)', fontWeight: 700,
              background: i < step ? 'var(--brand)' : i === step ? 'var(--brand)' : 'var(--card)', color: i <= step ? '#fff' : 'var(--muted-foreground)', border: '1px solid ' + (i <= step ? 'var(--brand)' : 'var(--border-strong)') }}>{i < step ? '✓' : i + 1}</span>
            <span style={{ fontSize: 'var(--text-body-sm)', fontWeight: 600, color: i <= step ? 'var(--foreground)' : 'var(--muted-foreground)', whiteSpace: 'nowrap' }}>{s}</span>
            {i < INTAKE_STEPS.length - 1 && <span className="mr-intake-step-line" style={{ flex: 1, height: 1, background: i < step ? 'var(--brand)' : 'var(--border)', margin: '0 8px' }} />}
          </div>
        ))}
      </div>

      <div style={{ flex: 1, overflowY: 'auto', padding: 16 }}>
        {/* K18-18: 依頼先を選んだら、その取引先の 初動対応注意点 を出す(仕様 04:131「案件登録時に表示」)。
            どの手順に進んでも見えるよう、手順の中身より上に 1 か所だけ置く。値が無ければ何も出さない。 */}
        <ClientRulesBand where="intake" fields={['firstResponseNotes']}
          rules={picked && clientDbId && picked.id === clientDbId ? picked : null} style={{ marginBottom: 14 }} />
        {step === 0 && (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
              <ChannelGlyph ch={msg.channel} />
              <div>
                <div style={{ fontSize: 'var(--text-body-md)', fontWeight: 600 }}>{msg.from}</div>
                <div style={{ fontSize: 'var(--text-caption)', color: 'var(--muted-foreground)' }} className="mono">{CHANNEL_LABEL[msg.channel]} · {msg.meta} · {msg.at}</div>
              </div>
            </div>
            <AfCard pad={12} style={{ background: 'var(--accent)' }}>
              <div style={{ fontSize: 'var(--text-body-md)', lineHeight: 1.7 }}>{msg.body || '手入力の受付です。下の聞き取りメモに内容を書いてください。'}</div>
            </AfCard>
            <div>
              {/* K18-17: 受付チャネル(cases.intake_channel)。何で受けたか。既定は電話(受付はほぼ受電)。
                  語彙は DB の comm_channel(INTAKE_CHANNELS)。保存先は case.create の intake_channel。 */}
              <FieldLabel hint="何で受けたか。案件の受付チャネルとして保存されます">受付チャネル</FieldLabel>
              <select aria-label="受付チャネル" value={intakeChannel} onChange={e => setIntakeChannel(e.target.value)}
                style={{ width: '100%', height: 36, padding: '0 10px', borderRadius: 'var(--radius-md)',
                  border: '1px solid var(--input)', fontSize: 'var(--text-body-md)', fontFamily: 'var(--font-sans)',
                  color: 'var(--foreground)', background: 'var(--card)' }}>
                {INTAKE_CHANNELS.map(k => <option key={k} value={k}>{k}</option>)}
              </select>
            </div>
            <div>
              <FieldLabel hint="名前・電話番号で検索。重複登録を防ぐため検索結果を先に確認してください">依頼先</FieldLabel>
              {msg.matched
                ? <AfAlert variant="success" title="受信の差出人と一致">{msg.matched} を依頼者として入れました。規定があれば下の方針に出ます。</AfAlert>
                : <div style={{ display:'flex', flexDirection:'column', gap:6 }}>
                    {/* 受付-10(決-22): 取引先マスタの検索(名前・電話番号)。
                        打ち間違いがマスタに増えないよう、候補を選ぶか
                        「この名前と電話で登録」で明示的にその場登録する。 */}
                    <div style={{ display:'flex', gap:8, flexWrap:'wrap' }}>
                      <AfInput value={client}
                        onChange={e=>{ setClient(e.target.value); setClientDbId(null); setClientRegisterNew(false); }}
                        placeholder="取引先名で検索" style={{ flex:'1 1 200px' }} />
                      <AfInput value={clientPhone}
                        onChange={e=>{ setClientPhone(e.target.value); setClientDbId(null); setClientRegisterNew(false); }}
                        placeholder="電話番号で検索" style={{ flex:'1 1 160px' }} />
                    </div>

                    {partnerHits && partnerHits.length > 0 && !clientDbId && !clientRegisterNew && (
                      <div style={{ display:'flex', flexDirection:'column', gap:2, border:'1px solid var(--border)', borderRadius:'var(--radius-md)', padding:4, maxHeight:180, overflowY:'auto' }}>
                        <div style={{ fontSize:'var(--text-caption)', color:'var(--muted-foreground)', padding:'2px 6px' }}>候補 {partnerHits.length}件</div>
                        {partnerHits.map(p => (
                          <button key={p.id} onClick={()=>pickPartner(p)} style={{
                            all:'unset', cursor:'pointer', padding:'6px 8px', borderRadius:'var(--radius-sm)',
                            display:'flex', justifyContent:'space-between', gap:8, fontFamily:'var(--font-sans)' }}
                            onMouseEnter={e=>e.currentTarget.style.background='var(--accent)'}
                            onMouseLeave={e=>e.currentTarget.style.background='transparent'}>
                            <span style={{ fontWeight:600, fontSize:'var(--text-body-sm)' }}>{p.name}</span>
                            <span className="mono" style={{ color:'var(--muted-foreground)', fontSize:'var(--text-body-sm)' }}>{p.tel || '電話番号なし'}</span>
                          </button>
                        ))}
                      </div>
                    )}

                    {clientDbId
                      ? <AfAlert variant="success" title="取引先マスタと一致">{client} として登録します。</AfAlert>
                      : clientRegisterNew
                        ? <AfAlert variant="success" title="新規取引先として登録">「{client}」(電話: {clientPhone.trim() || '未入力'})を新しい取引先として登録します。</AfAlert>
                        : partnerErr
                          ? <AfAlert variant="notice" title="検索できませんでした">{partnerErr}</AfAlert>
                          : partnerHits === null
                            ? <AfAlert variant="notice" title="依頼先">名前または電話番号を入力すると取引先マスタを検索します。</AfAlert>
                            : <div style={{ display:'flex', flexDirection:'column', gap:6 }}>
                                <AfAlert variant="notice" title={partnerHits.length ? '候補に無ければ登録できます' : 'マスタに見つかりません'}>
                                  {partnerHits.length
                                    ? '一致しなければ、名前と電話番号を確定させたうえでその場登録できます。'
                                    : `「${client || clientPhone}」に一致する取引先はありませんでした。`}
                                </AfAlert>
                                {client.trim() && clientPhone.trim()
                                  ? <AfButton onClick={()=>setClientRegisterNew(true)}>この名前と電話で登録</AfButton>
                                  : <div style={{ fontSize:'var(--text-body-sm)', color:'var(--muted-foreground)' }}>
                                      その場登録には名前と電話番号の両方が必要です。
                                    </div>}
                              </div>}
                  </div>}
            </div>
            <div>
              <FieldLabel hint="案件の「初期報」として保存されます">聞き取りメモ</FieldLabel>
              <textarea value={memo} onChange={e=>setMemo(e.target.value)} style={{ width: '100%', minHeight: 60, padding: 8, borderRadius: 'var(--radius-md)', border: '1px solid var(--input)', fontSize: 'var(--text-body-md)', fontFamily: 'var(--font-sans)', resize: 'vertical', outline: 'none', color: 'var(--foreground)', background: 'var(--card)' }} />
            </div>
          </div>
        )}

        {step === 1 && (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
            <div>
              <FieldLabel hint="仮の一覧です。語彙が決まれば差し替えます">物件種別</FieldLabel>
              <select aria-label="物件種別" value={propKind} onChange={e => setPropKind(e.target.value)}
                style={{ width: '100%', height: 36, padding: '0 10px', borderRadius: 'var(--radius-md)',
                  border: '1px solid var(--input)', fontSize: 'var(--text-body-md)', fontFamily: 'var(--font-sans)',
                  color: 'var(--foreground)', background: 'var(--card)', marginBottom: 12 }}>
                {PROPERTY_KINDS.map(k => <option key={k} value={k}>{k}</option>)}
              </select>
              <FieldLabel hint="受注の範囲。工事の進行は登録時には作りません">依頼内容</FieldLabel>
              <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
                <Radio checked={order === 'both'} onClick={() => setOrder('both')} label="調査＋工事" sub="原因調査から復旧まで受ける。工事の進行はあとから案件詳細で足します。" />
                <Radio checked={order === 'survey'} onClick={() => setOrder('survey')} label="調査のみ" sub="原因特定・報告まで。工事は別途発注（他社施工の可能性あり）。" />
                <Radio checked={order === 'work'} onClick={() => setOrder('work')} label="工事のみ" sub="原因が既に特定済み。見積から開始。" />
              </div>
            </div>
            <div>
              <FieldLabel hint="顧客の要望（駆けつけ希望か）">緊急度</FieldLabel>
              <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
                <Radio checked={urgent === 'rush'} onClick={() => setUrgent('rush')} label="最短駆けつけ希望" sub="先に業者を手配し到着時間を確定 → 関係者へ連絡。" />
                <Radio checked={urgent === 'normal'} onClick={() => setUrgent('normal')} label="通常（日程調整）" sub="日程を調整して対応。" />
              </div>
            </div>
            <div data-intake-living={livingRestriction}>
              <FieldLabel hint="被害側の部屋。未選択なら保存しません">居住制限</FieldLabel>
              <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
                <Radio checked={livingRestriction === '制限なし'} onClick={() => setLivingRestriction('制限なし')} label="制限なし" sub="通常どおり生活できる。進め方は通常（依頼者規定があればその初期値）。" />
                <Radio checked={livingRestriction === '一部制限あり'} onClick={() => setLivingRestriction('一部制限あり')} label="一部制限あり" sub="お湯・水回りが一部使えないなど。細部は重要事項・連絡履歴へ。" />
                <Radio checked={unlivable} onClick={() => setLivingRestriction('生活できず避難')}
                  label={<span style={{ color: 'var(--destructive)' }}>生活できず避難</span>}
                  sub="住めない。進め方は P1（先行工事）が目安です（進め方が空の保険精算の復旧工事には自動で P1。規定などで入っている工事は登録後に担当が決めます）。" />
                {livingRestriction && (
                  <button onClick={() => setLivingRestriction('')}
                    style={{ all: 'unset', cursor: 'pointer', fontSize: 'var(--text-body-sm)', color: 'var(--brand)', fontWeight: 600, padding: '2px 0' }}>選択を外す（未確認）</button>
                )}
              </div>
            </div>
            {unlivable && <AfAlert variant="error" title="進め方は P1（先行工事）が目安です">生活できず避難（住めない）として被害箇所の部屋に保存します。進め方が空の保険精算の復旧工事には P1（先行工事）が自動で入ります。依頼者の規定などで進め方が入っている工事は自動では切り替えず、案件詳細に「P1 が目安」と出します（切り替えるかは担当が決めます）。</AfAlert>}
          </div>
        )}

        {step === 2 && (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
            <AfCard pad={12}>
              <div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
                <InfoLine label="受付チャネル" value={intakeChannel} />
                <InfoLine label="物件種別" value={propKind} />
                <InfoLine label="依頼内容" value={ORDER_LABEL[order]} strong />
                <InfoLine label="対応モード" value={mode} />
                <InfoLine label="緊急度" value={urgent === 'rush' ? '最短駆けつけ希望' : '通常'} />
                <InfoLine label="居住制限" value={livingRestriction || '未確認'} />
                <InfoLine label="進め方（初期値）" value={unlivable ? 'P1（先行工事）が目安（生活できず避難・空の工事は自動で P1、規定などで入っている工事は担当が決める）' : selOrg && selOrg.defaultApproach ? afApproachLabel(selOrg.defaultApproach) + '（依頼者規定）' : '未設定（案件で決める）'} />
                <InfoLine label="対応の急ぎ" value={urgent === 'rush' ? '最短駆けつけ' : '通常'} last />
              </div>
            </AfCard>
            <div>
              <FieldLabel hint="担当固定があれば空のときだけ入ります">本担当</FieldLabel>
              {/* 担当者は名簿から選ぶ。名前だけだとDBの利用者に紐づけられず、
                  以前は「担当者は名前しか持っておらず、まだ紐づけられません」として
                  **丸ごと捨てられていた**(誰の案件か分からないまま登録されていた)。 */}
              <select value={ownerDbId || ''}
                onChange={e=>{ const id = e.target.value || null; setOwnerDbId(id); setOwnerAuto(false);
                  const hit = masters.users.find(u => u.id === id); setOwner(hit ? hit.name : ''); }}
                style={{ width:'100%', height:28, padding:'0 8px', borderRadius:'var(--radius-md)',
                  borderWidth:1, borderStyle:'solid', borderColor:'var(--input)',
                  fontSize:'var(--text-body-md)', fontFamily:'var(--font-sans)',
                  color:'var(--foreground)', background:'var(--card)' }}>
                <option value="">担当者を選ぶ（あとで決める）</option>
                {masters.users.map(u => <option key={u.id} value={u.id}>{u.name}</option>)}
              </select>
              {!masters.users.length && (
                <div style={{ fontSize:'var(--text-body-sm)', color:'var(--muted-foreground)', marginTop:4 }}>
                  担当者の名簿を読み込めていません。あとで案件詳細から設定してください。
                </div>
              )}
              {ownerAuto && ownerDbId && (
                <div style={{ fontSize:'var(--text-body-sm)', color:'var(--muted-foreground)', marginTop:4 }}>
                  {selOrg ? selOrg.name : '取引先'} の担当固定から自動で入りました（変更できます）。
                </div>
              )}
              <div style={{ marginTop: 10 }}>
                <FieldLabel>報告先（報告書・見積の共有先。ここで決めた人数まで共有が完了しません）</FieldLabel>
                <div style={{ display: 'flex', gap: 5, flexWrap: 'wrap' }}>
                  {reportCands().map(n=>{
                    const on = reportTo.indexOf(n)>=0;
                    return (
                      <button key={n} onClick={()=>setReportTo(l=>on?l.filter(x=>x!==n):[...l,n])}
                        style={{ all:'unset', cursor:'pointer', fontFamily:'var(--font-sans)', display:'inline-flex', alignItems:'center', gap:4,
                          fontSize:'var(--text-body-sm)', fontWeight:on?700:500, padding:'4px 11px', borderRadius:'var(--radius-full)',
                          border:'1px solid '+(on?'var(--brand)':'var(--border)'), background:on?'var(--brand-muted)':'transparent',
                          color:on?'var(--brand)':'var(--muted-foreground)' }}>
                        {on && <span style={{ fontSize:9 }}>✓</span>}{n}
                      </button>
                    );
                  })}
                </div>
                <div style={{ fontSize:'var(--text-caption)', color:'var(--foreground-subtle)', marginTop:4 }}>
                  取引先マスタの既定値が入っています{client?'（'+client+'）':''}。ここで変更でき、案件詳細でも変更できます。
                </div>
              </div>
            </div>
            {urgent === 'rush' && <AfAlert variant="notice" title="初動">先に業者を手配して到着時間を確定し、その時間を関係者へ連絡（駆けつけ希望の初動順序）。</AfAlert>}
          </div>
        )}

        {step === 3 && (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
              <div><FieldLabel>建物名</FieldLabel><AfInput value={bldg} onChange={e=>setBldg(e.target.value)} placeholder="建物名" style={{ width: '100%' }} /></div>
              <div><FieldLabel>住所</FieldLabel><AfInput value={addr} onChange={e=>setAddr(e.target.value)} placeholder="江東区..." style={{ width: '100%' }} /></div>
            </div>
            <div>
              <FieldLabel hint="システムが裏で建物・部屋・案件対象部屋に振り分け">対象部屋（立場）</FieldLabel>
              <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
                {rooms.map((r,i)=><RoomLine key={i} value={r} onChange={patch=>setRoom(i,patch)} />)}
                <button onClick={()=>setRooms(l=>[...l,{ stance:'被害', room:'', detail:'' }])}
                  style={{ all: 'unset', cursor: 'pointer', fontSize: 'var(--text-body-sm)', color: 'var(--brand)', fontWeight: 600, padding: '4px 0' }}>＋ 対象部屋を追加</button>
              </div>
              {livingRestriction && !(hasAddr && hasDamageSpot) && (
                <AfAlert variant="notice" title="居住制限はまだ保存できません">
                  {!hasAddr
                    ? '住所が未入力です。居住制限（' + livingRestriction + '）は、住所と被害箇所の対象部屋がそろわないと保存されません。'
                    : '被害箇所の対象部屋（部屋番号または箇所）が未入力です。このままでは居住制限（' + livingRestriction + '）は保存されません。'}
                </AfAlert>
              )}
            </div>
            <AfCard pad={12} style={{ background: 'var(--accent)' }}>
              <FieldLabel>依頼者規定（管理 › 取引先 の設定から）</FieldLabel>
              {/* 以前は「保険申請 / タイプ② P2 / 月次窓口 / 自社」が固定文字で出ていた。
                  いまは選んだ取引先の規定(seed の orgs)をそのまま出し、無ければ無いと言う。 */}
              {!selOrg
                ? <div style={{ fontSize:'var(--text-body-sm)', color:'var(--muted-foreground)' }}>取引先マスタから依頼先を選ぶと、その取引先の規定がここに入ります。</div>
                : !(selOrg.defaultSettlementSurvey || selOrg.defaultSettlementRepair || selOrg.defaultApproach
                    || selOrg.defaultBillingPattern || selOrg.insuranceWindow || selOrg.ownerWindow || selOrg.defaultCommissionRate != null)
                  ? <div style={{ fontSize:'var(--text-body-sm)', color:'var(--muted-foreground)' }}>{selOrg.name} の依頼者規定はまだ設定されていません。管理者が「管理 › 取引先」で設定できます。</div>
                  : <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
                      {selOrg.defaultSettlementSurvey && <AfChip tone="brand">規定精算（調査）：{selOrg.defaultSettlementSurvey}</AfChip>}
                      {selOrg.defaultSettlementRepair && <AfChip tone="brand">規定精算（復旧）：{selOrg.defaultSettlementRepair}</AfChip>}
                      <AfChip tone="brand">進め方：{unlivable ? 'P1（先行工事・住めない切替）' : (afApproachLabel(selOrg.defaultApproach) || '未設定')}</AfChip>
                      {selOrg.defaultBillingPattern && <AfChip tone="neutral">請求パターン：{selOrg.defaultBillingPattern}</AfChip>}
                      {selOrg.insuranceWindow && <AfChip tone="neutral">保険窓口：{selOrg.insuranceWindow}</AfChip>}
                      {selOrg.ownerWindow && <AfChip tone="neutral">オーナー窓口：{selOrg.ownerWindow}</AfChip>}
                      {selOrg.defaultCommissionRate != null && <AfChip tone="neutral">手数料：{Math.round(Number(selOrg.defaultCommissionRate) * 1000) / 10}%</AfChip>}
                    </div>}
            </AfCard>
            <div>
              <FieldLabel hint="任意。案件登録のあとで添付します（失敗しても案件は残ります）">現場写真・資料</FieldLabel>
              {(() => {
                const FileDrop = window.FileDrop;
                if (FileDrop) return <FileDrop onFiles={list => setPendingFiles(prev => [...prev, ...list])} />;
                return (
                  <input type="file" multiple accept="image/*,application/pdf"
                    onChange={e => { const arr = [...(e.target.files || [])]; if (arr.length) setPendingFiles(prev => [...prev, ...arr]); e.target.value = ''; }} />
                );
              })()}
              {pendingFiles.length > 0 && (
                <div style={{ marginTop: 8, display: 'flex', flexDirection: 'column', gap: 4 }}>
                  {pendingFiles.map((f, i) => (
                    <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 'var(--text-body-sm)' }}>
                      <span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.name}</span>
                      <button type="button" onClick={() => setPendingFiles(prev => prev.filter((_, j) => j !== i))}
                        style={{ all: 'unset', cursor: 'pointer', color: 'var(--muted-foreground)', fontSize: 'var(--text-caption)', fontWeight: 600 }}>外す</button>
                    </div>
                  ))}
                </div>
              )}
            </div>
          </div>
        )}
      </div>

      <div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '12px 16px', borderTop: '1px solid var(--border)' }}>
        <AfButton variant="ghost" onClick={onCancel} disabled={submitting}>キャンセル</AfButton>
        <div style={{ flex: 1 }} />
        {step > 0 && <AfButton onClick={() => setStep(step - 1)} disabled={submitting}>戻る</AfButton>}
        {step < 3
          ? <AfButton variant="primary" onClick={() => setStep(step + 1)}>次へ</AfButton>
          : <AfButton variant="primary" disabled={submitting} onClick={submit}>{submitting ? '登録中…' : '案件登録'}</AfButton>}
      </div>
    </div>
  );
}

function InfoLine({ label, value, strong, last }) {
  return <div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, padding: '7px 0', borderBottom: last ? 'none' : '1px solid var(--border)', fontSize: 'var(--text-body-md)' }}>
    <span style={{ color: 'var(--muted-foreground)' }}>{label}</span>
    <span style={{ fontWeight: strong ? 700 : 500, color: strong ? 'var(--brand)' : 'var(--foreground)' }}>{value}</span>
  </div>;
}

function RoomLine({ value, onChange }) {
  return <div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap:'wrap' }}>
    <AfInput value={value.room} onChange={e=>onChange({ room:e.target.value })} placeholder="部屋番号（戸建は空）" style={{ width: 120 }} />
    <AfInput value={value.detail} onChange={e=>onChange({ detail:e.target.value })} placeholder="箇所（例：廊下の天井）" style={{ width: 170 }} />
    <div style={{ display: 'flex', gap: 6 }}>
      {['原因', '被害', '両方'].map(s => (
        <button key={s} onClick={()=>onChange({ stance:s })} style={{ all:'unset', cursor:'pointer', fontFamily:'var(--font-sans)',
          fontSize:'var(--text-body-sm)', fontWeight:value.stance===s?700:500, padding:'3px 10px', borderRadius:'var(--radius-full)',
          border:'1px solid '+(value.stance===s?'var(--brand)':'var(--border)'), background:value.stance===s?'var(--brand-muted)':'transparent',
          color:value.stance===s?'var(--brand)':'var(--muted-foreground)' }}>{s}</button>
      ))}
    </div>
  </div>;
}

// 受信トレイに出すもの。
//
// data.jsx の INBOX には架空の4件(「アーク管理 田中」「豊洲の物件で下階に水漏れ」など)
// が焼き込まれていた。実在しない相手・実在しない電話番号で、30人が毎日ここから
// 起票する画面なので、**そのまま出すと架空の相手に連絡してしまう**。
// 起動時に実データを読めなかったときに赤い画面で止めるのと同じ理由で、ここも出さない。
//
// 4チャネル(電話・メール・Slack・HP問合せ)の取り込みは未実装で、DBにも元が無い
// (communications 47,815件はすべて案件に紐付いており、未処理の受信は0件)。
// 取り込みが入るまでは空にして、手入力の受付だけを残す。
const INTAKE_MANUAL = {
  id: 'manual', channel: 'phone', from: '手入力', meta: '電話・口頭など', at: '',
  urgent: false, matched: null, body: '', manual: true,
};
const INTAKE_INBOX = [];   // 取り込みが入ったらここに実データを渡す

function Intake({ onDone, onToast, onCreate }) {
  const list = [...INTAKE_INBOX, INTAKE_MANUAL];
  const [sel, setSel] = React.useState(list[0].id);
  const msg = list.find(m => m.id === sel) || INTAKE_MANUAL;
  // K18-47: 左にウィザード・右に受信トレイ(池田さんの画面編集版の並び)。受信トレイの枠と空の状態は
  // IntakeInbox.jsx(index.html でこのファイルより前に読む)。取り込みが入るまで INTAKE_INBOX は空で、
  // 「手入力」の 1 行だけが並ぶ。
  return (
    <div className="mr-intake-shell" style={{ display: 'flex', flex: 1, minWidth: 0, height: '100%', minHeight: 0 }}>
      {/* wizard */}
      <div style={{ flex: 1, minWidth: 0, background: 'var(--card)' }}>
        <IntakeWizard key={sel} msg={msg} onCreate={onCreate} onToast={onToast}
          onCancel={() => onToast && onToast('受信トレイに戻りました')} onDone={id => onDone && onDone(id)} />
      </div>
      {/* inbox */}
      <IntakeInbox list={list} inboxCount={INTAKE_INBOX.length} sel={sel} onSelect={setSel} Glyph={ChannelGlyph} />
    </div>
  );
}

Object.assign(window, { Intake });
