/* ============================================================
   カレンダー（月 / 週 / 日、担当者別の色分け）
   ============================================================ */
function buildEvents(cases, meetings, sources = window.APP_DATA) {
  const D = window.APP_DATA;
  const evs = [];
  // 担当未設定は「担当なし」のまま表示する＝先頭ユーザーの仮表示(旧ow0)は誤解を生むため廃止（2026-07-10）
  const seen = new Set();
  const dayOf = (dt) => (dt || '').slice(0, 10);
  cases.forEach(c => {
    if (c.archived) return; // アーカイブ案件（成件後の継続会議など）はカレンダーに出さない
    const owner = c.ownerId ? D.user(c.ownerId) : null;
    const subs = (c.subIds || []).map(id => D.user(id)).filter(u => u && (!owner || u.id !== owner.id));
    // 次回商談とRC商談日が両方未来のときは casePlanPick が正を一つに絞る（二重表示防止）
    const planPick = casePlanPick(c);
    // 次回商談（予定）※電話予定はカレンダーに出さない（電話はToDo＝次の一手で管理する方針・2026-07-08）
    if (c.nextMeeting && planPick.nm && caseNextMethod(c) !== 'phone') {
      evs.push({ id: 'ev-' + c.id, caseId: c.id, datetime: c.nextMeeting, title: c.title, customer: D.customer(c.customerId), owner, subs, status: c.status, kind: 'next', method: caseNextMethod(c) });
      seen.add(c.id + dayOf(c.nextMeeting));
    }
    // 会議記録（1回目・2回目…の商談）— 二次会議が漏れないように。同日の商談予定より優先（動画・出席者つき）
    (c.meetingDocs || []).forEach(doc => {
      if (!doc.datetime || seen.has(c.id + dayOf(doc.datetime))) return;
      evs.push({ id: 'md-' + doc.id, caseId: c.id, datetime: doc.datetime, title: c.title, customer: D.customer(c.customerId), owner, subs, status: c.status, kind: 'doc', doc });
      seen.add(c.id + dayOf(doc.datetime));
    });
    // ReadyCrew の商談日（appointAt）— 過去分は履歴として表示、未来分は casePlanPick の正のときだけ ※電話予定は出さない
    if (c.appointAt && planPick.ap && caseNextMethod(c) !== 'phone' && !seen.has(c.id + dayOf(c.appointAt))) {
      evs.push({ id: 'ap-' + c.id, caseId: c.id, datetime: c.appointAt, title: c.title, customer: D.customer(c.customerId), owner, subs, status: c.status, kind: 'appoint', method: caseNextMethod(c) });
      seen.add(c.id + dayOf(c.appointAt));
    }
  });
  // 商談記録（実施済み）も日付に表示。
  // ただし電話・メールのメモは対象外：記入時刻がそのまま datetime になる「架電/連絡ログ」であり、
  // 商談予定と誤読される＋同一担当の重複警告の誤報源になる（履歴は案件詳細の商談履歴で見られる）
  (meetings || []).forEach(m => {
    if (m.method === 'phone' || m.method === 'email') return;
    const c = D.caseById(m.caseId);
    if (!c || c.archived || !m.datetime) return;
    if (seen.has(c.id + dayOf(m.datetime))) return; // 重複回避
    seen.add(c.id + dayOf(m.datetime));
    evs.push({ id: 'mev-' + m.id, caseId: m.caseId, datetime: m.datetime, title: c.title, customer: D.customer(c.customerId), owner: m.authorId ? D.user(m.authorId) : null, status: c.status, kind: 'done' });
  });
  // Google カレンダー（ICS同期）— 参加者・出欠・Meetリンク付き。会社名が一致すれば案件に紐づけ
  const normG = (s) => (s || '').replace(/株式会社|（株）|\(株\)|・(レディクル|発注ナビ).*$|[（(].*$|\s+/g, '');
  const gcalDayTitles = new Set(); // 同日・同社の Fireflies 標準イベントとの重複を避ける
  // 明らかな社内ノイズ（社内◯◯・リマインダー等）は案件カレンダーに一切出さない（未紐付けとしても出さない）。
  // 「社内」を含むタイトルが顧客名にファジー一致→実在メンバーが担当として誤表示される事故もここで根治（2026-07-08）
  const gNoise = (t) => /社内|\[提醒\]|【提醒】|リマインダ|リマインド|reminder/i.test(t || '');
  (sources.gcalEvents || []).forEach(g => {
    if (!g.datetime) return;
    if (gNoise(g.title)) return;
    let matched = cases.find(c => c.id === g.caseId) || null;
    for (const c of cases) {
      if (matched) break;
      const cust = D.customer(c.customerId);
      if (!cust) continue;
      const core = normG(cust.company).toLowerCase(); // 大文字小文字を無視（例: JPASS定例会 → JPass 案件）
      const gt = normG(g.title).toLowerCase();
      if ((core.length >= 3 && gt.includes(core)) || (core.length >= 2 && gt === core)) { matched = c; break; }
    }
    // 案件に紐づかないGoogle予定も「未紐付け」として表示（カレンダーにはあるのに案件に無い＝取りこぼしを見える化。
    // 2026-07-07：以前は完全に非表示だったが、ポップアップから「案件を作成/紐付け」できる導線とセットで表示に変更。
    // ノイズが気になる場合はツールバーの「未紐付け」トグルで非表示にできる）
    if (matched && matched.archived) return; // アーカイブ案件の予定（継続会議など）はカレンダーに出さない
    const key = (matched ? matched.id : 'gc' + g.id) + dayOf(g.datetime);
    if (seen.has(key)) {
      // 同日の案件イベントが既にある → Google の参加者・出欠・Meetリンクをマージ
      const ex = evs.find(e => e.caseId === (matched && matched.id) && dayOf(e.datetime) === dayOf(g.datetime));
      if (ex && !ex.gcal) ex.gcal = g;
      return;
    }
    seen.add(key);
    gcalDayTitles.add(normG(g.title).slice(0, 4) + dayOf(g.datetime));
    // 未紐付けは担当を付けない（以前は先頭ユーザーを仮表示→「担当が間違っている」誤解の元だった）
    const owner = matched ? (matched.ownerId ? D.user(matched.ownerId) : null) : null;
    evs.push({ id: g.id, caseId: matched ? matched.id : null, unlinked: !matched, datetime: g.datetime,
      title: matched ? matched.title : g.title,
      customer: matched ? D.customer(matched.customerId) : { shortName: (g.title || t('calendar.fallback.event')).slice(0, 6), company: g.title },
      owner, status: matched ? matched.status : null, kind: 'gcal', gcal: g });
  });
  // Fireflies の通話：レディクル・発注ナビ・案件名（会社名）一致をすべて表示
  const importedFf = new Set();
  cases.forEach(c => (c.meetingDocs || []).forEach(d => importedFf.add(d.ffId)));
  const norm = (s) => (s || '').replace(/株式会社|（株）|\(株\)|\s+/g, '');
  (sources.firefliesMeetings || []).forEach(f => {
    if (!f.datetime || importedFf.has(f.id)) return; // 取り込み済みは meetingDocs 側で表示
    if (gNoise(f.title)) return; // 社内定例・リマインダー等のノイズは出さない
    let matched = cases.find(c => c.id === f.caseId) || null;
    for (const c of cases) {
      if (matched) break;
      const cust = D.customer(c.customerId);
      if (!cust) continue;
      const core = norm(cust.company).toLowerCase(); // 大文字小文字を無視
      const ft = norm(f.title).toLowerCase();
      if ((core.length >= 3 && ft.includes(core)) || (core.length >= 2 && ft === core)) { matched = c; break; }
    }
    // 未紐付けの通話も表示（レディクル/発注ナビ以外も）＝ポップアップから案件作成/取込できる
    if (matched && matched.archived) return; // アーカイブ案件の通話はカレンダーに出さない
    if (gcalDayTitles.has(normG(f.title).slice(0, 4) + dayOf(f.datetime))) return; // Google 予定と重複
    const key = (matched ? matched.id : 'ff') + dayOf(f.datetime);
    if (seen.has(key)) return;
    seen.add(key);
    const owner = matched ? (matched.ownerId ? D.user(matched.ownerId) : null) : null;
    evs.push({ id: 'ff-' + f.id, caseId: matched ? matched.id : null, unlinked: !matched, datetime: f.datetime,
      title: matched ? matched.title : f.title,
      customer: matched ? D.customer(matched.customerId) : { shortName: (f.title || t('calendar.fallback.call')).replace(/・(レディクル|MTG|商談).*/g, '').replace(/株式会社/g, '').slice(0, 6), company: f.title },
      owner, status: matched ? matched.status : null, kind: 'ff', ff: f });
  });
  return evs;
}
function dateKey(d) { return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; }
function addDays(d, n) { const x = new Date(d); x.setDate(x.getDate() + n); return x; }

/* 同時刻に複数の予定がある場合の横並びレイアウト（Googleカレンダー方式）。
   各予定は1時間ブロックとして衝突グループを作り、列番号と列数を割り当てる */
function overlapLayout(evs) {
  const startMin = (e) => { const d = parseDT(e.datetime); return d ? d.getHours() * 60 + d.getMinutes() : 0; };
  const sorted = [...evs].sort((a, b) => (a.datetime || '').localeCompare(b.datetime || ''));
  const out = new Map();
  let cluster = [], clusterEnd = -1;
  const flush = () => {
    if (!cluster.length) return;
    const colEnd = [];
    cluster.forEach(e => {
      const s = startMin(e);
      let c = colEnd.findIndex(t => t <= s);
      if (c < 0) { c = colEnd.length; colEnd.push(0); }
      colEnd[c] = s + 60;
      out.set(e.id, { col: c });
    });
    cluster.forEach(e => { out.get(e.id).cols = colEnd.length; });
    cluster = [];
  };
  sorted.forEach(e => {
    const s = startMin(e);
    if (s >= clusterEnd) { flush(); clusterEnd = s + 60; } else { clusterEnd = Math.max(clusterEnd, s + 60); }
    cluster.push(e);
  });
  flush();
  return out;
}

/* イベントの表示色：担当者(owner) or ステータス(status) で切替 */
function evColor(e, colorBy) {
  const D = window.APP_DATA;
  if (colorBy === 'status' && e.status && D.STATUS && D.STATUS[e.status]) return D.STATUS[e.status].color;
  return (e.owner && e.owner.color) || '#4a5af0';
}
/* ダブルブッキング検出：同じ担当者・同日で時間帯(60分想定)が重なる予定の id 集合 */
function conflictIds(events) {
  const out = new Set();
  const groups = {};
  events.forEach(e => { if (!e.datetime || !e.owner) return; const k = e.owner.id + '|' + e.datetime.slice(0, 10); (groups[k] = groups[k] || []).push(e); });
  Object.values(groups).forEach(list => {
    if (list.length < 2) return;
    const arr = list.map(e => { const d = parseDT(e.datetime); return { e, s: d ? d.getHours() * 60 + d.getMinutes() : 0 }; }).sort((a, b) => a.s - b.s);
    for (let i = 1; i < arr.length; i++) if (arr[i].s < arr[i - 1].s + 60) { out.add(arr[i].e.id); out.add(arr[i - 1].e.id); }
  });
  return out;
}

/* 予定の新規追加・編集モーダル（案件の商談予定 = nextMeeting を更新） */
function EventFormModal({ initial, onClose }) {
  const { cases, scheduleMeeting } = useStore();
  const D = window.APP_DATA;
  const isEdit = !!initial.caseId;
  // 担当が割り当て済みで、クローズしていない案件のみ選択可能
  const selectable = cases.filter(c => c.ownerId && !c.archived && !['won', 'lost', 'done'].includes(c.status));
  const dt = initial.datetime ? parseDT(initial.datetime) : null;
  const [caseId, setCaseId] = React.useState(initial.caseId || '');
  const [date, setDate] = React.useState(dt ? dateKey(dt) : (initial.date || today()));
  const [time, setTime] = React.useState(dt ? fmtTime(initial.datetime) : (initial.time || '10:00'));
  const c = cases.find(k => k.id === caseId);
  // 予定の手段（電話/オンライン/訪問）＝タイムライン・カレンダーの表示が変わる（電話予定に会議URLを出さない等）
  const [method, setMethod] = React.useState(initial.method || (c && c.nextMeetingMethod) || 'online');
  const owner = c && c.ownerId ? D.user(c.ownerId) : null;
  const submit = () => {
    if (!caseId || !date || !time) return;
    scheduleMeeting(caseId, `${date}T${time}`, method);
    onClose();
  };
  const cancelMeeting = () => { scheduleMeeting(caseId, null); onClose(); };
  return (
    <Modal open onClose={onClose} width={520}
      title={isEdit ? t('calendar.eventModal.editTitle') : t('calendar.eventModal.addTitle')}
      subtitle={isEdit ? t('calendar.eventModal.editSubtitle') : t('calendar.eventModal.addSubtitle')}
      footer={<>
        {isEdit && <><Button variant="subtle" icon="x" onClick={cancelMeeting}>{t('calendar.btn.cancelMeeting')}</Button><div style={{ flex: 1 }} /></>}
        <Button variant="subtle" onClick={onClose}>{t('btn.cancel')}</Button>
        <Button variant="primary" icon="check" onClick={submit} disabled={!caseId || !date || !time}>{isEdit ? t('calendar.btn.save') : t('calendar.btn.register')}</Button>
      </>}>
      <Field label={t('cases.label')} required hint={isEdit ? null : t('calendar.eventModal.caseHint')}>
        <select value={caseId} onChange={(e) => setCaseId(e.target.value)} disabled={isEdit}
          style={{ ...inputStyle, appearance: 'none', WebkitAppearance: 'none', cursor: isEdit ? 'default' : 'pointer', color: caseId ? '#2b2f38' : '#9aa1ab', background: isEdit ? '#f6f7fa' : '#fff' }}>
          <option value="">{t('calendar.eventModal.selectCase')}</option>
          {selectable.map(k => <option key={k.id} value={k.id}>{D.customer(k.customerId).shortName}　{k.title}</option>)}
        </select>
      </Field>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
        <Field label={t('calendar.label.date')} required><TextInput type="date" value={date} onChange={(e) => setDate(e.target.value)} /></Field>
        <Field label={t('calendar.label.time')} required><TextInput type="time" value={time} onChange={(e) => setTime(e.target.value)} /></Field>
      </div>
      <Field label={t('calendar.label.method')}>
        <div style={{ display: 'flex', gap: 8 }}>
          {[['online', 'video', t('cd.methodOnline')], ['phone', 'phone', (D.METHODS.phone || {}).label || '電話'], ['visit', 'mapPin', (D.METHODS.visit || {}).label || '訪問']].map(([v, ic, lb]) => (
            <button key={v} onClick={() => setMethod(v)}
              style={{ display: 'inline-flex', alignItems: 'center', gap: 6, padding: '7px 14px', borderRadius: 9, cursor: 'pointer', fontFamily: 'inherit', fontSize: 12.5, fontWeight: 600,
                border: '1.5px solid ' + (method === v ? '#4a5af0' : '#e2e5ea'), background: method === v ? '#eef0fe' : '#fff', color: method === v ? '#4a5af0' : '#5b626d' }}>
              <Icon name={ic} size={13} stroke={2.2} />{lb}
            </button>
          ))}
        </div>
        {method === 'phone' && (
          <div style={{ display: 'flex', gap: 7, alignItems: 'flex-start', marginTop: 8, padding: '9px 12px', background: '#fdeee2', border: '1px solid #f7cfae', borderRadius: 8, fontSize: 12, color: '#b45309', lineHeight: 1.6 }}>
            <Icon name="phone" size={13} stroke={2.2} style={{ flex: '0 0 auto', marginTop: 2 }} />
            <span>{t('calendar.eventModal.phoneHint')}</span>
          </div>
        )}
      </Field>
      {owner && (
        <div style={{ display: 'flex', alignItems: 'center', gap: 9, padding: '10px 13px', background: '#f8f9fb', borderRadius: 9, marginTop: 2 }}>
          <Avatar user={owner} size={24} />
          <span style={{ fontSize: 12.5, color: '#3b414b' }}>{t('calendar.eventModal.ownerInfo', { name: owner.name })}</span>
        </div>
      )}
    </Modal>
  );
}

function Calendar() {
  const { cases, navigate, meetings, scheduleMeeting, patchCase, showToast, gcalEvents, firefliesMeetings } = useStore();
  const D = window.APP_DATA;
  const [view, setView] = React.useState('month');
  const [ownerFilter, setOwnerFilter] = React.useState('all');
  const [statusFilter, setStatusFilter] = React.useState('all');
  const [colorBy, setColorBy] = React.useState('owner');
  // 初期表示：今日以降で最も近い商談予定の月へ（過去↔未来は月送りで閲覧可）。なければ今日
  const initialAnchor = React.useMemo(() => {
    const future = buildEvents(cases, meetings, {gcalEvents,firefliesMeetings}).map(e => e.datetime).filter(dt => dt >= today()).sort();
    return new Date((future[0] ? future[0].slice(0, 10) : today()) + 'T00:00');
  }, []);
  const [anchor, setAnchor] = React.useState(initialAnchor);
  const [popup, setPopup] = React.useState(null);
  const [form, setForm] = React.useState(null); // {caseId?, datetime?, date?, time?}
  const [gcalOpen, setGcalOpen] = React.useState(false); // Google カレンダー取込モーダル
  // ダブルブッキング警告の閉じる：今出ている重複の組み合わせ(署名)を記憶。同じ重複の間は閉じたまま、新しい重複が出たら再表示
  const [dismissedConf, setDismissedConf] = React.useState(() => { try { return localStorage.getItem('anken_cal_conf_dismiss') || ''; } catch (_) { return ''; } });
  // 未紐付け（案件に無い予定/通話）の表示トグル＋作成/紐付け導線
  // 既定は非表示（社内ノイズ・私的予定の混入防止＝元設計の意図を尊重）。件数チップを押した時だけ表示
  const [showUnlinked, setShowUnlinked] = React.useState(() => { try { return localStorage.getItem('anken_cal_unlinked') === '1'; } catch (_) { return false; } });
  const toggleUnlinked = () => setShowUnlinked(v => { const nx = !v; try { localStorage.setItem('anken_cal_unlinked', nx ? '1' : '0'); } catch (_) {} return nx; });
  const [createFrom, setCreateFrom] = React.useState(null); // 未紐付けイベント → 案件作成モーダル
  const [linkFf, setLinkFf] = React.useState(null); // 未紐付けFireflies → 既存案件へ取込ピッカー
  const [ffBusy, setFfBusy] = React.useState(false);
  // Fireflies通話を案件の会議記録として取込（AI要約つき・既存の詳細タブと同じAPI）
  const importFfToCase = async (caseId, ff) => {
    if (ffBusy) return; setFfBusy(true);
    try {
      const k = cases.find(c => c.id === caseId);
      if (k && (k.meetingDocs || []).some(d => d.ffId === ff.id)) { showToast(t('case-detail.meetingDocAlreadyAdded'), 'x'); setFfBusy(false); return; }
      const r = await API.aiMeetingDoc(ff.id);
      patchCase(caseId, { meetingDocs: [r.doc, ...((k && k.meetingDocs) || [])] });
      showToast(t('calendar.unlinked.ffImported'));
    } catch (e2) { showToast(e2.message || t('cd.toast.analysisFailed'), 'x'); }
    setFfBusy(false); setLinkFf(null); setPopup(null);
  };
  const allEvents = buildEvents(cases, meetings, {gcalEvents,firefliesMeetings}).sort((a, b) => (a.datetime || '').localeCompare(b.datetime || '')); // 日内も時刻順
  const unlinkedCount = allEvents.filter(e => e.unlinked).length;
  let events = showUnlinked ? allEvents : allEvents.filter(e => !e.unlinked);
  if (ownerFilter !== 'all') events = events.filter(e => e.unlinked || e.owner.id === ownerFilter);
  if (statusFilter !== 'all') events = events.filter(e => e.unlinked || e.status === statusFilter);
  const conflicts = conflictIds(events);
  const confKey = [...conflicts].sort().join(',');

  const ownerOpts = [{ value: 'all', label: t('calendar.owner.all') }, ...D.users.map(u => ({ value: u.id, label: u.short }))];
  const statusOpts = [{ value: 'all', label: t('calendar.status.all') }, ...Object.entries(D.STATUS || {}).map(([k, v]) => ({ value: k, label: v.label }))];
  const colorOpts = [{ value: 'owner', label: t('calendar.colorBy.owner') }, { value: 'status', label: t('calendar.colorBy.status') }];
  const HOURS = []; for (let h = 8; h <= 19; h++) HOURS.push(h);
  const HOUR_H = 52, START = 8;

  // 週の起点（日曜）
  const weekStart = addDays(anchor, -anchor.getDay());
  const weekDays = Array.from({ length: 7 }, (_, i) => addDays(weekStart, i));
  const rangeLabel = view === 'week'
    ? `${weekStart.getFullYear()}年 ${weekStart.getMonth() + 1}/${weekStart.getDate()} 〜 ${addDays(weekStart, 6).getMonth() + 1}/${addDays(weekStart, 6).getDate()}`
    : view === 'day' ? `${anchor.getFullYear()}年 ${anchor.getMonth() + 1}月${anchor.getDate()}日(${wd(anchor.getDay())})`
    : view === 'list' ? t('calendar.list.title')
    : `${anchor.getFullYear()}年 ${anchor.getMonth() + 1}月`;
  const move = (dir) => setAnchor(a => addDays(a, view === 'week' ? dir * 7 : view === 'day' ? dir : dir * 30));

  const evStyle = (e) => { const d = parseDT(e.datetime); const top = (d.getHours() - START) * HOUR_H + (d.getMinutes() / 60) * HOUR_H;
    return { top, color: evColor(e, colorBy) }; };
  const methodOf = (e) => 'visit';

  const right = (
    <>
      <div style={{ display: 'flex', background: '#f0f1f4', borderRadius: 9, padding: 3, flexShrink: 0 }}>
        {[['month', t('calendar.view.month')], ['week', t('calendar.view.week')], ['day', t('calendar.view.day')], ['list', t('calendar.view.list')]].map(([v, l]) => (
          <button key={v} onClick={() => setView(v)} style={{ padding: '5px 12px', borderRadius: 7, border: 'none', cursor: 'pointer', fontFamily: 'inherit', fontSize: 13, fontWeight: 600, whiteSpace: 'nowrap',
            background: view === v ? '#fff' : 'transparent', color: view === v ? '#1c1f26' : '#7b828d', boxShadow: view === v ? '0 1px 2px rgba(20,22,40,.1)' : 'none' }}>{l}</button>
        ))}
      </div>
      <FilterPill label={t('cases.owner')} value={ownerFilter} options={ownerOpts} onChange={setOwnerFilter} icon="user" />
      <FilterPill label={t('cases.status')} value={statusFilter} options={statusOpts} onChange={setStatusFilter} icon="filter" />
      <FilterPill label={t('calendar.colorBy.label')} value={colorBy} options={colorOpts} onChange={setColorBy} icon="chart" />
      <Button variant="default" icon="refresh" onClick={() => setGcalOpen(true)}>{t('calendar.btn.import')}</Button>
    </>
  );

  return (
    <Page title={t('page.calendar')} right={right}>
      {/* ツールバー */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 16 }}>
        <Button variant="default" size="sm" onClick={() => setAnchor(new Date(today() + 'T00:00'))}>{t('calendar.btn.today')}</Button>
        {view !== 'list' && <div style={{ display: 'flex', gap: 4 }}>
          <IconButton name="chevronLeft" size={18} onClick={() => move(-1)} />
          <IconButton name="chevronRight" size={18} onClick={() => move(1)} />
        </div>}
        <div style={{ fontSize: 16, fontWeight: 700, color: '#1c1f26' }}>{rangeLabel}</div>
        <div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 14, flexWrap: 'wrap', justifyContent: 'flex-end' }}>
          {colorBy === 'owner'
            ? D.users.filter(u => events.some(e => e.owner && e.owner.id === u.id)).map(u => (
              <span key={u.id} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12, color: '#7b828d' }}>
                <Avatar user={u} size={18} />{u.short}
              </span>))
            : Object.entries(D.STATUS || {}).filter(([k]) => events.some(e => e.status === k)).map(([k, v]) => (
              <span key={k} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12, color: '#7b828d' }}>
                <span style={{ width: 9, height: 9, borderRadius: 3, background: v.color }} />{v.label}
              </span>))}
          {unlinkedCount > 0 && (
            <button onClick={toggleUnlinked} title={t('calendar.unlinked.toggleHint')}
              style={{ display: 'inline-flex', alignItems: 'center', gap: 5, border: '1.5px dashed ' + (showUnlinked ? '#e08a1e' : '#d5d9df'), background: showUnlinked ? '#fdf6ec' : '#fff',
                color: showUnlinked ? '#b45309' : '#9aa1ab', borderRadius: 999, padding: '4px 11px', fontSize: 12, fontWeight: 700, cursor: 'pointer', fontFamily: 'inherit' }}>
              <Icon name="alert" size={12} stroke={2.2} />{t('calendar.unlinked.chip', { n: unlinkedCount })}
            </button>
          )}
          <Button variant="primary" icon="plus" onClick={() => setForm({})}>{t('calendar.btn.addEvent')}</Button>
        </div>
      </div>

      {conflicts.size > 0 && confKey !== dismissedConf && (() => {
        const dupEvents = events.filter(e => conflicts.has(e.id)).sort((a, b) => (a.datetime || '').localeCompare(b.datetime || ''));
        return (
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', marginBottom: 14, padding: '11px 16px', background: '#fdf2f2', border: '1px solid #f3c9c9', borderRadius: 11 }}>
            <Icon name="alert" size={15} stroke={2} style={{ color: '#dc2626', flex: '0 0 auto' }} />
            <span style={{ fontSize: 12.5, fontWeight: 700, color: '#b91c1c' }}>{t('calendar.conflict.title', { n: conflicts.size })}</span>
            <span style={{ fontSize: 12, color: '#c2706f' }}>{t('calendar.conflict.warn')}</span>
            <span style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
              {dupEvents.map(e => (
                <button key={e.id} onClick={() => setPopup(e)}
                  style={{ border: '1px solid #eccaca', background: '#fff', color: '#b91c1c', borderRadius: 999, padding: '3px 11px', fontSize: 12, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit' }}>
                  {e.datetime ? `${e.datetime.slice(5, 10).replace('-', '/')} ${fmtTime(e.datetime)}` : ''} {e.owner.short}・{e.customer.shortName}
                </button>
              ))}
            </span>
            <button onClick={() => { setDismissedConf(confKey); try { localStorage.setItem('anken_cal_conf_dismiss', confKey); } catch (_) {} }} title={t('btn.close')}
              style={{ marginLeft: 'auto', flex: '0 0 auto', border: 'none', background: 'transparent', cursor: 'pointer', color: '#c2706f', display: 'inline-flex', padding: 4, borderRadius: 6 }}>
              <Icon name="x" size={15} stroke={2.2} />
            </button>
          </div>
        );
      })()}

      {view === 'week' && (
        <div style={{ background: '#fff', border: '1px solid #ecedf0', borderRadius: 12, overflow: 'hidden', boxShadow: '0 1px 2px rgba(20,22,40,.04)' }}>
          {/* 曜日ヘッダ */}
          <div style={{ display: 'grid', gridTemplateColumns: '56px repeat(7,1fr)', borderBottom: '1px solid #f0f1f4' }}>
            <div />
            {weekDays.map((d, i) => {
              const isToday = dateKey(d) === today();
              return (
                <div key={i} style={{ textAlign: 'center', padding: '10px 0', borderLeft: '1px solid #f4f5f7' }}>
                  <div style={{ fontSize: 12, color: d.getDay() === 0 ? '#dc2626' : d.getDay() === 6 ? '#2563eb' : '#9aa1ab', fontWeight: 600 }}>{wd(d.getDay())}</div>
                  <div style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 30, height: 30, borderRadius: '50%', marginTop: 3,
                    fontSize: 15, fontWeight: 700, color: isToday ? '#fff' : '#1c1f26', background: isToday ? '#4a5af0' : 'transparent' }}>{d.getDate()}</div>
                </div>
              );
            })}
          </div>
          {/* タイムグリッド */}
          <div style={{ display: 'grid', gridTemplateColumns: '56px repeat(7,1fr)', position: 'relative', maxHeight: 560, overflowY: 'auto' }}>
            <div>
              {HOURS.map(h => <div key={h} style={{ height: HOUR_H, position: 'relative' }}><span style={{ position: 'absolute', top: -7, right: 8, fontSize: 12, color: '#b4bac3', fontFamily: 'var(--mono)' }}>{String(h).padStart(2, '0')}:00</span></div>)}
            </div>
            {weekDays.map((d, di) => {
              const dayEvents = events.filter(e => dateKey(parseDT(e.datetime)) === dateKey(d));
              const lay = overlapLayout(dayEvents); // 同時刻は横並びに分割
              return (
                <div key={di} style={{ borderLeft: '1px solid #f4f5f7', position: 'relative' }}>
                  {HOURS.map(h => <div key={h} className="row-hover" onClick={() => setForm({ date: dateKey(d), time: String(h).padStart(2, '0') + ':00' })}
                    style={{ height: HOUR_H, borderBottom: '1px solid #f6f7fa', cursor: 'pointer' }} title={t('calendar.hint.addEvent')} />)}
                  {dayEvents.map(e => { const st = evStyle(e); const L = lay.get(e.id) || { col: 0, cols: 1 }; return (
                    <div key={e.id} onClick={() => setPopup(e)} style={{ position: 'absolute', top: st.top, height: HOUR_H - 6, borderRadius: 7,
                      left: `calc(${(L.col / L.cols) * 100}% + 3px)`, width: `calc(${100 / L.cols}% - 6px)`,
                      background: st.color + '18', borderLeft: '3px solid ' + st.color, padding: '4px 7px', cursor: 'pointer', overflow: 'hidden', transition: 'transform .1s' }}
                      onMouseEnter={(ev) => ev.currentTarget.style.transform = 'scale(1.02)'} onMouseLeave={(ev) => ev.currentTarget.style.transform = 'scale(1)'}>
                      <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
                        <span style={{ fontSize: 12, fontWeight: 700, color: st.color, fontFamily: 'var(--mono)', flex: 1 }}>{fmtTime(e.datetime)}</span>
                        {conflicts.has(e.id) && <span title={t('calendar.conflict.warn')} style={{ display: 'inline-flex', flex: '0 0 auto' }}><Icon name="alert" size={11} stroke={2.2} style={{ color: '#dc2626' }} /></span>}
                        <AvatarGroup users={[e.owner, ...(e.subs || [])].filter(Boolean)} size={16} />
                      </div>
                      <div style={{ fontSize: 12, fontWeight: 600, color: '#2b2f38', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{e.customer.shortName} {e.title}</div>
                    </div>
                  ); })}
                </div>
              );
            })}
          </div>
        </div>
      )}

      {view === 'day' && (
        <div style={{ background: '#fff', border: '1px solid #ecedf0', borderRadius: 12, overflow: 'hidden', boxShadow: '0 1px 2px rgba(20,22,40,.04)' }}>
          <div style={{ display: 'grid', gridTemplateColumns: '64px 1fr', maxHeight: 580, overflowY: 'auto' }}>
            <div>{HOURS.map(h => <div key={h} style={{ height: HOUR_H, position: 'relative' }}><span style={{ position: 'absolute', top: -7, right: 10, fontSize: 12, color: '#b4bac3', fontFamily: 'var(--mono)' }}>{String(h).padStart(2, '0')}:00</span></div>)}</div>
            <div style={{ borderLeft: '1px solid #f4f5f7', position: 'relative' }}>
              {HOURS.map(h => <div key={h} className="row-hover" onClick={() => setForm({ date: dateKey(anchor), time: String(h).padStart(2, '0') + ':00' })}
                style={{ height: HOUR_H, borderBottom: '1px solid #f6f7fa', cursor: 'pointer' }} title={t('calendar.hint.addEvent')} />)}
              {(() => { const dayEvs = events.filter(e => dateKey(parseDT(e.datetime)) === dateKey(anchor)); const lay = overlapLayout(dayEvs); return dayEvs.map(e => { const st = evStyle(e); const L = lay.get(e.id) || { col: 0, cols: 1 }; return (
                <div key={e.id} onClick={() => setPopup(e)} style={{ position: 'absolute', top: st.top, height: HOUR_H - 6, borderRadius: 8,
                  left: `calc(${(L.col / L.cols) * 100}% + 8px)`, width: `calc(${100 / L.cols}% - 16px)`,
                  background: st.color + '18', borderLeft: '3px solid ' + st.color, padding: '6px 11px', cursor: 'pointer', overflow: 'hidden' }}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                    <span style={{ fontSize: 12, fontWeight: 700, color: st.color, fontFamily: 'var(--mono)' }}>{fmtTime(e.datetime)}</span>
                    {conflicts.has(e.id) && <span title={t('calendar.conflict.warn')} style={{ display: 'inline-flex' }}><Icon name="alert" size={12} stroke={2.2} style={{ color: '#dc2626' }} /></span>}
                    <AvatarGroup users={[e.owner, ...(e.subs || [])].filter(Boolean)} size={18} />
                  </div>
                  <div style={{ fontSize: 12.5, fontWeight: 600, color: '#2b2f38', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{e.customer.company} · {e.title}</div>
                </div>
              ); }); })()}
              {events.filter(e => dateKey(parseDT(e.datetime)) === dateKey(anchor)).length === 0 && <div style={{ position: 'absolute', top: 40, left: 0, right: 0, textAlign: 'center', color: '#b4bac3', fontSize: 13 }}>{t('calendar.empty.noMeetings')}</div>}
            </div>
          </div>
        </div>
      )}

      {view === 'month' && <MonthView anchor={anchor} events={events} onPick={setPopup} onDay={(d) => { setAnchor(d); setView('day'); }} colorBy={colorBy} conflicts={conflicts} />}

      {view === 'list' && <ListView events={events} onPick={setPopup} colorBy={colorBy} conflicts={conflicts} />}

      {/* 日程未定の進行中案件（ReadyCrew側でも商談日が未定＝リスケ中など）を明示 */}
      {(() => {
        const undated = cases.filter(c => !c.archived && !['won', 'lost', 'done'].includes(c.status)
          && !caseMeetingAt(c) && !(c.meetingDocs || []).length && !meetings.some(m => m.caseId === c.id));
        if (!undated.length) return null;
        return (
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', marginTop: 14, padding: '11px 16px', background: '#fffaf0', border: '1px solid #f3e7c9', borderRadius: 11 }}>
            <Icon name="alert" size={15} stroke={2} style={{ color: '#b45309', flex: '0 0 auto' }} />
            <span style={{ fontSize: 12.5, fontWeight: 700, color: '#8a6d1f' }}>{t('calendar.undated.count', { n: undated.length })}</span>
            <span style={{ fontSize: 12, color: '#a08a4a' }}>{t('calendar.undated.hint')}</span>
            <span style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
              {undated.map(c => {
                const cust = D.customer(c.customerId);
                return (
                  <button key={c.id} onClick={() => navigate('case', c.id)}
                    style={{ border: '1px solid #ecdcb0', background: '#fff', color: '#8a6d1f', borderRadius: 999, padding: '3px 11px', fontSize: 12, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit' }}>
                    {cust ? cust.shortName : c.title.slice(0, 6)}
                  </button>
                );
              })}
            </span>
          </div>
        );
      })()}

      {popup && <EventPopup e={popup} onClose={() => setPopup(null)} onOpen={() => { navigate('case', popup.caseId); }}
        onEdit={() => { setForm({ caseId: popup.caseId, datetime: popup.datetime, method: popup.method }); setPopup(null); }} colorBy={colorBy} conflict={conflicts.has(popup.id)}
        onCreateCase={() => { setCreateFrom(popup); setPopup(null); }}
        onLinkCase={() => { if (popup.kind === 'ff') { setLinkFf(popup); } else { setForm({ datetime: popup.datetime }); } setPopup(null); }} />}
      {form && <EventFormModal initial={form} onClose={() => setForm(null)} />}
      {/* 未紐付けイベント → 新規案件を作成（保存後：Google予定は商談日をセット／Fireflies通話は会議記録として取込） */}
      {createFrom && <CaseFormModal initialTitle={(createFrom.gcal ? createFrom.gcal.title : (createFrom.ff && createFrom.ff.title)) || ''}
        onClose={() => setCreateFrom(null)}
        onSaved={(id) => { if (createFrom.kind === 'gcal') scheduleMeeting(id, createFrom.datetime); else if (createFrom.kind === 'ff' && createFrom.ff) importFfToCase(id, createFrom.ff); setCreateFrom(null); }} />}
      {/* 未紐付けFireflies通話 → 既存案件へ会議記録として取込 */}
      {linkFf && (
        <Modal open onClose={() => setLinkFf(null)} title={t('calendar.unlinked.ffLinkTitle')} subtitle={(linkFf.ff && linkFf.ff.title) || ''} width={480}
          footer={<Button variant="subtle" onClick={() => setLinkFf(null)}>{t('btn.cancel')}</Button>}>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 6, maxHeight: 360, overflowY: 'auto' }}>
            {cases.filter(c => !c.archived && !['won', 'lost', 'done'].includes(c.status)).slice(0, 80).map(c => (
              <button key={c.id} disabled={ffBusy} onClick={() => importFfToCase(c.id, linkFf.ff)}
                style={{ textAlign: 'left', border: '1px solid #ecedf0', background: '#fff', borderRadius: 9, padding: '9px 12px', cursor: 'pointer', fontFamily: 'inherit', opacity: ffBusy ? 0.6 : 1 }}>
                <div style={{ fontSize: 13, fontWeight: 600, color: '#1f2430' }}>{c.title}</div>
                <div style={{ fontSize: 11.5, color: '#9aa1ab', marginTop: 2 }}>{(D.customer(c.customerId) || {}).company || ''}</div>
              </button>
            ))}
          </div>
          {ffBusy && <div style={{ fontSize: 12.5, color: '#4a5af0', marginTop: 10, fontWeight: 600 }}>{t('calendar.unlinked.ffImporting')}</div>}
        </Modal>
      )}
      {gcalOpen && <GcalDetailModal item={{ id: 'gcal' }} onClose={() => setGcalOpen(false)} />}
    </Page>
  );
}

function MonthView({ anchor, events, onPick, onDay, colorBy, conflicts }) {
  const first = new Date(anchor.getFullYear(), anchor.getMonth(), 1);
  const start = addDays(first, -first.getDay());
  const cells = Array.from({ length: 42 }, (_, i) => addDays(start, i));
  return (
    <div style={{ background: '#fff', border: '1px solid #ecedf0', borderRadius: 12, overflow: 'hidden', boxShadow: '0 1px 2px rgba(20,22,40,.04)' }}>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7,1fr)', borderBottom: '1px solid #f0f1f4' }}>
        {wdAll().map((w, i) => <div key={i} style={{ textAlign: 'center', padding: '9px 0', fontSize: 12, fontWeight: 600, color: i === 0 ? '#dc2626' : i === 6 ? '#2563eb' : '#9aa1ab' }}>{w}</div>)}
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7,1fr)', gridTemplateRows: 'repeat(6,1fr)' }}>
        {cells.map((d, i) => {
          const inMonth = d.getMonth() === anchor.getMonth();
          const dayEv = events.filter(e => dateKey(parseDT(e.datetime)) === dateKey(d));
          const isToday = dateKey(d) === today();
          return (
            <div key={i} onClick={() => onDay(d)} style={{ minHeight: 92, borderLeft: i % 7 === 0 ? 'none' : '1px solid #f4f5f7', borderTop: i >= 7 ? '1px solid #f4f5f7' : 'none',
              padding: 7, cursor: 'pointer', background: inMonth ? '#fff' : '#fbfbfc' }}>
              <div style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 24, height: 24, borderRadius: '50%', fontSize: 12.5, fontWeight: 600,
                color: isToday ? '#fff' : (inMonth ? '#2b2f38' : '#c4c9d0'), background: isToday ? '#4a5af0' : 'transparent' }}>{d.getDate()}</div>
              <div style={{ marginTop: 4, display: 'flex', flexDirection: 'column', gap: 3 }}>
                {dayEv.slice(0, 3).map(e => (
                  <div key={e.id} onClick={(ev) => { ev.stopPropagation(); onPick(e); }} style={{ display: 'flex', alignItems: 'center', gap: 5, fontSize: 12, color: '#3b414b', background: evColor(e, colorBy) + '14', borderRadius: 4, padding: '2px 5px' }}>
                    <span style={{ width: 6, height: 6, borderRadius: '50%', background: evColor(e, colorBy), flex: '0 0 auto' }} />
                    <span style={{ flex: 1, minWidth: 0, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{fmtTime(e.datetime)} {e.customer.shortName}</span>
                    {conflicts && conflicts.has(e.id) && <Icon name="alert" size={10} stroke={2.4} style={{ color: '#dc2626', flex: '0 0 auto' }} />}
                    {/* 担当者名を常に表示（誰の商談か一目で分かるように）＋副担当も表示 */}
                    <AvatarGroup users={[e.owner, ...(e.subs || [])].filter(Boolean)} size={15} />
                  </div>
                ))}
                {dayEv.length > 3 && <div style={{ fontSize: 12, color: '#9aa1ab', paddingLeft: 5 }}>{t('calendar.more', { n: dayEv.length - 3 })}</div>}
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
}

/* イベント詳細 — Google カレンダー風（参加者・参加可否・備考・会議リンク） */
const eventKindLabel = (kind, method) => {
  // 予定（次回商談・ReadyCrew商談日）は手段があればそれを優先表示（「電話の予定は電話予定として」）
  if ((kind === 'next' || kind === 'appoint') && method === 'phone') return t('cd.phonePlanned');
  if ((kind === 'next' || kind === 'appoint') && method === 'visit') return t('cd.visitPlanned');
  return { next: t('calendar.kind.next'), appoint: t('calendar.kind.appoint'), done: t('calendar.kind.done'), doc: t('calendar.kind.doc'), ff: t('calendar.kind.ff'), gcal: t('calendar.kind.gcal') }[kind];
};
const gcalStatus = () => ({ yes: { label: t('calendar.rsvp.yes'), color: '#16a34a', icon: '✓' }, no: { label: t('calendar.rsvp.no'), color: '#dc2626', icon: '✕' }, maybe: { label: t('calendar.rsvp.maybe'), color: '#d97706', icon: '?' }, pending: { label: t('calendar.rsvp.pending'), color: '#9aa1ab', icon: '·' } });
const rsvpOpts = () => [['yes', t('calendar.rsvp.yes'), '#16a34a'], ['maybe', t('calendar.rsvp.maybe'), '#d97706'], ['no', t('calendar.rsvp.no'), '#dc2626']];

/* リスト（アジェンダ）ビュー：本日以降の予定を日付ごとに時系列で。日時・担当・顧客・準備状況・リンクを一覧 */
function ListView({ events, onPick, colorBy, conflicts }) {
  const D = window.APP_DATA;
  const upcoming = events.filter(e => e.datetime && e.datetime.slice(0, 10) >= today()).sort((a, b) => (a.datetime || '').localeCompare(b.datetime || ''));
  const groups = [];
  upcoming.forEach(e => { const day = e.datetime.slice(0, 10); let g = groups.find(x => x.day === day); if (!g) { g = { day, items: [] }; groups.push(g); } g.items.push(e); });
  if (!groups.length) return (
    <div style={{ background: '#fff', border: '1px solid #ecedf0', borderRadius: 12, padding: '48px 20px', textAlign: 'center', color: '#b4bac3', fontSize: 13.5, boxShadow: '0 1px 2px rgba(20,22,40,.04)' }}>{t('calendar.list.empty')}</div>
  );
  return (
    <div style={{ background: '#fff', border: '1px solid #ecedf0', borderRadius: 12, overflow: 'hidden', boxShadow: '0 1px 2px rgba(20,22,40,.04)' }}>
      {groups.map(g => {
        const d = new Date(g.day + 'T00:00'); const isToday = g.day === today();
        return (
          <div key={g.day}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 9, padding: '9px 16px', background: '#fafbfc', borderBottom: '1px solid #f0f1f4' }}>
              <span style={{ fontSize: 14, fontWeight: 700, color: isToday ? '#4a5af0' : '#1c1f26' }}>{d.getMonth() + 1}/{d.getDate()}</span>
              <span style={{ fontSize: 12, fontWeight: 600, color: d.getDay() === 0 ? '#dc2626' : d.getDay() === 6 ? '#2563eb' : '#9aa1ab' }}>({wd(d.getDay())})</span>
              {isToday && <span style={{ fontSize: 11, fontWeight: 700, color: '#fff', background: '#4a5af0', padding: '1px 8px', borderRadius: 999 }}>{t('calendar.list.today')}</span>}
              <span style={{ marginLeft: 'auto', fontSize: 12, color: '#b4bac3' }}>{t('calendar.list.count', { n: g.items.length })}</span>
            </div>
            {g.items.map(e => {
              const c = e.caseId ? D.caseById(e.caseId) : null;
              const hasLink = !!((e.gcal && e.gcal.meetUrl) || (e.doc && (e.doc.videoUrl || e.doc.url)) || (e.ff && e.ff.url) || casePlaceUrl(c) || (c && c.meetingLinks && c.meetingLinks.length));
              return (
                <div key={e.id} onClick={() => onPick(e)} className="row-hover"
                  style={{ display: 'flex', alignItems: 'center', gap: 11, padding: '11px 16px', borderBottom: '1px solid #f6f7fa', cursor: 'pointer' }}>
                  <span style={{ fontSize: 13, fontWeight: 700, color: evColor(e, colorBy), fontFamily: 'var(--mono)', flex: '0 0 auto', width: 44 }}>{fmtTime(e.datetime)}</span>
                  {(e.kind === 'next' || e.kind === 'appoint') && e.method === 'phone' && <span title={(D.METHODS.phone || {}).label || window.t("cd.tel")} style={{ display: 'inline-flex', flex: '0 0 auto' }}><Icon name="phone" size={13} stroke={2.2} style={{ color: '#ea580c' }} /></span>}
                  {(e.kind === 'next' || e.kind === 'appoint') && e.method === 'visit' && <span title={(D.METHODS.visit || {}).label || window.t("extra.common.visit")} style={{ display: 'inline-flex', flex: '0 0 auto' }}><Icon name="mapPin" size={13} stroke={2.2} style={{ color: '#2e9e6b' }} /></span>}
                  {conflicts && conflicts.has(e.id) && <span title={t('calendar.conflict.warn')} style={{ display: 'inline-flex', flex: '0 0 auto' }}><Icon name="alert" size={13} stroke={2.2} style={{ color: '#dc2626' }} /></span>}
                  <AvatarGroup users={[e.owner, ...(e.subs || [])].filter(Boolean)} size={20} />
                  <span style={{ flex: 1, minWidth: 0, fontSize: 13, fontWeight: 600, color: '#2b2f38', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{e.customer.shortName} <span style={{ color: '#7b828d', fontWeight: 400 }}>{e.title}</span></span>
                  {e.unlinked && <span style={{ flex: '0 0 auto', fontSize: 10, fontWeight: 700, color: '#b45309', background: '#fdf6ec', border: '1px dashed #ecc07f', padding: '1px 7px', borderRadius: 999 }}>{t('calendar.unlinked.badge')}</span>}
                  {c && <span style={{ display: 'inline-flex', gap: 3, flex: '0 0 auto' }}>
                    {[['cd.tab.proposal', c.proposalStatus], ['cd.tab.quote', c.quoteStatus], ['cd.tab.prototype', c.prototypeStatus]].map(([k, sv]) => { const s = PROPOSAL_STATUSES[sv || 'none'] || PROPOSAL_STATUSES.none; return <span key={k} title={t(k) + '：' + s.label} style={{ width: 8, height: 8, borderRadius: '50%', background: s.color }} />; })}
                  </span>}
                  {hasLink && <Icon name="link" size={13} stroke={2} style={{ color: '#b4bac3', flex: '0 0 auto' }} />}
                  {e.status && <StatusBadge status={e.status} size="sm" />}
                  <span style={{ fontSize: 11.5, fontWeight: 700, color: '#9aa1ab', background: '#f0f1f4', padding: '2px 7px', borderRadius: 5, flex: '0 0 auto' }}>{eventKindLabel(e.kind, e.method) || t('calendar.label.event')}</span>
                </div>
              );
            })}
          </div>
        );
      })}
    </div>
  );
}

function EventPopup({ e, onClose, onOpen, onEdit, colorBy, conflict, onCreateCase, onLinkCase }) {
  const { cases, patchCase, currentUser, showToast, navigate } = useStore();
  const D = window.APP_DATA;
  const GCAL_ST = gcalStatus();
  const RSVP_OPTS = rsvpOpts();
  const c = e.caseId ? cases.find(k => k.id === e.caseId) : null;
  // 参加者のメールが社内メンバーなら、生メールでなく氏名＋本人カラーのアバターで表示する
  const userByEmail = (em) => em && D.users && D.users.find(u => u.email && String(u.email).toLowerCase() === String(em).toLowerCase());

  // 参加者：Google カレンダー（出欠つき）＞ Fireflies の出席者 ＞ 担当者＋顧客窓口
  const gcal = e.gcal || null;
  const ffRow = e.ff || (e.doc ? D.firefliesMeetings.find(f => f.id === e.doc.ffId) : null);
  const attendees = (gcal && gcal.attendees && gcal.attendees.length)
    ? gcal.attendees // [{name,email,status}]
    : ((ffRow && ffRow.attendees && ffRow.attendees.length)
      ? ffRow.attendees
      : [e.owner && e.owner.name, e.customer && e.customer.contact ? `${e.customer.contact}（${e.customer.shortName}）` : null].filter(Boolean));
  const gStats = gcal ? ['yes', 'maybe', 'pending', 'no'].map(k => [k, attendees.filter(a => a.status === k).length]).filter(([, n]) => n > 0) : [];

  // 参加可否：案件行（eventRsvp）に保存して全員に共有。案件が無い通話は localStorage
  const rsvpMap = (c && c.eventRsvp && c.eventRsvp[e.id]) || {};
  const [inviteBusy, setInviteBusy] = React.useState(false); // 担当者のGoogleカレンダー招待
  const [localRsvp, setLocalRsvp] = React.useState(() => { try { return localStorage.getItem('anken_rsvp_' + e.id); } catch (_) { return null; } });
  const myRsvp = c ? (rsvpMap[currentUser.id] || null) : localRsvp;
  const setRsvp = (v) => {
    const next = myRsvp === v ? null : v;
    if (c) {
      const m = { ...rsvpMap };
      if (next) m[currentUser.id] = next; else delete m[currentUser.id];
      patchCase(c.id, { eventRsvp: { ...(c.eventRsvp || {}), [e.id]: m } });
    } else {
      try { next ? localStorage.setItem('anken_rsvp_' + e.id, next) : localStorage.removeItem('anken_rsvp_' + e.id); } catch (_) {}
      setLocalRsvp(next);
    }
  };

  // 備考：案件行（eventNotes）に保存
  const [note, setNote] = React.useState((c && c.eventNotes && c.eventNotes[e.id]) || '');
  const savedNote = (c && c.eventNotes && c.eventNotes[e.id]) || '';
  const saveNote = () => {
    if (!c) return;
    patchCase(c.id, { eventNotes: { ...(c.eventNotes || {}), [e.id]: note.trim() } });
    showToast(t('calendar.toast.noteSaved'));
  };

  // 会議リンク：Google Meet・録画・文字起こし・案件の会議リンク
  // 電話予定は会議リンクを出さない（時間になったら架電するだけ。URLが並ぶと紛らわしい）
  const isPhonePlan = (e.kind === 'next' || e.kind === 'appoint') && e.method === 'phone';
  /* 予定（未来）のリンクはタイムライン・商談タブと同じ単一真実（casePlanLinkList＝カレンダー予定のURLが正）。
     以前はGoogle予定のURLと案件の登録リンクを全部並べており、カレンダー=Zoom・案件=Meetの食い違い時に
     古い方を押して入室できない事故が同一担当で2回発生（2026-08-12 改善）。過去回は従来表示（履歴用）。 */
  const isPlanKind = (e.kind === 'next' || e.kind === 'appoint' || e.kind === 'gcal');
  const urlWarn = (!isPhonePlan && isPlanKind && c && !casePlaceUrl(c)) ? planUrlConflict(c, gcal) : null;
  const fixCaseUrl = () => {
    if (!urlWarn || !c) return;
    const link = { id: 'ml' + Date.now(), label: t('calendar.linkFromCal', { kind: urlWarn.gcalKind }), url: urlWarn.gcalUrl };
    patchCase(c.id, { meetingLinks: [link, ...(c.meetingLinks || [])] });   // 先頭に置く＝以後の全画面でこのURLが主になる
    showToast(t('calendar.linkConflictFixed', { kind: urlWarn.gcalKind }));
  };
  const markUrlOk = () => { if (urlWarn && c) patchCase(c.id, { meetUrlConflictOk: urlWarn.key }); };
  const links = [];
  if (!isPhonePlan) {
    // 手入力の「今回の会議URL」（casePlaceUrl・main側 2026-08-12 改修）は常に最優先で先頭に出す
    const placeUrl = casePlaceUrl(c);
    if (placeUrl) links.push({ label: t('ui.meetingLink.thisTime'), url: placeUrl, icon: 'video', color: '#16a34a' });
    if (isPlanKind && c) {
      // 予定（未来）はタイムライン・商談タブと同じ単一真実（casePlanLinkList＝カレンダー予定のURLが正）。古い登録リンクは並べない
      casePlanLinkList(c, gcal).filter(l => l.url !== placeUrl).forEach(l => links.push({ label: t('calendar.link.joinKind', { kind: l.label }), url: l.url, icon: 'video', color: placeUrl ? '#2563eb' : '#16a34a' }));
    } else if (gcal && gcal.meetUrl && gcal.meetUrl !== placeUrl) {
      links.push({ label: t('calendar.link.joinKind', { kind: meetUrlLabel(gcal.meetUrl) }), url: gcal.meetUrl, icon: 'video', color: placeUrl ? '#2563eb' : '#16a34a' });
    }
    if (e.doc && e.doc.videoUrl) links.push({ label: t('calendar.link.playRecording'), url: e.doc.videoUrl, icon: 'play', color: '#1d2129' });
    if (e.doc && e.doc.url) links.push({ label: t('calendar.link.firefliesTranscript'), url: e.doc.url, icon: 'spark', color: '#ef5a3c' });
    if (e.ff && e.ff.url) links.push({ label: t('calendar.link.firefliesTranscript'), url: e.ff.url, icon: 'spark', color: '#ef5a3c' });
    if (!(isPlanKind && c)) (c && c.meetingLinks || []).filter(l => l && l.url !== placeUrl).forEach(l => links.push({ label: l.label || t('calendar.link.meetingLink'), url: l.url, icon: 'video', color: '#2563eb' }));
    // Meet も会議リンクも無い場合は取得元（ReadyCrew）へのリンクを出す
    if (!links.length && c && c.source === 'scrape' && c.sourceUrl) links.push({ label: t('calendar.link.openReadyCrew'), url: c.sourceUrl, icon: 'refresh', color: '#4a5af0' });
  }

  const Row = ({ icon, children, top }) => (
    <div style={{ display: 'flex', alignItems: top ? 'flex-start' : 'center', gap: 12, padding: '5px 0' }}>
      <Icon name={icon} size={16} stroke={2} style={{ color: '#a8aeb8', flex: '0 0 auto', marginTop: top ? 3 : 0 }} />
      <div style={{ flex: 1, minWidth: 0, fontSize: 13, color: '#3b414b' }}>{children}</div>
    </div>
  );

  return (
    <div onMouseDown={onClose} style={{ position: 'fixed', inset: 0, zIndex: 150, background: 'rgba(24,26,32,.22)', display: 'flex', alignItems: 'flex-start', justifyContent: 'center', padding: '7vh 20px 40px', overflowY: 'auto' }}>
      <div onMouseDown={(ev) => ev.stopPropagation()} style={{ width: 420, maxWidth: '100%', background: '#fff', borderRadius: 14, boxShadow: '0 24px 60px rgba(20,22,40,.28)', overflow: 'hidden', animation: 'modalIn .16s ease' }}>
        <div style={{ height: 6, background: evColor(e, colorBy) }} />
        <div style={{ padding: '16px 20px 18px' }}>
          {/* タイトル */}
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 8 }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
              {e.status && <StatusBadge status={e.status} size="sm" />}
              {/* 電話予定/訪問予定は一目で分かるよう色付き＋アイコンで強調（グレーの汎用バッジにしない） */}
              {isPhonePlan
                ? <span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, fontSize: 12.5, fontWeight: 800, color: '#ea580c', background: '#fdeee2', border: '1px solid #f7cfae', padding: '3px 10px', borderRadius: 999 }}><Icon name="phone" size={13} stroke={2.4} />{t('cd.phonePlanned')}</span>
                : (e.kind === 'next' || e.kind === 'appoint') && e.method === 'visit'
                  ? <span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, fontSize: 12.5, fontWeight: 800, color: '#2e9e6b', background: '#e6f4ec', border: '1px solid #b7e0c8', padding: '3px 10px', borderRadius: 999 }}><Icon name="mapPin" size={13} stroke={2.4} />{t('cd.visitPlanned')}</span>
                  : <span style={{ fontSize: 12, fontWeight: 700, color: '#7b828d', background: '#f0f1f4', padding: '2px 8px', borderRadius: 5 }}>{eventKindLabel(e.kind, e.method) || t('calendar.label.event')}</span>}
              {e.owner && <span style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}><Avatar user={e.owner} size={18} /><span style={{ fontSize: 12, fontWeight: 600, color: '#3b414b' }}>{e.owner.short}</span></span>}
              {/* サポート担当も表示（メインと見分けがつくよう控えめ・ツールチップで役割明示） */}
              {(e.subs || []).map(u => (
                <span key={u.id} title={t('ui.supportAssignee')} style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
                  <Avatar user={u} size={18} /><span style={{ fontSize: 12, fontWeight: 600, color: '#8a919c' }}>{u.short}</span>
                </span>
              ))}
              {c && c.rank && c.rank !== window.t("extra.common.notYet") && D.RANKS && D.RANKS[c.rank] && <span title={D.RANKS[c.rank].desc} style={{ fontSize: 11.5, fontWeight: 700, color: D.RANKS[c.rank].fg, background: D.RANKS[c.rank].soft, padding: '2px 8px', borderRadius: 5 }}>{t('calendar.rankBadge', { rank: D.RANKS[c.rank].label })}</span>}
            </div>
            <IconButton name="x" size={16} onClick={onClose} />
          </div>
          <div style={{ fontSize: 16.5, fontWeight: 700, color: '#1c1f26', marginTop: 9, lineHeight: 1.4 }}>{e.title}</div>
          <div style={{ fontSize: 12.5, color: '#7b828d', marginTop: 3 }}>{e.customer.company}</div>
          {conflict && <div style={{ display: 'flex', alignItems: 'center', gap: 7, marginTop: 9, padding: '7px 10px', background: '#fdf2f2', border: '1px solid #f3c9c9', borderRadius: 8, fontSize: 12, color: '#b91c1c', fontWeight: 600 }}><Icon name="alert" size={13} stroke={2.2} style={{ color: '#dc2626', flex: '0 0 auto' }} />{t('calendar.conflict.popup')}</div>}

          <div style={{ height: 1, background: '#f0f1f4', margin: '11px 0 5px' }} />

          {/* 日時 */}
          <Row icon="clock"><b>{fmtDateFull(e.datetime)}（{wd(parseDT(e.datetime).getDay())}）</b> {fmtTime(e.datetime)}{e.doc && e.doc.duration ? <span style={{ color: '#9aa1ab' }}> · {t('calendar.duration.minutes', { n: e.doc.duration })}</span> : null}</Row>

          {/* 電話予定の案内（リンクの代わりに、やることを明示） */}
          {isPhonePlan && (
            <div style={{ display: 'flex', alignItems: 'center', gap: 9, margin: '7px 0 3px', padding: '10px 13px', background: '#fdeee2', border: '1px solid #f7cfae', borderRadius: 10 }}>
              <Icon name="phone" size={16} stroke={2.2} style={{ color: '#ea580c', flex: '0 0 auto' }} />
              <span style={{ fontSize: 12.5, fontWeight: 700, color: '#9a4a12', lineHeight: 1.5 }}>{t('cd.upcomingPhone')}</span>
            </div>
          )}

          {/* 参加者（Google 予定は出欠ステータス付き） */}
          <Row icon="customers" top>
            <div style={{ fontSize: 12, color: '#9aa1ab', marginBottom: 5 }}>
              {t('calendar.attendees.count', { n: attendees.length })}
              {gStats.length > 0 && <span style={{ marginLeft: 8 }}>{gStats.map(([k, n]) => t('calendar.attendees.statusCount', { label: GCAL_ST[k].label, n })).join(t('calendar.attendees.separator'))}</span>}
            </div>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
              {attendees.map((a, i) => {
                const isObj = a && typeof a === 'object';
                const mu = isObj ? userByEmail(a.email) : null; // 社内メンバーに一致したら氏名＋本人カラーで
                const name = mu ? mu.name : (isObj ? (a.name || a.email) : String(a));
                const av = mu ? { bg: mu.color, fg: '#fff', ch: (mu.short || mu.name).charAt(0) } : { bg: '#eef0fe', fg: '#4a5af0', ch: name.charAt(0) };
                const st = isObj ? GCAL_ST[a.status] : null;
                const isOrganizer = gcal && isObj && gcal.organizer && (a.name === gcal.organizer || a.email === gcal.organizer);
                return (
                  <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                    <span style={{ position: 'relative', width: 22, height: 22, flex: '0 0 auto' }}>
                      <span style={{ width: 22, height: 22, borderRadius: '50%', background: av.bg, color: av.fg, fontSize: 12, fontWeight: 700, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>{av.ch}</span>
                      {st && <span style={{ position: 'absolute', right: -3, bottom: -3, width: 12, height: 12, borderRadius: '50%', background: st.color, color: '#fff', fontSize: 12, fontWeight: 800, display: 'flex', alignItems: 'center', justifyContent: 'center', boxShadow: '0 0 0 1.5px #fff' }}>{st.icon}</span>}
                    </span>
                    <span style={{ fontSize: 12.5, color: '#2b2f38' }}>{name}</span>
                    {isOrganizer && <span style={{ fontSize: 12, color: '#7b828d', background: '#f0f1f4', padding: '1px 6px', borderRadius: 4, fontWeight: 700 }}>{t('calendar.attendees.organizer')}</span>}
                    {!isObj && e.owner && name.includes(e.owner.short) && <span style={{ fontSize: 12, color: '#4a5af0', background: '#eef0fe', padding: '1px 6px', borderRadius: 4, fontWeight: 700 }}>{t('calendar.attendees.mainOwner')}</span>}
                  </div>
                );
              })}
            </div>
          </Row>

          {/* 参加可否（自分） */}
          <Row icon="check2" top>
            <div style={{ fontSize: 12, color: '#9aa1ab', marginBottom: 6 }}>{t('calendar.label.rsvp')}</div>
            <div style={{ display: 'flex', gap: 7 }}>
              {RSVP_OPTS.map(([v, l, col]) => (
                <button key={v} onClick={() => setRsvp(v)}
                  style={{ padding: '5px 14px', borderRadius: 999, cursor: 'pointer', fontFamily: 'inherit', fontSize: 12, fontWeight: 700,
                    border: '1.5px solid ' + (myRsvp === v ? col : '#e2e5ea'),
                    background: myRsvp === v ? col + '14' : '#fff', color: myRsvp === v ? col : '#7b828d', transition: 'all .12s' }}>{l}</button>
              ))}
            </div>
            {c && Object.keys(rsvpMap).length > 0 && (
              <div style={{ display: 'flex', flexDirection: 'column', gap: 3, marginTop: 8 }}>
                {Object.entries(rsvpMap).map(([uid, v]) => {
                  const u = D.user(uid);
                  const opt = RSVP_OPTS.find(o => o[0] === v);
                  return u && opt ? (
                    <div key={uid} style={{ display: 'flex', alignItems: 'center', gap: 7, fontSize: 12, color: '#5b626d' }}>
                      <Avatar user={u} size={18} />{u.short}
                      <span style={{ color: opt[2], fontWeight: 700 }}>{opt[1]}</span>
                    </div>
                  ) : null;
                })}
              </div>
            )}
          </Row>

          {/* 担当者をGoogleカレンダーへ招待：既存の予定（自作 or ICS同期一致）があれば参加者を追記、無ければ新規作成 */}
          {c && (e.kind === 'next' || e.kind === 'appoint') && (
            <Row icon="google" top>
              <div style={{ fontSize: 12, color: '#9aa1ab', marginBottom: 5 }}>{t('cd.calInvite.membersLabel')}</div>
              <Button size="sm" variant="default" icon="google" disabled={inviteBusy}
                onClick={async () => {
                  if (inviteBusy) return;
                  setInviteBusy(true);
                  try {
                    const r = await API.calendarInvite(c.id);
                    showToast(r && r.mode === 'created' ? t('cd.calInvite.createdNew') : r && r.mode === 'updated' ? t('cd.calInvite.updated') : t('cd.calInvite.addedExisting'));
                  } catch (err) { showToast((err && err.message) || t('cd.calInvite.failed'), 'x'); }
                  setInviteBusy(false);
                }}>{inviteBusy ? t('cd.calInvite.sending') : t('cd.calInvite.membersBtn')}</Button>
            </Row>
          )}

          {/* カレンダー予定と案件登録リンクのURL食い違い警告（Zoom/Meet違い＝入れない事故防止） */}
          {urlWarn && (
            <div style={{ margin: '8px 0', padding: '10px 12px', background: '#fdeeee', border: '1px solid #f5c6c6', borderRadius: 10 }}>
              <div style={{ fontSize: 12, color: '#b91c1c', fontWeight: 700, lineHeight: 1.6 }}>
                {t('calendar.linkConflict', { gk: urlWarn.gcalKind, ck: urlWarn.caseKind })}
              </div>
              <div style={{ display: 'flex', gap: 8, marginTop: 8, flexWrap: 'wrap' }}>
                <button onClick={fixCaseUrl}
                  style={{ fontSize: 12, fontWeight: 700, padding: '6px 12px', borderRadius: 8, border: 'none', background: '#b91c1c', color: '#fff', cursor: 'pointer', fontFamily: 'inherit' }}>
                  {t('calendar.linkConflictFix', { kind: urlWarn.gcalKind })}
                </button>
                <button onClick={markUrlOk}
                  style={{ fontSize: 12, fontWeight: 600, padding: '6px 12px', borderRadius: 8, border: '1px solid #e6c6c6', background: '#fff', color: '#8a5a5a', cursor: 'pointer', fontFamily: 'inherit' }}>
                  {t('calendar.linkConflictOk')}
                </button>
              </div>
            </div>
          )}
          {/* 会議リンク */}
          {links.length > 0 && (
            <Row icon="link" top>
              <div style={{ fontSize: 12, color: '#9aa1ab', marginBottom: 5 }}>{t('calendar.label.links')}</div>
              <div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
                {links.map((l, i) => (
                  <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                    <a href={l.url} target="_blank" rel="noreferrer"
                      style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12.5, color: '#4a5af0', textDecoration: 'none', fontWeight: 600, minWidth: 0 }}>
                      <Icon name={l.icon} size={13} stroke={2} fill={l.icon === 'spark' || l.icon === 'play' ? l.color : 'none'} style={{ color: l.color, flex: '0 0 auto' }} />{l.label}
                    </a>
                    {/* リンクをコピー（お客様への案内に貼る用） */}
                    <button onClick={async () => showToast(...((await copyText(l.url)) ? [t('cd.linkCopied')] : [t('cd.copyFailed'), 'x']))}
                      title={t('cd.copyLink')}
                      style={{ display: 'inline-flex', alignItems: 'center', padding: 3, border: 'none', background: 'transparent', cursor: 'pointer', color: '#9aa1ab' }}>
                      <Icon name="copy" size={13} stroke={2} />
                    </button>
                  </div>
                ))}
              </div>
            </Row>
          )}

          {/* 商談準備（これからの商談のみ）：提案/見積/プロト の提出状況＋商談提案プレイブックへの動線 */}
          {c && e.datetime && e.datetime.slice(0, 10) >= today() && (
            <Row icon="check2" top>
              <div style={{ fontSize: 12, color: '#9aa1ab', marginBottom: 6 }}>{t('calendar.label.prep')}</div>
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 9 }}>
                {[[t('cd.tab.proposal'), c.proposalStatus, 'proposal'], [t('cd.tab.quote'), c.quoteStatus, 'quote'], [t('cd.tab.prototype'), c.prototypeStatus, 'prototype']].map(([lbl, sv, tab]) => {
                  const s = PROPOSAL_STATUSES[sv || 'none'] || PROPOSAL_STATUSES.none;
                  return <button key={lbl} onClick={() => { navigate('case', c.id, tab); onClose(); }} title={t('calendar.prep.openTabHint')} style={{ display: 'inline-flex', alignItems: 'center', gap: 5, fontSize: 12, fontWeight: 600, color: s.color, background: s.soft, padding: '3px 9px', borderRadius: 999, border: 'none', cursor: 'pointer', fontFamily: 'inherit' }}>{lbl}<span style={{ opacity: .8 }}>{s.label}</span></button>;
                })}
              </div>
              <button onClick={() => { navigate('case', c.id, 'shodan'); onClose(); }}
                style={{ display: 'inline-flex', alignItems: 'center', gap: 6, border: '1px solid #d8d5f5', background: '#f5f3ff', color: '#4a5af0', fontSize: 12.5, fontWeight: 700, borderRadius: 8, padding: '7px 12px', cursor: 'pointer', fontFamily: 'inherit' }}>
                <Icon name="spark" size={13} fill="#7c3aed" style={{ color: '#7c3aed' }} />{c.shodanPrep ? t('calendar.btn.viewPlaybook') : t('calendar.btn.makePlaybook')}
              </button>
            </Row>
          )}
          {/* 備考 */}
          {c && (
            <Row icon="edit" top>
              <div style={{ fontSize: 12, color: '#9aa1ab', marginBottom: 5 }}>{t('calendar.label.notes')}</div>
              <textarea value={note} onChange={(ev) => setNote(ev.target.value)} rows={2} placeholder={t('calendar.placeholder.addNote')}
                style={{ ...inputStyle, fontSize: 12.5, lineHeight: 1.6, resize: 'vertical', minHeight: 44 }} />
              {note.trim() !== savedNote && (
                <div style={{ marginTop: 6 }}><Button size="sm" variant="primary" icon="check" onClick={saveNote}>{t('calendar.btn.saveNote')}</Button></div>
              )}
            </Row>
          )}

          {/* 未紐付け（案件に無い予定/通話）→ ここから案件を作成 or 既存案件へ */}
          {!e.caseId && (
            <div style={{ marginTop: 12, padding: '10px 12px', background: '#fdf6ec', border: '1px dashed #ecc07f', borderRadius: 9 }}>
              <div style={{ fontSize: 12, fontWeight: 700, color: '#b45309', marginBottom: 8, display: 'flex', alignItems: 'center', gap: 6 }}>
                <Icon name="alert" size={13} stroke={2.2} />{t('calendar.unlinked.popupNote')}
              </div>
              <div style={{ display: 'flex', gap: 8 }}>
                {onCreateCase && <Button variant="primary" size="sm" icon="plus" onClick={onCreateCase} style={{ flex: 1 }}>{t('calendar.unlinked.createCase')}</Button>}
                {onLinkCase && <Button variant="default" size="sm" icon="link" onClick={onLinkCase} style={{ flex: 1 }}>{e.kind === 'ff' ? t('calendar.unlinked.linkFf') : t('calendar.unlinked.linkCase')}</Button>}
              </div>
            </div>
          )}
          <div style={{ display: 'flex', gap: 8, marginTop: 14 }}>
            {onEdit && e.kind === 'next' && <Button variant="default" icon="edit" onClick={onEdit} style={{ flex: 1 }}>{t('btn.edit')}</Button>}
            {e.caseId && <Button variant="primary" icon="arrowRight" onClick={onOpen} style={{ flex: 1.4 }}>{t('calendar.btn.openCase')}</Button>}
          </div>
        </div>
      </div>
    </div>
  );
}

Object.assign(window, { Calendar, MonthView, ListView, EventPopup, EventFormModal, buildEvents, dateKey, addDays });
