// Calendar — appointment modals (split from calendar.jsx).
// MeetingModal, ProspectModal, EditApptModal (the big one), ConflictWarning,
// DateTimePicker, Field. Loaded after calendar-grid.jsx.

// ─── MeetingModal ────────────────────────────────────────────────────────────
// Lightweight form for MT (นัดประชุม) appointments — only title, duration,
// and optional notes. No prospect tracking, no owner assignment.
function MeetingModal({ open, onClose, draft, onSave }) {
  const { apptTypes } = useConfig();
  const tid = draft?.type || 'MT';
  const mt = apptTypes.find(t => t.id === tid) || { id: tid, label: tid, color: '#64748B', tint: '#F8FAFC', dur: 60, full: 'นัดประชุม' };
  const MT_TITLE = { MT: 'นัดประชุม', EV: 'Event · ธุระส่วนตัว', CS: 'Class · ลงคลาสเรียน' };
  const MT_PH = { MT: 'กรอกชื่อประชุม + บันทึก', EV: 'กรอกชื่อธุระ + บันทึก', CS: 'กรอกชื่อคลาส + บันทึก' };

  const NAME_LBL = { MT: 'ชื่อประชุม', EV: 'ชื่อกิจกรรม / ธุระ', CS: 'ชื่อคลาส' };
  const NAME_PH = { MT: 'เช่น Weekly Team Sync, ประชุมทีม…', EV: 'เช่น ออกกำลังกาย, ทำธุระ…', CS: 'เช่น Stretching Pilates, คลาสพิเศษ…' };
  const LOC_PH = { MT: 'เช่น ห้องประชุม / Zoom link', EV: 'เช่น ฟิตเนส, ที่อยู่…', CS: 'เช่น สตูดิโอ, ออนไลน์ / Zoom link' };

  const [title, setTitle] = useState('');
  const [dur,   setDur]   = useState(1);
  const [place, setPlace] = useState('');
  const [notes, setNotes] = useState('');

  useEffect(() => {
    if (open) {
      setTitle('');
      setPlace('');
      setNotes('');
      setDur((mt.dur || 60) / 60);
    }
  }, [open]); // eslint-disable-line

  const canSave = title.trim().length > 0;
  const fmtDur = (h) => h === 0.5 ? '30 นาที' : (h % 1 === 0.5 ? `${Math.floor(h)} ชม. 30 นาที` : `${h} ชม.`);
  const submit = () => {
    if (!canSave) return;
    // Fold location into notes as a 📍 line so it persists (noTrack types have
    // no dedicated column).
    const noteLines = [place.trim() ? `📍 ${place.trim()}` : '', notes.trim()].filter(Boolean);
    onSave({ title: title.trim(), dur, notes: noteLines.join('\n') || undefined });
  };

  return (
    <Modal open={open} onClose={onClose} title={
      <div className="flex items-center gap-2">
        <div className="w-7 h-7 rounded-md flex items-center justify-center text-white text-[11px] font-bold" style={{ background: mt.color }}>{mt.label}</div>
        <div>
          <div className="text-sm font-semibold text-ink-900">{MT_TITLE[tid] || mt.full || 'นัดหมาย'}</div>
          <div className="text-[11px] text-ink-400">
            {draft ? `${formatTime(draft.start)} · ` : ''}{MT_PH[tid] || 'กรอกชื่อ + บันทึก'}
          </div>
        </div>
      </div>
    } size="sm" footer={
      <div className="flex items-center justify-end gap-2">
        <Btn variant="ghost" size="sm" onClick={onClose}>ยกเลิก</Btn>
        <Btn variant="primary" size="sm" disabled={!canSave}
          icon={<Icon name="check" className="w-3.5 h-3.5" />}
          onClick={submit}>
          บันทึก
        </Btn>
      </div>
    }>
      <div className="space-y-4">
        {/* Title */}
        <div>
          <label className="block text-[11px] font-semibold text-ink-600 uppercase tracking-wide mb-1.5">
            {NAME_LBL[tid] || 'ชื่อ'} <span className="text-rose-500">*</span>
          </label>
          <input
            autoFocus
            value={title}
            onChange={e => setTitle(e.target.value)}
            onKeyDown={e => e.key === 'Enter' && submit()}
            className="w-full h-10 px-3 text-sm border border-ink-200 rounded-lg outline-none focus:border-brand-500 focus:ring-2 focus:ring-brand-100"
            placeholder={NAME_PH[tid] || 'ชื่อ...'}
          />
        </div>

        {/* Location */}
        <div>
          <label className="block text-[11px] font-semibold text-ink-600 uppercase tracking-wide mb-1.5">สถานที่ (ไม่บังคับ)</label>
          <input
            value={place}
            onChange={e => setPlace(e.target.value)}
            className="w-full h-10 px-3 text-sm border border-ink-200 rounded-lg outline-none focus:border-brand-500 focus:ring-2 focus:ring-brand-100"
            placeholder={LOC_PH[tid] || 'สถานที่ / ลิงก์...'}
          />
        </div>

        {/* Duration — up to 8h for classes/events */}
        <div>
          <label className="block text-[11px] font-semibold text-ink-600 uppercase tracking-wide mb-1.5">ระยะเวลา</label>
          <div className="flex flex-wrap gap-1.5">
            {[0.5, 1, 1.5, 2, 3, 4, 6, 8].map(h => (
              <button key={h}
                onClick={() => setDur(h)}
                className="px-3 py-1.5 rounded-lg border-2 text-xs font-semibold transition-all num"
                style={Math.abs(dur - h) < 0.01
                  ? { borderColor: mt.color, background: mt.tint, color: mt.color }
                  : { borderColor: '#E6EAF3', background: 'white', color: '#445576' }}>
                {fmtDur(h)}
              </button>
            ))}
          </div>
        </div>

        {/* Notes */}
        <div>
          <label className="block text-[11px] font-semibold text-ink-600 uppercase tracking-wide mb-1.5">บันทึก (ไม่บังคับ)</label>
          <textarea
            value={notes}
            onChange={e => setNotes(e.target.value)}
            rows={3}
            className="w-full px-3 py-2 text-sm border border-ink-200 rounded-lg outline-none focus:border-brand-500 focus:ring-2 focus:ring-brand-100 resize-none"
            placeholder="Agenda, รายละเอียด, หมายเหตุ ..."
          />
        </div>
      </div>
    </Modal>
  );
}

function ProspectModal({ open, onClose, draft, onSave, currentUser, dlProspects = [], sixWCustomers = [] }) {
  const { apptTypes, leadSources, defaultCaseOwner } = useConfig();
  const type = draft ? apptTypes.find(t => t.id === draft.type) : null;
  const isFU = draft?.type === 'FU';
  // Default case-owner from Settings: 'partner' only applies if a partner exists.
  const defOwner = (defaultCaseOwner === 'partner' && currentUser?.partner_name) ? 'partner' : 'self';
  const [name, setName] = useState('');
  const [source, setSource] = useState(leadSources[0]?.id || 'fb');
  const [status, setStatus] = useState('pending');
  const [dur, setDur] = useState(1);
  const [notes, setNotes] = useState('');
  // caseFor: 'self' = primary's own case, 'partner' = partner's own case,
  // any UUID = that downline's case. We translate to (dlId, isPartner) on save.
  const [caseFor, setCaseFor] = useState(defOwner);
  const [country, setCountry] = useState('th');
  const [sourceUrl, setSourceUrl] = useState('');
  const [lineGroupUrl, setLineGroupUrl] = useState('');
  const [age, setAge] = useState('');
  const [igAvatar, setIgAvatar] = useState(null); // dicebear preview when source=ig and a handle is parseable
  const [confirming, setConfirming] = useState(false);
  // Follow-Up specific: who we're following up — DL or 6W customer
  const [fuTarget, setFuTarget] = useState('dl');     // 'dl' | '6w'
  const [fuTargetId, setFuTargetId] = useState('');   // selected dl prospect id OR 6W customer id

  useEffect(() => {
    if (open && draft) {
      setName(''); setStatus('pending'); setDur(draft.dur || 1); setNotes('');
      setCaseFor(defOwner); setCountry('th');
      setSourceUrl(''); setLineGroupUrl(''); setAge(''); setIgAvatar(null);
      setConfirming(false);
      setFuTarget('dl'); setFuTargetId('');
      if (leadSources.length > 0) setSource(leadSources[0].id);
    }
  }, [open, draft, currentUser?.id, leadSources]);

  // Parse a likely IG handle out of whatever the user pasted — full URL,
  // "@handle", or bare handle. Returns null if it does not look like one.
  const isIG = source === 'ig';
  const parseIgHandle = (raw) => {
    if (!raw) return null;
    const m = raw.match(/instagram\.com\/([^/?#\s]+)/i);
    if (m) return m[1].replace(/^@/, '');
    const bare = raw.trim().replace(/^@/, '');
    return /^[a-zA-Z0-9._]{1,30}$/.test(bare) ? bare : null;
  };
  const igHandleFromUrl = isIG ? parseIgHandle(sourceUrl) : null;
  useEffect(() => {
    if (!isIG || !igHandleFromUrl) { setIgAvatar(null); return; }
    setIgAvatar(`https://api.dicebear.com/9.x/notionists/svg?seed=${encodeURIComponent(igHandleFromUrl)}&backgroundColor=ffd5dc,ffdfbf,d1d4f9,c0aede,b6e3f4`);
  }, [isIG, igHandleFromUrl]);

  if (!draft) return null;

  const sourceObj = leadSources.find(s => s.id === source);
  const sourceUrlPlaceholder = {
    fb:      'https://facebook.com/<profile>',
    ig:      'https://instagram.com/<username>',
    tt:      'https://tiktok.com/@<username>',
    threads: 'https://threads.net/@<username>',
    line:    'https://line.me/ti/p/<id> หรือ LINE ID',
    ref:     'ลิงก์ที่เกี่ยวข้อง (ถ้ามี)',
    cold:    'ลิงก์ / เบอร์ติดต่อ (ถ้ามี)',
  }[source] || 'วาง link account ของ prospect';

  return (
    <Modal
      open={open}
      onClose={onClose}
      size="lg"
      title={
        <div className="flex items-center gap-2">
          <div className="w-7 h-7 rounded-md text-white font-bold text-xs flex items-center justify-center" style={{ background: type.color }}>{type.label}</div>
          <div>
            <div className="text-sm font-semibold text-ink-900">นัดหมายใหม่ · {type.full}</div>
            <div className="text-[11px] text-ink-400 num">{DAYS_FULL[draft.day]} · {formatTime(draft.start)}</div>
          </div>
        </div>
      }
      footer={
        <div className="flex items-center justify-between">
          <span className="text-[11px] text-ink-400 flex items-center gap-1.5"><Icon name="info" className="w-3.5 h-3.5" /> บันทึกใน Activity Log อัตโนมัติ</span>
          <div className="flex items-center gap-2">
            <Btn variant="ghost" size="sm" onClick={onClose}>ยกเลิก</Btn>
            <Btn variant="brand" size="sm" icon={<Icon name="check" className="w-3.5 h-3.5" />}
              disabled={!name.trim()}
              onClick={() => setConfirming(true)}>บันทึก</Btn>
          </div>
        </div>
      }
    >
      <div className="space-y-4">
        {isFU ? (
          /* === Follow-Up mode: pick who you're following up === */
          <div className="space-y-3 rounded-xl border border-cyan-200 bg-cyan-50/50 p-3">
            <div className="flex items-center gap-2">
              <Icon name="refresh" className="w-4 h-4 text-cyan-700" />
              <span className="text-xs font-semibold text-cyan-800">ติดตามใคร?</span>
            </div>
            <div className="grid grid-cols-2 gap-2">
              {[
                { id: 'dl', label: 'ติดตาม DL', desc: `${dlProspects.length} active`, icon: '💎', color: '#2F5DE5' },
                { id: '6w', label: 'ติดตาม 6W', desc: `${sixWCustomers.length} กำลังเรียน`, icon: '🥤', color: '#10B981' },
              ].map(opt => (
                <button key={opt.id} type="button"
                  onClick={() => { setFuTarget(opt.id); setFuTargetId(''); setName(''); }}
                  className="text-left p-2.5 rounded-lg border-2 transition-all"
                  style={fuTarget === opt.id
                    ? { borderColor: opt.color, background: opt.color + '12' }
                    : { borderColor: '#E6EAF3', background: 'white' }}>
                  <div className="flex items-center gap-1.5">
                    <span className="text-base leading-none">{opt.icon}</span>
                    <span className="text-xs font-bold" style={{ color: fuTarget === opt.id ? opt.color : '#0F1E38' }}>{opt.label}</span>
                  </div>
                  <div className="text-[10px] text-ink-500 mt-0.5">{opt.desc}</div>
                </button>
              ))}
            </div>

            <Field label={fuTarget === 'dl' ? 'เลือก DL · เฉพาะ active' : 'เลือก 6W · ลูกค้าที่เข้าคอร์ส'}>
              <select value={fuTargetId}
                onChange={e => {
                  // FU's prospect-name field is now a free-text "Topic"
                  // (see Field label below) — don't auto-fill it from the
                  // selected DL/customer. The DL info still flows via
                  // dl_prospect_id and appears as a "(DL: name)" pill on
                  // the calendar card.
                  setFuTargetId(e.target.value);
                }}
                className="input">
                <option value="">— เลือก{fuTarget === 'dl' ? ' DL' : 'ลูกค้า 6W'} —</option>
                {fuTarget === 'dl'
                  ? (dlProspects.length === 0
                      ? <option disabled>ยังไม่มี DL active ในหน้า ABO Tracking</option>
                      : dlProspects.map(p => (
                          <option key={p.id} value={p.id}>{p.name} · ABO {p.abo_code}</option>
                        )))
                  : (sixWCustomers.length === 0
                      ? <option disabled>ยังไม่มีลูกค้า 6W active</option>
                      : sixWCustomers.map(c => {
                          const isDL = c.abo_code && dlProspects.some(d => d.abo_code === c.abo_code);
                          return (
                            <option key={c.id} value={c.id}>
                              {c.name}{c.abo_code ? ` · ABO ${c.abo_code}` : ''}{isDL ? ' · 💎 DL' : ''}
                            </option>
                          );
                        }))
                }
              </select>
            </Field>
          </div>
        ) : (
          /* === Default mode: DL + Country row === */
          <div className="grid grid-cols-12 gap-3">
            <div className="col-span-7">
              <Field label={
                <div className="flex items-center gap-2">
                  <span>เจ้าของเคส</span>
                  {caseFor === 'self'
                    ? <Pill color="#B87C00" tint="#FFECB8" size="xs">★ {currentUser?.name || 'ฉัน'}</Pill>
                    : caseFor === 'partner'
                    ? <Pill color="#10B981" tint="#E7F8F1" size="xs">★ {currentUser?.partner_name || 'คู่ร่วมธุรกิจ'}</Pill>
                    : <Pill color="#2F5DE5" tint="#EEF4FF" size="xs">DL</Pill>}
                </div>
              }>
                <select value={caseFor} onChange={e => setCaseFor(e.target.value)} className="input">
                  <optgroup label="── ตัวเอง / คู่ร่วมธุรกิจ ──">
                    {defOwner === 'partner' && currentUser?.partner_name ? (
                      <>
                        <option value="partner">★ {currentUser.partner_name}</option>
                        <option value="self">★ {currentUser?.name || 'ฉัน'}{currentUser?.role ? ` (${currentUser.role})` : ''}</option>
                      </>
                    ) : (
                      <>
                        <option value="self">★ {currentUser?.name || 'ฉัน'}{currentUser?.role ? ` (${currentUser.role})` : ''}</option>
                        {currentUser?.partner_name && (
                          <option value="partner">★ {currentUser.partner_name}</option>
                        )}
                      </>
                    )}
                  </optgroup>
                  <optgroup label={`── Downline (DL · ${dlProspects.length}) ──`}>
                    {dlProspects.length === 0 ? (
                      <option disabled>— ยังไม่มี DL ที่มี ABO ในหน้า ABO Tracking —</option>
                    ) : (
                      dlProspects.map(p => (
                        <option key={p.id} value={p.id}>{p.name} · ABO {p.abo_code}</option>
                      ))
                    )}
                  </optgroup>
                </select>
              </Field>
            </div>
            <div className="col-span-5">
              <Field label="ประเทศ">
                <div className="grid grid-cols-4 gap-1">
                  {COUNTRIES.map(c => (
                    <button key={c.id} onClick={() => setCountry(c.id)}
                      title={c.label}
                      className={`flex flex-col items-center justify-center py-1.5 rounded-md border transition-all ${country === c.id ? 'border-brand-500 bg-brand-50' : 'bg-white border-ink-200 hover:border-ink-300'}`}>
                      <span className="text-lg leading-none">{c.flag}</span>
                      <span className={`text-[9px] mt-0.5 ${country === c.id ? 'text-brand-700 font-semibold' : 'text-ink-500'}`}>{c.label}</span>
                    </button>
                  ))}
                </div>
              </Field>
            </div>
          </div>
        )}

        <div className="grid grid-cols-12 gap-3">
          <div className={isFU ? 'col-span-9' : 'col-span-7'}>
            <Field label={isFU
              ? <span>หัวข้อ (Topic) <span className="text-rose-500">*</span></span>
              : <span>ชื่อ Prospect (Prospect Name) <span className="text-rose-500">*</span></span>}>
              <div className="relative">
                {igAvatar && !isFU && (
                  <img src={igAvatar} alt="" className="absolute left-2 top-1/2 -translate-y-1/2 w-6 h-6 rounded-full bg-white border border-ink-200" />
                )}
                <input autoFocus value={name} onChange={e => setName(e.target.value)}
                  placeholder={isFU ? 'เช่น คุยเรื่อง detox · ตามผล Boarding Pack · นัดส่งสินค้า' : 'เช่น คุณวริศรา ม.'}
                  className={`input ${igAvatar && !isFU ? 'pl-10' : ''}`} />
              </div>
            </Field>
          </div>
          {!isFU && (
            <div className="col-span-2">
              <Field label="อายุ">
                <input
                  type="number"
                  inputMode="numeric"
                  min="0"
                  max="130"
                  value={age}
                  onChange={e => setAge(e.target.value.replace(/[^0-9]/g, ''))}
                  placeholder="—"
                  className="input num text-center"
                />
              </Field>
            </div>
          )}
          <div className="col-span-3">
            <Field label="ระยะเวลา">
              <select value={dur} onChange={e => setDur(parseFloat(e.target.value))} className="input">
                <option value={0.5}>30 นาที</option>
                <option value={0.75}>45 นาที</option>
                <option value={1}>1 ชม.</option>
                <option value={1.5}>1.5 ชม.</option>
                <option value={2}>2 ชม.</option>
              </select>
            </Field>
          </div>
        </div>

        {!isFU && (
          <Field label="แหล่งที่มา (Lead Source)">
            <div className="grid grid-cols-3 gap-2">
              {leadSources.map(s => (
                <button key={s.id} onClick={() => setSource(s.id)}
                  className={`flex items-center gap-2 p-2 rounded-lg border text-xs font-medium transition-all ${source === s.id ? 'border-transparent text-white' : 'bg-white border-ink-200 text-ink-700 hover:border-ink-300'}`}
                  style={source === s.id ? { background: s.color } : {}}>
                  <div className="w-2 h-2 rounded-full" style={{ background: source === s.id ? 'white' : s.color }} />
                  {s.label}
                </button>
              ))}
            </div>
          </Field>
        )}

        {/* Account / link of the prospect on the selected source */}
        {!isFU && (
          <div
            className="rounded-xl border p-3 space-y-2.5"
            style={{
              borderColor: (sourceObj?.color || '#C9D2E3') + '55',
              background: (sourceObj?.color || '#F4F6FB') + '0D',
            }}
          >
            <div className="flex items-center gap-2">
              <span className="w-2.5 h-2.5 rounded-full" style={{ background: sourceObj?.color || '#94A3B8' }} />
              <span className="text-[11px] font-semibold text-ink-700">
                Link / Account ของ Prospect บน {sourceObj?.label || 'แหล่งที่มา'}
              </span>
              {isIG && igHandleFromUrl && (
                <span className="text-[10px] text-pink-500/80 ml-auto">ตรวจพบ @{igHandleFromUrl}</span>
              )}
            </div>
            <input
              type="url"
              value={sourceUrl}
              onChange={e => setSourceUrl(e.target.value)}
              placeholder={sourceUrlPlaceholder}
              className="input"
              inputMode="url"
            />
            {isIG && igAvatar && (
              <div className="flex items-center gap-3 p-2 bg-white rounded-lg border border-pink-100">
                <img src={igAvatar} alt="" className="w-10 h-10 rounded-full" />
                <div className="flex-1 min-w-0">
                  <div className="text-xs font-semibold text-ink-900">@{igHandleFromUrl}</div>
                  <div className="text-[10px] text-ink-400">Avatar preview · ดึงจาก handle</div>
                </div>
                <Pill color="#10B981" tint="#E7F8F1" size="xs" icon={<Icon name="check" className="w-3 h-3" />}>OK</Pill>
              </div>
            )}
          </div>
        )}

        {/* LINE group invite — user pastes the link manually from LINE app
            (group → ⋯ → Invite → Copy link). The app then exposes a
            "เปิดกลุ่ม LINE" deep-link on the edit modal. LINE does not
            expose group membership via any API, so this has to be
            user-entered. Hidden for FU since FU appts don't have
            their own prospect. */}
        {!isFU && (
          <Field label={
            <div className="flex items-center gap-1.5">
              <span className="w-4 h-4 rounded-sm flex items-center justify-center" style={{ background: '#06C755' }}>
                <Icon name="line" className="w-3 h-3 text-white" />
              </span>
              <span>กลุ่ม LINE กับ Prospect <span className="text-ink-400 font-normal">(ถ้ามี)</span></span>
            </div>
          }>
            <input
              type="url"
              value={lineGroupUrl}
              onChange={e => setLineGroupUrl(e.target.value)}
              placeholder="https://line.me/R/ti/g/..."
              className="input"
              inputMode="url"
            />
          </Field>
        )}

        <Field label="โน้ตเพิ่มเติม (ถ้ามี)">
          <textarea value={notes} onChange={e => setNotes(e.target.value)} rows={2}
            placeholder="เป้าหมาย / รายละเอียดเพิ่มเติม" className="input resize-none" />
        </Field>
      </div>
      <style>{`
        .input {
          width: 100%; padding: 8px 12px; font-size: 13px;
          border: 1px solid #C9D2E3; border-radius: 8px; background: white;
          outline: none; transition: all .15s; color: #0F1E38;
        }
        .input:focus { border-color: #2F5DE5; box-shadow: 0 0 0 3px rgba(47,93,229,0.15); }
      `}</style>

      <ConfirmDialog
        open={confirming}
        onCancel={() => setConfirming(false)}
        onConfirm={() => {
          setConfirming(false);
          // Resolve caseFor → (dlId, dlProspectId, isPartner).
          //   'self'     → owner = current user, no DL
          //   'partner'  → owner = current user, isPartner = true
          //   <UUID>     → that prospect (from ABO Tracking) is the DL
          //
          // FU mode short-circuits the resolution:
          //   target 'dl' → fuTargetId is a DL prospect → treat as DL case
          //   target '6w' → 6W customer → personal follow-up (self)
          let resolvedDlId, resolvedDlProspectId, isPartner;
          if (isFU) {
            isPartner = false;
            if (fuTarget === 'dl' && fuTargetId) {
              resolvedDlId = null;
              resolvedDlProspectId = fuTargetId;
            } else {
              resolvedDlId = currentUser?.id;
              resolvedDlProspectId = null;
            }
          } else {
            isPartner = caseFor === 'partner';
            const isSelf = caseFor === 'self' || caseFor === 'partner';
            resolvedDlId = isSelf ? currentUser?.id : null;
            resolvedDlProspectId = isSelf ? null : caseFor;
          }
          onSave({
            prospect: name.trim(),
            source, status, notes,
            dur,  // forward the user's chosen duration so saveDraft persists
                  // it instead of falling back to the type's default. The
                  // <select> binds dur to setDur(parseFloat(value)).
            dlId: resolvedDlId, dlProspectId: resolvedDlProspectId, isPartner, country,
            age: isFU ? null : (age.trim() === '' ? null : Math.max(0, Math.min(130, parseInt(age, 10) || 0))),
            sourceUrl: isFU ? null : (sourceUrl.trim() || null),
            lineGroupUrl: isFU ? null : (lineGroupUrl.trim() || null),
            igHandle: isFU ? null : (isIG ? igHandleFromUrl : null),
            igAvatar: isFU ? null : (isIG ? igAvatar : null),
          });
        }}
        message={
          <span>
            บันทึกนัด <b>{type?.label}</b> สำหรับ <b>{name.trim() || '—'}</b> วัน{DAYS_FULL[draft.day]} เวลา {formatTime(draft.start)} ใช่หรือไม่?
          </span>
        }
      />
    </Modal>
  );
}

function EditApptModal({ open, onClose, appt, appts = [], onSave, onDelete, onCreateAppt, currentUser, dlProspects = [], onNavigate }) {
  const { apptTypes, leadSources } = useConfig();
  const [notes, setNotes] = useState('');
  const [outcome, setOutcome] = useState(null); // 'bi' | 'skip' | 'postpone' | 'parked' | null
  // "ของไว้รอติดตาม" (6W only) — date the user wants to be reminded. Default
  // to 7 days from today. The daily 7am LINE push picks this up when
  // remind_on = today.
  const [parkedDate, setParkedDate] = useState(() => {
    const d = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
    return d.toISOString().slice(0, 10); // YYYY-MM-DD
  });
  // When outcome = 'bi' (ผ่าน), let the user choose whether the next
  // appointment is the natural next Follow-Up step (BI/S3/...) OR a 6W weight-
  // tracking appointment. Either choice also opts the prospect into ABO
  // Tracking (Follow-Up Sheet) by setting fu_track on the prospects row.
  const [nextTypeChoice, setNextTypeChoice] = useState('flow'); // 'flow' | '6W'
  // When "เคสปิดสำเร็จ" (outcome=bi + no nextStep), capture the ABO so we
  // can spin up a customer row in 6W Tracking automatically.
  const [closingAbo, setClosingAbo] = useState('');
  const [nextDay, setNextDay] = useState(4);
  const [nextStart, setNextStart] = useState(10);
  const [sendBoarding, setSendBoarding] = useState(true);
  const [copied, setCopied] = useState(false);
  // Each item: { url (preview), file? (pending upload), id?/path? (saved) }
  const [photos, setPhotos] = useState([]);
  const [loadingPhotos, setLoadingPhotos] = useState(false);
  const [confirming, setConfirming] = useState(false);
  // Editable prospect name. Persisted on save to both the appt row and the
  // linked prospects row (when prospect_id exists).
  const [prospectName, setProspectName] = useState('');
  // Editable LINE group invite URL — pasted by the user. Deep-links into
  // the LINE app on click via `line.me/R/ti/g/...`.
  const [lineGroupUrl, setLineGroupUrl] = useState('');
  // Editable schedule — toggles an inline DateTimePicker so the user can
  // shift this appt without dragging on the calendar grid.
  const [editingSchedule, setEditingSchedule] = useState(false);
  // Case-owner edit — same semantics as ProspectModal's caseFor:
  //   'self'    → owner = current user, no DL
  //   'partner' → owner = current user, isPartner = true
  //   <UUID>    → DL prospect id
  const [caseFor, setCaseFor] = useState('self');
  const [editDay, setEditDay] = useState(0);
  const [editStart, setEditStart] = useState(10);
  const [editDur, setEditDur] = useState(1);
  const fileInputRef = useRef(null);

  useEffect(() => {
    if (appt) {
      setNotes(appt.notes || '');
      setOutcome(null);
      setEditingSchedule(false);
      setEditDay(appt.day ?? 0);
      setEditStart(appt.start ?? 10);
      setEditDur(appt.dur ?? 1);
      // Default "ผ่าน → ต่อ" / "เลื่อน" to appt.day + 2, but never earlier
      // than today. We can't book in the past.
      const todayIdx = window.TODAY_DAY_IDX ?? -1;
      const base = Math.max(appt.day || 0, todayIdx >= 0 ? todayIdx : 0);
      setNextDay(Math.min(27, base + 2));
      // Same idea for start: if the proposed day is today, push the time
      // past "now" (rounded to the next 30-min slot) instead of reusing
      // the original (possibly already-elapsed) start.
      const now = new Date();
      const nowSlot = Math.ceil((now.getHours() + now.getMinutes() / 60) * 2) / 2;
      const proposedDay = Math.min(27, base + 2);
      const proposedStart = (todayIdx >= 0 && proposedDay === todayIdx)
        ? Math.max(appt.start || 10, nowSlot)
        : (appt.start || 10);
      setNextStart(proposedStart);
      setSendBoarding(true);
      // Load already-saved photos for this appt from storage.
      setPhotos([]);
      if (window.SB_READY && window.db?.photos?.listForAppt && appt.id && !String(appt.id).startsWith('new-')) {
        setLoadingPhotos(true);
        window.db.photos.listForAppt(appt.id).then(rows => {
          setPhotos((rows || []).map(r => ({ id: r.id, path: r.storage_path, url: window.db.photos.url(r.storage_path) })));
        }).finally(() => setLoadingPhotos(false));
      }
      setConfirming(false);
      setProspectName(appt.prospect || '');
      setLineGroupUrl(appt.lineGroupUrl || '');
      // Hydrate case-owner state from the appt: a dlProspectId wins, then
      // isPartner, else default to "self".
      setCaseFor(
        appt.dlProspectId
          ? appt.dlProspectId
          : appt.isPartner
            ? 'partner'
            : 'self'
      );
    }
  }, [appt]);

  // NOTE: every hook MUST run on every render — keep useMemo above the early
  // return so React's hook-order invariant holds when this modal mounts with
  // appt=null and later receives a real appointment.
  const currentStep = appt ? (window.FOLLOW_UP_STEPS.find(s => s.type === appt.type) || null) : null;
  const nextStep    = currentStep ? window.FOLLOW_UP_STEPS.find(s => s.step === currentStep.step + 1) : null;
  // Resolve next appointment type from the user's choice:
  //   'flow' → continue down the Follow-Up sequence (nextStep.type, e.g. BI)
  //   '6W'   → branch into the weight-tracking flow
  const resolvedNextType = outcome === 'bi'
    ? (nextTypeChoice === '6W' ? '6W' : (nextStep?.type || appt?.type))
    : appt?.type;
  const nextType    = resolvedNextType;
  const nextDur     = (apptTypes.find(t => t.id === nextType)?.dur || 60) / 60;

  // Find overlapping appointments at (nextDay, nextStart, nextDur) — excluding current appt being edited
  const conflicts = useMemo(() => {
    if (!appt || !outcome || outcome === 'skip') return [];
    return appts.filter(a =>
      a.id !== appt.id &&
      a.status !== 'cancelled' &&
      // Class / Event / central-Class are calendar blocks, not cases — they
      // never count as a conflict.
      !['EV', 'CS', 'CL'].includes(a.type) &&
      a.day === nextDay &&
      a.start < nextStart + nextDur &&
      nextStart < a.start + a.dur
    );
  }, [appts, appt?.id, outcome, nextDay, nextStart, nextDur]);

  if (!appt) return null;
  const type = apptTypes.find(t => t.id === appt.type) || { label: appt.type || '?', color: '#94A3B8', tint: '#F1F5F9', full: appt.type || '—' };
  const isFU = appt.type === 'FU';
  // noTrack types (MT / EV / CS) are simple calendar blocks — no follow-up
  // outcome, no prospect tracking, just title + note.
  const isNoTrack = !!type.noTrack || ['MT', 'EV', 'CS'].includes(appt.type);
  const src = leadSources.find(s => s.id === appt.source) || { label: '—', color: '#94A3B8' };
  const country = window.COUNTRIES.find(c => c.id === (appt.country || 'th'));
  // Case owner: prefer the ABO Tracking prospect (new model), then the legacy
  // users.ABO_LIST entry, then fall back to the appointment's owner (the
  // current user — meaning "เคสของตัวเอง" / "คู่ร่วมธุรกิจ").
  const dlProspect = appt.dlProspectId ? dlProspects.find(p => p.id === appt.dlProspectId) : null;
  const dl = dlProspect
    ? { id: dlProspect.id, name: dlProspect.name, role: dlProspect.abo_code ? `ABO ${dlProspect.abo_code}` : '—', color: '#10B981', avatar: dlProspect.name?.charAt(0) || 'ค' }
    : window.ABO_LIST.find(u => u.id === (appt.dlId || appt.owner));

  // 'track' marks the BM as completed (it happened) — only the next appt
  // is missing, which is exactly what ABO Tracking is for.
  const STATUS_BY_OUTCOME = { bi: 'completed', skip: 'cancelled', postpone: 'followup', parked: 'cancelled', track: 'completed', bookbm: 'completed' };
  // "ของไว้รอติดตาม" is only meaningful on 6W appts; "ยังไม่ได้นัดต่อ" on BM.
  const isSixW = appt?.type === '6W';
  const isBM = appt?.type === 'BM';
  const isBI = appt?.type === 'BI';
  // BM + BI both get the "ยังไม่ได้นัดต่อ · รอติดตาม" outcome.
  const isTrackable = isBM || isBI;

  const handlePhotoUpload = (e) => {
    const files = Array.from(e.target.files || []);
    files.forEach(f => {
      const reader = new FileReader();
      reader.onload = () => setPhotos(prev => [...prev, { file: f, url: reader.result }]);
      reader.readAsDataURL(f);
    });
    e.target.value = '';
  };
  const removePhoto = (i) => {
    const p = photos[i];
    setPhotos(prev => prev.filter((_, idx) => idx !== i));
    // Delete an already-saved photo from storage + table.
    if (p?.id && window.SB_READY && window.db?.photos?.remove) {
      window.db.photos.remove(p.id, p.path).catch(() => {});
    }
  };

  const handleSave = () => {
    const newStatus = outcome ? STATUS_BY_OUTCOME[outcome] : appt.status;
    const trimmedName = (prospectName || '').trim();
    const nameChanged = trimmedName && trimmedName !== (appt.prospect || '').trim();
    // Include schedule patches when the user touched the inline picker.
    const schedulePatch = scheduleChanged
      ? { day: editDay, start: editStart, dur: editDur }
      : {};
    // Resolve case-owner edits the same way ProspectModal does:
    //   'self'    → owner = current user, no DL, isPartner = false
    //   'partner' → owner = current user, isPartner = true
    //   <UUID>    → that prospect's row IS the DL (dl_prospect_id)
    let ownerPatch = {};
    if (ownerChanged) {
      if (caseFor === 'self') {
        ownerPatch = { dlId: currentUser?.id, dlProspectId: null, isPartner: false };
      } else if (caseFor === 'partner') {
        ownerPatch = { dlId: currentUser?.id, dlProspectId: null, isPartner: true };
      } else {
        const picked = dlProspects.find(p => p.id === caseFor);
        ownerPatch = {
          dlId: null,
          dlProspectId: caseFor,
          isPartner: false,
          dlName: picked?.name || null,
        };
      }
    }
    // noTrack types (MT/EV/CS) have no prospect row — their display name is the
    // first line of `notes`. Persist a renamed title back into notes so it
    // actually saves to the appointment.
    let notesToSave = notes;
    if (isNoTrack && nameChanged) {
      const rest = (notes || '').split('\n').slice(1).join('\n');
      notesToSave = [trimmedName, rest].filter(Boolean).join('\n');
    }
    // Upload any pending photo files to storage for this appt (fire-and-forget;
    // they're linked to appointment_photos by appt id).
    const pendingFiles = photos.filter(p => p.file);
    if (pendingFiles.length && window.SB_READY && window.db?.photos?.upload && appt.id && !String(appt.id).startsWith('new-')) {
      (async () => {
        for (const p of pendingFiles) {
          const { error } = await window.db.photos.upload(appt.id, p.file);
          if (error) console.warn('[Calendar] photo upload failed:', error.message);
        }
      })();
    }

    // Postpone replaces the old appt with a new one — don't bother patching the
    // old row's status; it's deleted below in the postpone branch.
    if (outcome !== 'postpone') {
      onSave({
        status: newStatus, notes: notesToSave, outcome,
        prospect: trimmedName || appt.prospect,
        ...schedulePatch,
        ...ownerPatch,
      });
    }

    // Persist edits to the linked prospects row — name + LINE group URL.
    // Both shapes flow through window.db.prospects.update so changes show up
    // everywhere (ABO Tracking, future appointment embeds, etc.).
    const newLineUrl = lineGroupUrl.trim() || null;
    const lineGroupChanged = newLineUrl !== (appt.lineGroupUrl || null);
    if ((nameChanged || lineGroupChanged) && window.SB_READY && appt.prospect_id) {
      const patch = {};
      if (nameChanged)      patch.name = trimmedName;
      if (lineGroupChanged) patch.line_group_url = newLineUrl;
      (async () => {
        const { error } = await window.db.prospects.update(appt.prospect_id, patch);
        if (error) console.warn('[Calendar] prospect update failed:', error.message);
      })();
    }
    // "เคสปิดสำเร็จ" → spawn a 6W Tracking customer with the entered ABO.
    // Fires whenever outcome=bi and there's no nextStep in the flow (i.e.,
    // 6W appointments or the very last step). The new customer's Before
    // measurement is seeded from today + the appointment date label.
    if (outcome === 'bi' && !nextStep) {
      if (window.SB_READY && currentUser?.id) {
        const todayStr = (() => {
          const d = new Date();
          const dd = String(d.getDate()).padStart(2, '0');
          const mm = String(d.getMonth() + 1).padStart(2, '0');
          const yy = d.getFullYear() + 543;
          return `${dd}/${mm}/${yy}`;
        })();
        const measurements = { em6w: { Before: { date: todayStr, uu: '' } }, em13w: {} };
        const payload = {
          owner_id: currentUser.id,
          name: appt.prospect || 'ลูกค้าใหม่',
          abo_code: closingAbo && closingAbo.length === 10 ? closingAbo : null,
          status: 'active',
          measurements,
          products: {},
          body_photos: {},
          follow_up_log: [],
        };
        (async () => {
          const { error: cErr } = await window.db.customers.create(payload);
          if (cErr) console.warn('[Calendar] 6W customer create failed:', cErr.message);
        })();
      }
    }

    // "ผ่าน" → create the next appointment AND add the prospect to ABO Tracking.
    if (outcome === 'bi' && (nextStep || nextTypeChoice === '6W')) {
      const goingTo6W = nextTypeChoice === '6W';
      const nextApptType = goingTo6W ? '6W' : nextStep?.type;
      const fuTrack = goingTo6W ? 'EM6W' : 'Sponsor';
      onCreateAppt({
        day: nextDay, start: nextStart, type: nextApptType,
        prospect: appt.prospect, source: appt.source,
        dlId: appt.dlId, dlProspectId: appt.dlProspectId || null, isPartner: !!appt.isPartner, country: appt.country,
        igHandle: appt.igHandle, igAvatar: appt.igAvatar, sourceUrl: appt.sourceUrl, age: appt.age,
        step: goingTo6W ? null : nextStep?.step,
        notes: sendBoarding && currentStep?.link ? `📦 ส่ง ${currentStep.link.label} แล้ว · ต่อจาก Step ${currentStep?.step || '?'}` : `ต่อจาก Step ${currentStep?.step || ''}`,
        prospect_id: appt.prospect_id,
      });

      // Auto-add the prospect to ABO Tracking AND back-fill follow-up flags
      // for everything they've completed up to (and including) this step.
      // Mapping appt type → fu_state flags:
      //   BM → bmodel
      //   BI → bmodel + bincome (BM is implied)
      //   6W → em6w='enrolled'
      //   S3 → bmodel + bincome + 2yr
      const flagsByType = {
        'BM': { bmodel: true },
        'BI': { bmodel: true, bincome: true },
        '6W': { em6w: 'enrolled' },
        'S3': { bmodel: true, bincome: true, '2yr': true },
      };
      const newFlags = flagsByType[appt.type] || {};

      if (window.SB_READY && appt.prospect_id) {
        (async () => {
          try {
            // Read the current fu_state so we don't clobber other flags.
            const { data: cur, error: readErr } = await window.supabase
              .from('prospects')
              .select('fu_state, fu_track')
              .eq('id', appt.prospect_id)
              .single();
            if (readErr) { console.warn('[Calendar] fu_state read failed:', readErr.message); return; }
            const mergedState = { ...(cur?.fu_state || {}), ...newFlags };
            const patch = { fu_state: mergedState };
            // Only set fu_track if it isn't already set (don't overwrite an
            // EM6W tag with Sponsor when the user later passes a Sponsor step)
            if (!cur?.fu_track) patch.fu_track = fuTrack;
            const { error: updErr } = await window.db.prospects.update(appt.prospect_id, patch);
            if (updErr) console.warn('[Calendar] prospect update failed:', updErr.message);
          } catch (e) {
            console.warn('[Calendar] prospect update threw:', e);
          }
        })();
      }
    } else if (outcome === 'postpone') {
      // Create the new (rescheduled) appt, then remove the old one so it
      // doesn't linger as a duplicate.
      onCreateAppt({
        day: nextDay, start: nextStart, type: appt.type,
        prospect: appt.prospect, source: appt.source,
        dlId: appt.dlId, dlProspectId: appt.dlProspectId || null, isPartner: !!appt.isPartner, country: appt.country,
        igHandle: appt.igHandle, igAvatar: appt.igAvatar, sourceUrl: appt.sourceUrl, age: appt.age,
        step: currentStep?.step,
        notes: `เลื่อนจากนัด ${type.label} เดิม`,
      });
      if (onDelete) onDelete();
    } else if (outcome === 'bookbm') {
      // Book a follow-on BM for this prospect (current appt marked completed
      // by STATUS_BY_OUTCOME). Carries owner/source/contact over.
      onCreateAppt({
        day: nextDay, start: nextStart, type: 'BM',
        prospect: appt.prospect, source: appt.source,
        dlId: appt.dlId, dlProspectId: appt.dlProspectId || null, isPartner: !!appt.isPartner, country: appt.country,
        igHandle: appt.igHandle, igAvatar: appt.igAvatar, sourceUrl: appt.sourceUrl, age: appt.age,
        step: 1,
        notes: `นัด BM ต่อจาก ${type.label}`,
      });
    } else if (outcome === 'parked') {
      // "ของไว้รอติดตาม" — insert a parked_follow_up row pointing at this
      // appt + the picked remind_on date. The daily 7am LINE push joins on
      // this table where remind_on = today.
      if (window.SB_READY && currentUser?.id) {
        (async () => {
          const row = {
            owner_id: currentUser.id,
            prospect_id: appt.prospect_id || null,
            source_appt_id: String(appt.id).startsWith('new-') ? null : appt.id,
            customer_name: appt.prospect || '—',
            remind_on: parkedDate,
            notes: notes?.trim() || null,
            status: 'pending',
            kind: '6w',
          };
          const { error } = await window.db.parkedFollowUps.create(row);
          if (error) {
            console.warn('[Calendar] parked_follow_up create failed:', error.message);
            alert('เพิ่มลิสต์ติดตามไม่สำเร็จ: ' + error.message);
          } else {
            // Surface success via a window event so other views can refresh.
            window.dispatchEvent(new CustomEvent('emphasis:parked-follow-up-created', { detail: row }));
          }
        })();
      }
    } else if (outcome === 'track') {
      // "ยังไม่ได้นัดต่อ · รอติดตาม" — the session happened but no next appt
      // was booked. Just drop a "รอติดตาม" entry (NO ABO enrolment, no nav).
      if (window.SB_READY && currentUser?.id) {
        (async () => {
          const today = new Date(Date.now() + 7 * 3600 * 1000).toISOString().slice(0, 10);
          const row = {
            owner_id: currentUser.id,
            prospect_id: appt.prospect_id || null,
            source_appt_id: String(appt.id).startsWith('new-') ? null : appt.id,
            customer_name: appt.prospect || '—',
            remind_on: today,
            notes: notes?.trim() || `${type.label} แล้ว · ยังไม่ได้นัดต่อ`,
            status: 'pending',
            kind: 'bm',
          };
          const { error } = await window.db.parkedFollowUps.create(row);
          if (!error) window.dispatchEvent(new CustomEvent('emphasis:parked-follow-up-created', { detail: row }));
          else console.warn('[Calendar] track parked create failed:', error.message);
        })();
      }
    }
  };

  const copyLink = async (url) => {
    try {
      await navigator.clipboard.writeText(url);
      setCopied(true);
      setTimeout(() => setCopied(false), 1500);
    } catch {}
  };

  // Save is allowed when any of these is true:
  //   - an outcome is picked (ผ่าน / ยังไม่สนใจ / เลื่อน)
  //   - it's an FU appt (notes-only is fine)
  //   - the user edited notes / prospect name / case owner
  //   - the user edited the schedule (day / time / duration)
  const notesChanged   = (notes || '') !== (appt?.notes || '');
  const nameChanged    = (prospectName || '').trim() !== (appt?.prospect || '').trim() && !!prospectName.trim();
  const scheduleChanged = appt && (
    editDay !== appt.day ||
    Math.abs(editStart - appt.start) > 0.001 ||
    Math.abs(editDur - appt.dur) > 0.001
  );
  const originalCaseFor = appt
    ? (appt.dlProspectId || (appt.isPartner ? 'partner' : 'self'))
    : 'self';
  const ownerChanged = appt && caseFor !== originalCaseFor;
  const lineGroupDirty = (lineGroupUrl || '').trim() !== (appt?.lineGroupUrl || '').trim();
  const canSave = isFU || outcome !== null || notesChanged || nameChanged || scheduleChanged || ownerChanged || lineGroupDirty;

  return (
    <Modal open={open} onClose={onClose} size="lg" title={
      <div className="flex items-center gap-2">
        <div className="w-7 h-7 rounded-md text-white font-bold text-xs flex items-center justify-center" style={{ background: type.color }}>{type.label}</div>
        <div>
          <div className="flex items-center gap-2">
            {appt.igAvatar && <img src={appt.igAvatar} alt="" className="w-5 h-5 rounded-full border border-ink-200" />}
            <input
              value={prospectName}
              onChange={(e) => setProspectName(e.target.value)}
              placeholder={isFU ? 'หัวข้อ (Topic)' : 'ชื่อ Prospect'}
              title={isFU ? 'คลิกเพื่อแก้ไขหัวข้อ' : 'คลิกเพื่อแก้ไขชื่อ'}
              className="text-sm font-semibold text-ink-900 bg-transparent outline-none border-b border-transparent hover:border-ink-200 focus:border-brand-500 px-0.5 min-w-0"
              style={{ width: `${Math.max(8, Math.min(28, (prospectName || '').length + 2))}ch` }}
            />
            {country && <span className="text-base leading-none" title={country.label}>{country.flag}</span>}
          </div>
          <div className="text-[11px] text-ink-400 num flex items-center gap-1 flex-wrap">
            <span>
              {formatDayOffset(editDay)} · {formatTime(editStart)} ({Math.round(editDur*60)} นาที)
            </span>
            <button type="button"
              onClick={() => setEditingSchedule(s => !s)}
              className={`ml-1 inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[10px] font-semibold transition-all ${
                editingSchedule
                  ? 'bg-brand-100 text-brand-700'
                  : 'text-brand-600 hover:bg-brand-50'
              }`}
              title="แก้ไขวัน/เวลา">
              <svg className="w-2.5 h-2.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.75"><path d="M11 2l3 3-9 9H2v-3l9-9z" strokeLinejoin="round"/></svg>
              แก้ไขเวลา
            </button>
            {scheduleChanged && !editingSchedule && (
              <Pill color="#B87C00" tint="#FFECB8" size="xs">แก้แล้ว · กดบันทึก</Pill>
            )}
            {dl
              ? (dl.id === currentUser?.id
                  ? (appt.isPartner
                      ? <span>· ★ {currentUser?.partner_name || 'คู่ร่วมธุรกิจ'}</span>
                      : <span>· ★ {currentUser?.name || 'ฉัน'}</span>)
                  : <span>· DL: {dl.name}</span>)
              : null}
          </div>
        </div>
      </div>
    } footer={
      <div className="flex items-center justify-between">
        <button onClick={onDelete} className="text-xs font-medium text-rose-500 hover:text-rose-700">ลบนัดนี้</button>
        <div className="flex items-center gap-2">
          {!outcome && !isFU && !notesChanged && !nameChanged && !scheduleChanged && !ownerChanged && !lineGroupDirty && (
            <span className="text-[11px] text-gold-600 font-medium">เลือกผลการนัด หรือแก้ไขข้อมูล ↑</span>
          )}
          <Btn variant="ghost" size="sm" onClick={onClose}>ยกเลิก</Btn>
          <Btn variant="brand" size="sm" onClick={() => setConfirming(true)} disabled={!canSave}
               icon={<Icon name="check" className="w-3.5 h-3.5" />}>บันทึก</Btn>
        </div>
      </div>
    }>
      <div className="space-y-4">
        {/* Inline schedule editor — toggled by the "แก้ไขเวลา" chip in the
            header. Same DateTimePicker the "ผ่าน/เลื่อน" flows use; reuses
            the past-day/past-time guards. */}
        {editingSchedule && (
          <div className="rounded-xl border-2 border-brand-200 bg-brand-50/40 p-3 space-y-3">
            <div className="flex items-center justify-between gap-2">
              <div className="flex items-center gap-1.5 text-xs font-semibold text-brand-800">
                <svg className="w-3.5 h-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.75"><path d="M11 2l3 3-9 9H2v-3l9-9z" strokeLinejoin="round"/></svg>
                เลือกวัน / เวลาใหม่
              </div>
              <button onClick={() => {
                  setEditDay(appt.day ?? 0);
                  setEditStart(appt.start ?? 10);
                  setEditDur(appt.dur ?? 1);
                  setEditingSchedule(false);
                }}
                className="text-[10px] text-ink-500 hover:text-ink-800 underline">
                ยกเลิกการแก้ไข
              </button>
            </div>
            <DateTimePicker
              day={editDay}
              start={editStart}
              onDay={setEditDay}
              onStart={setEditStart}
              color={type.color}
            />
            <div>
              <div className="text-[10px] font-medium text-ink-500 mb-1.5 uppercase tracking-wide">ระยะเวลา</div>
              <div className="flex items-center gap-1.5 flex-wrap">
                {[0.5, 0.75, 1, 1.5, 2].map(d => (
                  <button key={d}
                    onClick={() => setEditDur(d)}
                    className="px-2.5 py-1 rounded-md text-[11px] font-medium border-2 transition-all"
                    style={Math.abs(editDur - d) < 0.001
                      ? { borderColor: type.color, background: type.color + '15', color: type.color }
                      : { borderColor: '#E6EAF3', background: 'white', color: '#445576' }}>
                    {d < 1 ? `${d * 60} นาที` : `${d} ชม.`}
                  </button>
                ))}
              </div>
            </div>
            <div className="text-[10px] text-ink-500 leading-relaxed">
              💡 เปลี่ยนเวลานัดแล้ว ระบบจะ reset การเตือนล่วงหน้า 30 นาที — ส่ง LINE เตือนใหม่ตามเวลาที่แก้ไข
            </div>
          </div>
        )}

        {/* Source / DL bar
            For FU appts the lead-source slot is repurposed into a clickable
            shortcut: ติดตาม DL → ABO Tracking row, ติดตาม 6W → 6W Tracking
            customer. (Plain appts keep the original source + sourceUrl line.)
        */}
        <div className="flex items-center gap-3 p-3 rounded-lg bg-ink-50 flex-wrap">
          <div className="flex items-center gap-1.5 min-w-0">
            {isFU ? (() => {
              const fuKind = appt.dlProspectId ? 'dl' : '6w';
              const isDl = fuKind === 'dl';
              const handleJump = () => {
                // For DL case the appt's "prospect" is now the FU topic
                // (e.g. "ตามผล Boarding Pack"), not the DL name. Use
                // appt.dlName to drive the destination view's filter.
                const focusName = isDl
                  ? (appt.dlName || appt.prospect || '')
                  : (appt.prospect || '');
                const detail = {
                  kind: fuKind,
                  prospectId: appt.dlProspectId || null,
                  name: focusName,
                };
                // Persistent stash so the destination view picks it up even
                // when it mounts AFTER navigate (event listeners that don't
                // exist yet miss the dispatch).
                window.__emphasisPendingFocus = detail;
                window.dispatchEvent(new CustomEvent('emphasis:focus-followup', { detail }));
                onClose?.();
                onNavigate?.(isDl ? 'abo' : '6w');
              };
              return (
                <button onClick={handleJump}
                  className="inline-flex items-center gap-1.5 text-xs font-semibold px-2 py-1 rounded-md transition-all hover:shadow-card"
                  style={isDl
                    ? { background: '#EEF4FF', color: '#2F5DE5', border: '1px solid #2F5DE540' }
                    : { background: '#E7F8F1', color: '#0F8B5E', border: '1px solid #10B98140' }}
                  title={isDl ? 'เปิดในหน้า ABO Tracking' : 'เปิดในหน้า 6W Tracking'}>
                  <span className="text-sm leading-none">{isDl ? '💎' : '🥤'}</span>
                  <span>{isDl ? 'ดูใน ABO Tracking' : 'ดูใน 6W Tracking'}</span>
                  <Icon name="arrowR" className="w-3 h-3" />
                </button>
              );
            })() : (() => {
              // Resolve the best URL to open: explicit sourceUrl wins,
              // else derive from igHandle (IG-only). Make the source
              // pill + the @handle both clickable when we have one.
              const profileUrl = appt.sourceUrl
                ? appt.sourceUrl
                : (appt.igHandle ? `https://instagram.com/${appt.igHandle}` : null);
              const SourceLabel = profileUrl ? 'a' : 'span';
              const labelProps = profileUrl
                ? { href: profileUrl, target: '_blank', rel: 'noopener noreferrer', title: 'เปิด ' + (src.label || 'profile') }
                : {};
              return (
                <>
                  <div className="w-2 h-2 rounded-full flex-shrink-0" style={{ background: src.color }} />
                  <SourceLabel {...labelProps}
                    className={`text-xs ${profileUrl ? 'underline font-medium' : 'text-ink-700'}`}
                    style={profileUrl ? { color: src.color } : {}}>
                    {src.label}
                  </SourceLabel>
                  {appt.igHandle && (
                    profileUrl ? (
                      <a href={profileUrl} target="_blank" rel="noopener noreferrer"
                         className="text-xs text-pink-600 font-medium underline truncate max-w-[180px]"
                         title={`เปิด @${appt.igHandle}`}>
                        @{appt.igHandle}
                      </a>
                    ) : (
                      <span className="text-xs text-pink-600 font-medium">@{appt.igHandle}</span>
                    )
                  )}
                  {profileUrl && (
                    <a href={profileUrl} target="_blank" rel="noopener noreferrer"
                       className="text-[11px] font-medium inline-flex items-center gap-0.5 flex-shrink-0"
                       style={{ color: src.color }}
                       title={profileUrl}>
                      <Icon name="arrowR" className="w-3 h-3" />
                      เปิด
                    </a>
                  )}
                </>
              );
            })()}
          </div>
          {/* Editable case-owner picker (skip for FU + noTrack types — those
              are not tracked cases, just calendar blocks). */}
          {!isFU && !isNoTrack && (
            <div className="flex items-center gap-1.5 pl-3 border-l border-ink-200 flex-wrap">
              <span className="text-[10px] uppercase tracking-wider text-ink-400 font-semibold">เคสของ</span>
              <select value={caseFor} onChange={e => setCaseFor(e.target.value)}
                className="h-7 text-xs px-2 pr-7 bg-white border border-ink-200 rounded-md focus:border-brand-500 focus:ring-2 focus:ring-brand-500/15 outline-none cursor-pointer max-w-[200px] truncate"
                title="คลิกเพื่อเปลี่ยนเจ้าของเคส">
                <optgroup label="── ตัวเอง / คู่ร่วมธุรกิจ ──">
                  <option value="self">★ {currentUser?.name || 'ฉัน'}</option>
                  {currentUser?.partner_name && (
                    <option value="partner">★ {currentUser.partner_name}</option>
                  )}
                </optgroup>
                <optgroup label={`── Downline (${dlProspects.length}) ──`}>
                  {dlProspects.length === 0
                    ? <option disabled>— ยังไม่มี DL ที่มี ABO —</option>
                    : dlProspects.map(p => (
                        <option key={p.id} value={p.id}>DL · {p.name}{p.abo_code ? ` (ABO ${p.abo_code})` : ''}</option>
                      ))}
                </optgroup>
              </select>
              {ownerChanged && (
                <Pill color="#B87C00" tint="#FFECB8" size="xs">แก้แล้ว · กดบันทึก</Pill>
              )}
            </div>
          )}
          <span className="text-xs text-ink-400 ml-auto">{type.full}</span>
        </div>

        {/* Follow Up Process — choose outcome (skipped for FU + noTrack types) */}
        {!isFU && !isNoTrack && (
        <div className="rounded-xl border border-brand-200 bg-brand-50/50 p-3 space-y-3">
          <div className="flex items-center gap-2">
            <Icon name="sparkles" className="w-4 h-4 text-brand-600" />
            <span className="text-xs font-semibold text-brand-800">Follow Up Process · เลือกผลการนัด</span>
          </div>

          {/* Standard outcomes. 6W appts get a "ของไว้รอติดตาม" tile;
              BM appts get a "ยังไม่ได้นัดต่อ · รอติดตาม" tile that enrols
              the prospect into ABO Tracking with BM ticked. */}
          <div className="grid grid-cols-2 md:grid-cols-3 gap-2">
            {[
              { id: 'bi',       label: nextStep ? `ผ่าน → Step ${nextStep.step}` : 'เคสปิดสำเร็จ', desc: nextStep ? `นัด ${nextStep.title} ต่อ` : '🎉 สร้างใน 6W Tracking', color: '#10B981', tint: '#E7F8F1', icon: 'arrowR' },
              // "นัด BM ต่อ" — book a follow-on BM. Only on 6W appts (a 6W lead
              // that's ready to move into the business flow).
              ...(isSixW ? [
                { id: 'bookbm', label: 'นัด BM ต่อ', desc: 'จองนัด BM ครั้งถัดไป', color: '#2F5DE5', tint: '#EEF4FF', icon: 'calendar' },
              ] : []),
              // ของไว้รอติดตาม sits right next to "เคสปิดสำเร็จ" on 6W
              // appts so the most-used positive-path outcomes cluster on
              // the left of the grid.
              ...(isSixW ? [
                { id: 'parked', label: 'กดของไว้รอติดตาม', desc: 'เลือกวันแจ้งเตือน · LINE OA ตอน 07:00', color: '#7C3AED', tint: '#F3EFFE', icon: 'bell' },
              ] : []),
              // BM / BI: "ยังไม่ได้นัดต่อ" — the session happened but no next
              // appt was booked → just keep in the รอติดตาม list.
              ...(isTrackable ? [
                { id: 'track', label: 'ยังไม่ได้นัดต่อ', desc: 'เก็บเข้า รอติดตาม', color: '#0EA5E9', tint: '#E0F2FE', icon: 'clock' },
              ] : []),
              { id: 'skip',     label: 'ยังไม่สนใจ',     desc: 'ปิดเคส, เก็บไว้ในระบบ', color: '#94A3B8', tint: '#F1F5F9', icon: 'x' },
              { id: 'postpone', label: 'เลื่อน',         desc: `นัด ${type.label} ใหม่อีกครั้ง`,  color: '#F59E0B', tint: '#FFF6E0', icon: 'clock' },
            ].map(o => (
              <button key={o.id} onClick={() => setOutcome(o.id)}
                className={`text-left p-3 rounded-lg border-2 transition-all ${outcome === o.id ? 'shadow-card' : 'border-transparent hover:border-ink-200'}`}
                style={outcome === o.id ? { borderColor: o.color, background: o.tint } : { background: 'white' }}>
                <div className="flex items-center gap-1.5 mb-1">
                  <div className="w-6 h-6 rounded-md flex items-center justify-center text-white" style={{ background: o.color }}>
                    <Icon name={o.icon} className="w-3 h-3" strokeWidth={2.5} />
                  </div>
                  <span className="text-xs font-semibold" style={{ color: outcome === o.id ? o.color : '#0F1E38' }}>{o.label}</span>
                </div>
                <div className="text-[10px] text-ink-500 leading-snug">{o.desc}</div>
              </button>
            ))}
          </div>

          {/* "ของไว้รอติดตาม" — let the user pick the reminder date. */}
          {outcome === 'parked' && (
            <div className="pt-2 border-t border-brand-200/60 space-y-2">
              <div className="text-[11px] font-semibold text-purple-700 uppercase tracking-wide flex items-center gap-1.5">
                <Icon name="bell" className="w-3.5 h-3.5" /> ตามวันที่
              </div>
              <input
                type="date"
                value={parkedDate}
                min={new Date().toISOString().slice(0, 10)}
                onChange={e => setParkedDate(e.target.value)}
                className="w-full h-10 px-3 text-sm border-2 border-purple-200 rounded-lg outline-none focus:border-purple-500 num bg-white"
              />
              <div className="text-[10px] text-ink-500 leading-relaxed">
                💡 ระบบจะส่ง LINE OA แจ้งเตือน <b>{(() => { try { return new Date(parkedDate).toLocaleDateString('th-TH', { weekday: 'short', day: 'numeric', month: 'short' }); } catch { return parkedDate; } })()}</b> ตอน 07:00 น.
                · เคสจะถูกเก็บไว้ในรายการ "ของไว้รอติดตาม"
              </div>
            </div>
          )}

          {/* ผ่าน — advance to next step. Let the user pick the next type
              (continue the Follow-Up flow or branch into 6W). Either choice
              also auto-adds the prospect to ABO Tracking on save. */}
          {outcome === 'bi' && (() => {
            const flowType = nextStep ? (apptTypes.find(t => t.id === nextStep.type) || type) : null;
            const sixWType = apptTypes.find(t => t.id === '6W') || { id: '6W', label: '6W', color: '#10B981', tint: '#E7F8F1', full: '6W · ลดน้ำหนัก' };
            const chosen = nextTypeChoice === '6W' ? sixWType : flowType;
            if (!chosen) return null;
            return (
              <div className="space-y-3 pt-2 border-t border-brand-200/60">
                <div className="flex items-center gap-2 flex-wrap">
                  <span className="text-[10px] uppercase tracking-wider font-semibold text-ink-500">นัดต่อเป็น:</span>
                  {flowType && (
                    <button
                      onClick={() => setNextTypeChoice('flow')}
                      className="inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-[11px] font-semibold border-2 transition-all"
                      style={nextTypeChoice === 'flow'
                        ? { borderColor: flowType.color, background: flowType.color + '15', color: flowType.color }
                        : { borderColor: '#E6EAF3', background: 'white', color: '#445576' }}>
                      <span className="w-5 h-5 rounded text-white text-[9px] font-bold flex items-center justify-center" style={{ background: flowType.color }}>{flowType.label}</span>
                      {flowType.full}
                    </button>
                  )}
                  <button
                    onClick={() => setNextTypeChoice('6W')}
                    className="inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-[11px] font-semibold border-2 transition-all"
                    style={nextTypeChoice === '6W'
                      ? { borderColor: sixWType.color, background: sixWType.color + '15', color: sixWType.color }
                      : { borderColor: '#E6EAF3', background: 'white', color: '#445576' }}>
                    <span className="w-5 h-5 rounded text-white text-[9px] font-bold flex items-center justify-center" style={{ background: sixWType.color }}>6W</span>
                    6W · ลดน้ำหนัก
                  </button>
                </div>
                <div className="flex items-center gap-2 text-[10px] text-emerald-700 bg-emerald-50/70 border border-emerald-200/60 rounded-md px-2 py-1.5">
                  <Icon name="check" className="w-3 h-3" strokeWidth={2.5} />
                  <span>เพิ่ม <b>{appt.prospect || 'prospect'}</b> เข้า ABO Tracking ({nextTypeChoice === '6W' ? 'EM6W' : 'Sponsor'}) ให้อัตโนมัติ</span>
                </div>
                <DateTimePicker day={nextDay} start={nextStart} onDay={setNextDay} onStart={setNextStart} color={chosen.color} />
                <ConflictWarning conflicts={conflicts} apptTypes={apptTypes} />

                {/* What to prepare before next step (current step items + link if any) */}
                {currentStep?.link && (
                  <div className="rounded-lg border border-gold-200 bg-gradient-to-br from-gold-50 to-white p-3">
                    <label className="flex items-start gap-2.5 cursor-pointer">
                      <input type="checkbox" checked={sendBoarding} onChange={e => setSendBoarding(e.target.checked)}
                        className="mt-0.5 w-4 h-4 accent-gold-500" />
                      <div className="flex-1 min-w-0">
                        <div className="flex items-center gap-2">
                          <span className="text-lg leading-none">📦</span>
                          <span className="text-xs font-semibold text-ink-900">ส่ง {currentStep.link.label} ก่อนนัดถัดไป</span>
                          <Pill color="#B87C00" tint="#FFECB8" size="xs">Step {currentStep.step}</Pill>
                        </div>
                        <div className="mt-2 flex items-center gap-2 p-2 bg-white rounded border border-ink-100">
                          <Icon name="line" className="w-3.5 h-3.5 text-emerald-600 flex-shrink-0" />
                          <code className="text-[10px] font-mono text-ink-600 truncate flex-1">{currentStep.link.url}</code>
                          <button onClick={(e) => { e.preventDefault(); copyLink(currentStep.link.url); }}
                            className="text-[10px] font-semibold px-2 py-1 rounded bg-ink-900 text-white hover:bg-ink-700 transition-colors flex-shrink-0">
                            {copied ? '✓ Copied' : 'Copy link'}
                          </button>
                        </div>
                        <div className="text-[10px] text-ink-400 mt-1.5">ระบบจะส่ง LINE Push พร้อมลิงก์ให้ผู้มุ่งหวังโดยอัตโนมัติ</div>
                      </div>
                    </label>
                  </div>
                )}

              </div>
            );
          })()}

          {/* เคสปิดสำเร็จ — ไม่มี next step (Step 5 จบ flow, หรือ type นอก flow เช่น 6W).
              กรอก ABO 10 หลัก → กดบันทึก → ระบบจะสร้างลูกค้าในหน้า 6W Tracking ให้. */}
          {outcome === 'bi' && !nextStep && (() => {
            const aboValid = closingAbo.length === 0 || /^\d{10}$/.test(closingAbo);
            return (
              <div className="pt-2 border-t border-brand-200/60 space-y-3">
                <div className="rounded-lg bg-gradient-to-br from-emerald-50 to-amber-50 border border-emerald-200 p-3 text-center">
                  <div className="text-2xl mb-1">🎉</div>
                  <div className="text-sm font-semibold text-emerald-700">เคสปิดสำเร็จ</div>
                  <div className="text-[11px] text-ink-500 mt-0.5">
                    กดบันทึก → ระบบจะสร้าง <b>{appt.prospect || 'ลูกค้า'}</b> ในหน้า 6W Tracking อัตโนมัติ
                  </div>
                </div>
                <div>
                  <label className="flex items-center justify-between text-[11px] font-semibold text-ink-700 mb-1">
                    <span>เลข ABO ของลูกค้า (10 หลัก)</span>
                    <span className={`text-[10px] num ${closingAbo.length === 10 ? 'text-emerald-600' : closingAbo.length > 0 ? 'text-amber-600' : 'text-ink-300'}`}>
                      {closingAbo.length}/10
                    </span>
                  </label>
                  <input
                    value={closingAbo}
                    onChange={e => setClosingAbo(e.target.value.replace(/\D/g, '').slice(0, 10))}
                    placeholder="0000000000"
                    inputMode="numeric"
                    maxLength={10}
                    className={`w-full h-10 px-3 text-sm num tracking-wider border rounded-lg outline-none focus:ring-2 bg-white ${!aboValid ? 'border-rose-300 focus:border-rose-500 focus:ring-rose-500/15' : 'border-emerald-300 focus:border-emerald-500 focus:ring-emerald-500/20'}`}
                  />
                  {!aboValid && <p className="mt-1 text-[10px] text-rose-600">ต้องเป็นตัวเลข 10 หลักเท่านั้น</p>}
                  <p className="mt-1 text-[10px] text-ink-400">ปล่อยว่างก็ได้ — สามารถมาเติม ABO ในหน้า 6W Tracking ภายหลังได้</p>
                </div>
              </div>
            );
          })()}

          {/* Postpone outcome */}
          {outcome === 'postpone' && (
            <div className="space-y-3 pt-2 border-t border-brand-200/60">
              <div className="text-[11px] font-semibold text-gold-700 uppercase tracking-wide">นัด {type.label} ใหม่</div>
              <DateTimePicker day={nextDay} start={nextStart} onDay={setNextDay} onStart={setNextStart} color={type.color} />
              <ConflictWarning conflicts={conflicts} apptTypes={apptTypes} />
            </div>
          )}

          {/* Book a follow-on BM */}
          {outcome === 'bookbm' && (
            <div className="space-y-3 pt-2 border-t border-brand-200/60">
              <div className="text-[11px] font-semibold text-brand-700 uppercase tracking-wide">นัด BM ครั้งถัดไป</div>
              <DateTimePicker day={nextDay} start={nextStart} onDay={setNextDay} onStart={setNextStart} color="#2F5DE5" />
            </div>
          )}

          {/* Skip outcome */}
          {outcome === 'skip' && (
            <div className="p-3 rounded-lg bg-ink-50 text-[11px] text-ink-600 border-t border-brand-200/60">
              เก็บข้อมูลผู้มุ่งหวังไว้ในระบบ — อาจติดต่อใหม่ใน 3-6 เดือน
            </div>
          )}
        </div>
        )}

        {/* Upload photo after case completed — skip when postponing (case not done yet) */}
        {outcome && outcome !== 'postpone' && (
          <Field label={
            <div className="flex items-center gap-2">
              <span>📷 รูปหลังทำเคส</span>
              <Pill color="#94A3B8" tint="#F1F5F9" size="xs">{photos.length} รูป</Pill>
            </div>
          }>
            <div className="space-y-2">
              <input ref={fileInputRef} type="file" accept="image/*" multiple onChange={handlePhotoUpload} className="hidden" />
              {photos.length === 0 ? (
                <button onClick={() => fileInputRef.current?.click()}
                  className="w-full border-2 border-dashed border-ink-200 rounded-lg p-5 flex flex-col items-center gap-2 text-ink-500 hover:border-brand-400 hover:bg-brand-50/30 hover:text-brand-600 transition-all">
                  <Icon name="plus" className="w-5 h-5" strokeWidth={2} />
                  <span className="text-xs font-medium">เพิ่มรูป (ภาพหน้างาน / สลิป / ใบเสร็จ)</span>
                  <span className="text-[10px] text-ink-400">รองรับหลายรูป · JPG / PNG</span>
                </button>
              ) : (
                <div className="grid grid-cols-4 gap-2">
                  {photos.map((p, i) => (
                    <div key={i} className="relative aspect-square rounded-lg overflow-hidden border border-ink-200 group">
                      <img src={p.url} alt={`photo ${i+1}`} className="w-full h-full object-cover" />
                      {p.file && <span className="absolute bottom-1 left-1 text-[8px] font-bold px-1 rounded bg-amber-500 text-white">ยังไม่บันทึก</span>}
                      <button onClick={() => removePhoto(i)}
                        className="absolute top-1 right-1 w-5 h-5 rounded-full bg-ink-900/80 text-white flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
                        <Icon name="x" className="w-3 h-3" strokeWidth={2.5} />
                      </button>
                    </div>
                  ))}
                  <button onClick={() => fileInputRef.current?.click()}
                    className="aspect-square rounded-lg border-2 border-dashed border-ink-200 flex items-center justify-center text-ink-400 hover:border-brand-400 hover:text-brand-600 hover:bg-brand-50/30 transition-all">
                    <Icon name="plus" className="w-5 h-5" strokeWidth={2} />
                  </button>
                </div>
              )}
            </div>
          </Field>
        )}

        {/* LINE group with prospect — paste invite link, then tap the icon
            to deep-link into the LINE app. Skipped for FU (FU appts don't
            have their own prospect row to attach this to). */}
        {!isFU && appt.prospect_id && (
          <Field label={
            <div className="flex items-center gap-1.5">
              <span className="w-4 h-4 rounded-sm flex items-center justify-center" style={{ background: '#06C755' }}>
                <Icon name="line" className="w-3 h-3 text-white" />
              </span>
              <span>กลุ่ม LINE กับ Prospect</span>
              {lineGroupUrl.trim() && (
                <a href={lineGroupUrl.trim()} target="_blank" rel="noopener noreferrer"
                  className="ml-auto inline-flex items-center gap-1 text-[11px] font-semibold px-2 py-1 rounded-md hover:shadow-card transition-all"
                  style={{ background: '#06C75515', color: '#06C755', border: '1px solid #06C75540' }}
                  title="เปิดกลุ่มใน LINE app">
                  <Icon name="arrowR" className="w-3 h-3" />
                  เปิดกลุ่ม
                </a>
              )}
            </div>
          }>
            <input
              type="url"
              value={lineGroupUrl}
              onChange={e => setLineGroupUrl(e.target.value)}
              placeholder="https://line.me/R/ti/g/..."
              className="input"
              inputMode="url"
            />
            <p className="text-[10px] text-ink-400 mt-1">
              วิธีเอา link: เปิดกลุ่มใน LINE → แตะชื่อกลุ่ม → Invite → Copy link
            </p>
          </Field>
        )}

        <Field label="โน้ต">
          <textarea value={notes} onChange={e => setNotes(e.target.value)} rows={3} className="input resize-none" placeholder="ผลการนัด, สิ่งที่ต้องตามต่อ" />
        </Field>
      </div>
      <style>{`
        .input { width: 100%; padding: 8px 12px; font-size: 13px; border: 1px solid #C9D2E3; border-radius: 8px; background: white; outline: none; color: #0F1E38; }
        .input:focus { border-color: #2F5DE5; box-shadow: 0 0 0 3px rgba(47,93,229,0.15); }
      `}</style>

      <ConfirmDialog
        open={confirming}
        onCancel={() => setConfirming(false)}
        onConfirm={() => { setConfirming(false); handleSave(); }}
        message={
          // Pure-edit path (no outcome, but data changed) gets its own copy
          // so the user doesn't see "ผลการนัด: —". FU still takes its branch.
          !outcome && !isFU && (notesChanged || nameChanged || scheduleChanged || ownerChanged || lineGroupDirty) ? (
            <span>
              บันทึกการแก้ไขนัด <b>{prospectName || appt.prospect}</b> ใช่หรือไม่?
              {scheduleChanged && (
                <div className="mt-2 text-xs text-ink-500">
                  เวลาใหม่: {formatDayOffset(editDay)} · {formatTime(editStart)} ({Math.round(editDur*60)} นาที)
                </div>
              )}
            </span>
          ) : isFU ? (
            <span>บันทึกโน้ตของนัด Follow Up <b>{appt.prospect}</b> ใช่หรือไม่?</span>
          ) : (
            <span>
              บันทึกผลการนัด <b>{appt.prospect}</b> เป็น{' '}
              <b>{
                outcome === 'bi' ? 'ผ่าน (ไป ' + (nextStep?.type || '—') + ')'
                : outcome === 'postpone' ? 'เลื่อนนัด'
                : outcome === 'bookbm' ? 'นัด BM ต่อ'
                : outcome === 'skip' ? 'ยกเลิก'
                : outcome === 'parked' ? `ของไว้รอติดตาม (${parkedDate})`
                : outcome === 'track' ? 'ยังไม่ได้นัดต่อ · รอติดตาม'
                : '—'
              }</b>
              {' '}ใช่หรือไม่?
              {outcome === 'bookbm' && (
                <div className="mt-2 text-xs text-ink-500">สร้างนัด BM ใน {formatDayOffset(nextDay)} เวลา {formatTime(nextStart)}</div>
              )}
              {outcome === 'track' && (
                <div className="mt-2 text-xs text-ink-500">เก็บ <b>{appt.prospect}</b> เข้ารายการ "รอติดตาม"</div>
              )}
              {outcome === 'bi' && nextStep && (
                <div className="mt-2 text-xs text-ink-500">จะสร้างนัด {nextStep.type} ต่อใน {formatDayOffset(nextDay)} เวลา {formatTime(nextStart)}</div>
              )}
              {outcome === 'postpone' && (
                <div className="mt-2 text-xs text-ink-500">จะสร้างนัด {type.label} ใหม่ใน {formatDayOffset(nextDay)} เวลา {formatTime(nextStart)}</div>
              )}
            </span>
          )
        }
      />
    </Modal>
  );
}

// Warning shown when the proposed BI/postpone slot overlaps existing appointments
function ConflictWarning({ conflicts, apptTypes }) {
  if (!conflicts || conflicts.length === 0) {
    return (
      <div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-emerald-50 border border-emerald-200 text-[11px] text-emerald-700">
        <Icon name="check" className="w-3.5 h-3.5" strokeWidth={2.5} />
        <span className="font-medium">ช่วงเวลานี้ว่าง · ไม่ชนกับนัดอื่น</span>
      </div>
    );
  }
  return (
    <div className="rounded-lg bg-rose-50 border border-rose-200 p-3">
      <div className="flex items-center gap-2 text-rose-700 mb-2">
        <span className="text-base leading-none">⚠️</span>
        <span className="text-xs font-semibold">เวลานี้ชนกับ {conflicts.length} นัดอื่น</span>
      </div>
      <ul className="space-y-1.5">
        {conflicts.map(c => {
          const t = apptTypes.find(x => x.id === c.type);
          return (
            <li key={c.id} className="flex items-center gap-2 text-[11px] text-ink-700">
              <span className="px-1.5 py-0.5 rounded text-[10px] font-bold text-white" style={{ background: t?.color || '#94A3B8' }}>{t?.label || c.type}</span>
              <span className="num text-ink-500">{DAYS_FULL[c.day]} · {formatTime(c.start)}–{formatTime(c.start + c.dur)}</span>
              <span className="text-ink-300">·</span>
              <span className="font-medium truncate flex-1">{c.prospect}</span>
            </li>
          );
        })}
      </ul>
      <div className="text-[10px] text-rose-600 mt-2">ลองเปลี่ยนวัน/เวลา หรือยืนยันถ้าตั้งใจซ้อน</div>
    </div>
  );
}

// Simple inline day + time picker (works for week-of-current-view)
function DateTimePicker({ day, start, onDay, onStart, color = '#2F5DE5' }) {
  // The caller still passes `day` as an offset (0..N) from window.WEEK_START.
  // Internally we hand off to MonthDatePicker (Date-based), translating both
  // directions. No upper bound on `day` — the user can pick any future date.
  const now = new Date();
  const today = useMemo(() => { const d = new Date(); d.setHours(0, 0, 0, 0); return d; }, []);
  const nowSlot = Math.ceil((now.getHours() + now.getMinutes() / 60) * 2) / 2;

  const weekStart = window.WEEK_START ? new Date(window.WEEK_START) : today;
  const dayToDate = (d) => new Date(weekStart.getTime() + d * 86400000);
  const dateToDay = (d) => Math.round((d - weekStart) / 86400000);

  const selectedDate = dayToDate(day);
  const isTodaySelected = selectedDate.getTime() === today.getTime();

  return (
    <div className="space-y-3">
      <div>
        <div className="text-[10px] font-medium text-ink-500 mb-1.5 uppercase tracking-wide">วัน</div>
        <MonthDatePicker
          value={selectedDate}
          onChange={(d) => onDay(dateToDay(d))}
          minDate={today}
          color={color}
        />
      </div>
      <div>
        <div className="text-[10px] font-medium text-ink-500 mb-1.5 uppercase tracking-wide">เวลา</div>
        <select value={start} onChange={e => onStart(parseFloat(e.target.value))}
          className="w-full h-9 px-3 text-sm num border border-ink-200 rounded-md bg-white focus:outline-none focus:border-brand-500">
          {Array.from({ length: 32 }, (_, i) => 8 + i * 0.5).map(t => {
            const disabled = isTodaySelected && t < nowSlot;
            return (
              <option key={t} value={t} disabled={disabled}>
                {formatTime(t)}{disabled ? ' (ผ่านมาแล้ว)' : ''}
              </option>
            );
          })}
        </select>
      </div>
    </div>
  );
}

const Field = ({ label, children }) => (
  <div>
    <div className="text-[11px] font-medium text-ink-500 mb-1.5 uppercase tracking-wide">{label}</div>
    {children}
  </div>
);
