/* YeYe Portal — Modals + App root */
const PURCHASE_KEY = 'yeye_expat_purchased_v2';
const GHL_TASK_SYNC_BASE = 'https://ghl-read-service-ib6dfdthbq-ew.a.run.app';

function CompleteProfileBanner({ contact, lang, onGoto, currentPage, blankProfile, purchasedServices }) {
  if (!window.YEYE_PROFILE || !contact) return null;
  if (blankProfile) return null;
  if (!(purchasedServices || []).length) return null;
  if (currentPage === 'formSubmission' || currentPage === 'dashboard' || currentPage === 'documents') return null;
  const missing = window.YEYE_PROFILE.missing(contact);
  if (!missing.length) return null;
  const sections = window.YEYE_REQUIRED_SECTIONS || {};
  const grouped = window.YEYE_PROFILE.missingBySection(contact);
  const sectionKeys = Object.keys(grouped);
  const title = ({ tr: 'Profilinizi tamamlayın', cs: 'Doplňte svůj profil', en: 'Complete your profile' })[lang] || 'Complete your profile';
  const sub = ({
    tr: `Aktif hizmetiniz için eksik ${missing.length} alanı doldurmanız gerekiyor.`,
    cs: `Pro aktivní službu doplňte chybějící pole (${missing.length}).`,
    en: `Fill ${missing.length} missing field${missing.length > 1 ? 's' : ''} for your active service.`,
  })[lang] || `Fill ${missing.length} missing fields.`;
  const cta = ({ tr: 'Şimdi tamamla', cs: 'Doplnit teď', en: 'Complete now' })[lang] || 'Complete now';
  const chipLabels = sectionKeys.slice(0, 4).map((k) => {
    const sec = sections[k];
    return sec ? (sec[lang] || sec.en) + ` (${grouped[k].length})` : k;
  });
  return (
    <div style={{
      background: '#FEF3C7', border: '1px solid #F59E0B', borderRadius: 12,
      padding: '14px 18px', marginBottom: 16, display: 'flex', alignItems: 'center', gap: 14, flexWrap: 'wrap',
    }}>
      <div style={{ flex: '1 1 240px', display: 'flex', flexDirection: 'column', gap: 4 }}>
        <div style={{ fontSize: 14, fontWeight: 700, color: '#92400E', display: 'flex', alignItems: 'center', gap: 8 }}>
          <span style={{ fontSize: 18 }}>⚠️</span>{title}
        </div>
        <div style={{ fontSize: 13, color: '#78350F' }}>{sub}</div>
        {!!chipLabels.length && (
          <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 4 }}>
            {chipLabels.map((label) => (
              <span key={label} style={{ background: '#FCD34D', color: '#78350F', padding: '2px 10px', borderRadius: 999, fontSize: 11.5, fontWeight: 600 }}>{label}</span>
            ))}
          </div>
        )}
      </div>
      <button
        onClick={onGoto}
        style={{
          height: 40, padding: '0 18px', borderRadius: 10, background: '#92400E', color: '#fff',
          border: 'none', fontSize: 13.5, fontWeight: 600, cursor: 'pointer',
        }}>{cta}</button>
    </div>
  );
}
function readPurchaseStore() {
  try { return JSON.parse(localStorage.getItem(PURCHASE_KEY) || '{}') || {}; } catch (_) { return {}; }
}
function servicePrice(s, currency) { return (typeof svcPrice === 'function' ? svcPrice(s, currency) : (s.p + (s.u || ''))); }
function serviceTitle(s, t) {
  const title = (typeof svcName === 'function' ? svcName(s, t) : t('svc.' + s.k));
  if (!title || typeof title !== 'object') return title;
  const lang = localStorage.getItem('yeye-lang') || 'en';
  return title[lang] || title.en || title.tr || title.cs || s.k;
}
function serviceCategoryTitle(s, t) { return (typeof svcCatName === 'function' ? svcCatName(s, t) : t('scat.' + s.cat)); }
function cartPurchaseKey(item) {
  if (!item) return '';
  if (item.serviceKey) return item.serviceKey;
  const products = (window.YEYE && window.YEYE.marketplaceProducts) || [];
  const product = products.find((row) => row.key === item.productKey);
  const variant = product && (product.variants || []).find((row) => row.key === item.variantKey);
  return (variant && (variant.serviceKey || variant.k)) || item.productKey || '';
}
function serviceDesc(s, t) {
  const map = {
    immig: 'svc.detail.immig', docs: 'svc.detail.docs', legal: 'svc.detail.legal',
    biz: 'svc.detail.biz', fin: 'svc.detail.fin', relocation: 'svc.detail.immig',
    housing: 'svc.detail.docs', translation: 'svc.detail.legal', osvc: 'svc.detail.biz',
    sro: 'svc.detail.biz', tax: 'svc.detail.fin', vehicle: 'svc.detail.default',
    permanent: 'svc.detail.immig',
  };
  return t(map[s.cat] || 'svc.detail.default');
}
function browserTimeZone() {
  try { return Intl.DateTimeFormat().resolvedOptions().timeZone || 'Europe/Prague'; } catch (_) { return 'Europe/Prague'; }
}
function initialPortalLang() {
  const supported = ['en', 'tr', 'cs'];
  try {
    const stored = String(localStorage.getItem('yeye-lang') || '').toLowerCase();
    if (supported.includes(stored)) return stored;
  } catch (_) {}
  try {
    const browser = String(navigator.language || 'en').slice(0, 2).toLowerCase();
    if (supported.includes(browser)) return browser;
  } catch (_) {}
  return 'en';
}
function taskDateLabel(value) {
  if (!value) return '—';
  const d = new Date(value);
  if (Number.isNaN(d.getTime())) return String(value).slice(0, 12);
  return d.toLocaleDateString(undefined, { day: 'numeric', month: 'short' });
}
function taskCategoryFromText(task) {
  const raw = String([task.category, task.title, task.body, task.notes].filter(Boolean).join(' ')).toLowerCase();
  if (/invoice|payment|pay|fee|fatura|ödeme|odeme|ücret|ucret/.test(raw)) return 'finance';
  if (/appointment|calendar|randevu|biometric|embassy|police|oamp/.test(raw)) return 'appointment';
  if (/passport|document|upload|file|insurance|contract|belge|evrak|pasaport|sigorta/.test(raw)) return 'document';
  if (/immigration|visa|card|permit|residence|blue|employee|göç|goc|ikamet/.test(raw)) return 'immigration';
  if (/admin|office|form|signature|imza/.test(raw)) return 'admin';
  return 'other';
}
function parseTaskTitlePrefix(title) {
  const raw = String(title || '');
  const match = raw.match(/^\s*\[(Belge|Bilgi)\]\s*/i);
  if (!match) return { title: raw, taskType: '', taskTypeLabelKey: '', taskTypeTone: 'neut' };
  const kind = match[1].toLowerCase() === 'belge' ? 'belge' : 'bilgi';
  return {
    title: raw.slice(match[0].length).trim() || raw,
    taskType: kind,
    taskTypeLabelKey: kind === 'belge' ? 'task.type.documentRequest' : 'task.type.info',
    taskTypeTone: kind === 'belge' ? 'warn' : 'info',
  };
}
function normalizePortalTask(task) {
  const done = !!(task.done || task.completed || String(task.status || '').toLowerCase() === 'completed');
  const rawTitle = task.title || task.subject || task.body || 'GHL task';
  const parsedTitle = parseTaskTitlePrefix(rawTitle);
  const category = taskCategoryFromText({ ...task, title: rawTitle });
  return {
    id: task.id || task.ghlTaskId || ('task-' + Math.random().toString(36).slice(2)),
    ghlTaskId: task.ghlTaskId || task.id || task.taskId,
    contactId: task.contactId,
    title: parsedTitle.title,
    rawTitle,
    taskType: parsedTitle.taskType,
    taskTypeLabelKey: parsedTitle.taskTypeLabelKey,
    taskTypeTone: parsedTitle.taskTypeTone,
    body: task.body || task.notes || '',
    due: taskDateLabel(task.due || task.dueDate || task.date),
    dueRaw: task.due || task.dueDate || task.date,
    createdAt: task.createdAt || task.dateAdded || task.created_at || '',
    done,
    category,
    tone: done ? 'ok' : (category === 'finance' ? 'warn' : category === 'document' ? 'bad' : 'info'),
    source: task.source || 'ghl',
  };
}
function eventDateParts(isoDate) {
  const d = new Date((isoDate || '').slice(0, 10) + 'T12:00:00');
  if (Number.isNaN(d.getTime())) return { day: 16, dateLabel: '16 Jun' };
  const day = d.getDate();
  const dateLabel = d.toLocaleDateString(undefined, { day: 'numeric', month: 'short' });
  return { day, dateLabel };
}

/* ---------- Service purchase modal ---------- */
function ServiceDisclaimer({ data, t }) {
  const liStyle = { marginBottom: 4 };
  const ulStyle = { margin: '6px 0 14px 18px', padding: 0 };
  const h = { fontSize: 14, fontWeight: 700, marginTop: 0, marginBottom: 6 };
  const sub = { fontSize: 13.5, fontWeight: 600, marginTop: 10, marginBottom: 4 };
  const p = { fontSize: 13, lineHeight: 1.55, margin: '4px 0' };
  return (
    <div style={{ maxHeight: 360, overflowY: 'auto', padding: '12px 14px', border: '1px solid var(--line-2)', borderRadius: 10, background: 'var(--surface-2)' }}>
      <div style={h}>{data.title}</div>
      <div style={p}>{data.intro}</div>
      <div style={sub}>{t('mod.included')}</div>
      <ul style={ulStyle}>
        {data.included.map((x, i) => <li key={i} style={liStyle}>{x}</li>)}
      </ul>
      <div style={{ marginTop: 12, padding: '12px 14px', border: '1px solid var(--bad)', borderRadius: 10, background: 'var(--bad-bg)' }}>
        <div style={{ ...h, color: 'var(--bad)' }}>{t('mod.excluded')}</div>
        <div style={p}>{t('mod.excludedNote')}</div>
        <ul style={ulStyle}>
          {data.excluded.map((x, i) => <li key={i} style={liStyle}>{x}</li>)}
        </ul>
      </div>
    </div>
  );
}
function ServiceModal({ service, contact, liveContact, onClose, onConfirm, onRequestTicket }) {
  const { t, lang, currency } = useT();
  const s = service || window.YEYE.services[0];
  const resolveSelectedService = (base) => {
    if (!base || !base.family || !Array.isArray(base.tabs)) return base;
    const tab = base.tabs.find((x) => x.k === base._activeTabKey) || base.tabs[0];
    if (!tab) return base;
    const variants = Array.isArray(tab.variants) ? tab.variants : [];
    const variant = variants.find((x) => x.k === base._activeVariantKey) || variants[0];
    const picked = variant || tab;
    return {
      ...base,
      k: picked.k || tab.k,
      parentK: base.k,
      name: tab.label || base.name,
      p: picked.price || tab.price || base.p,
      priceLabel: picked.priceLabel || tab.priceLabel || base.priceLabel,
      u: picked.u || tab.u || base.u,
      payUrl: picked.payUrl != null ? picked.payUrl : tab.payUrl,
      description: picked.description || tab.description || base.description,
      deliverables: picked.deliverables || tab.deliverables || base.deliverables,
    };
  };
  const initialService = resolveSelectedService(s);
  const hasVariants = !s._activeTabKey && Array.isArray(s.variants) && s.variants.length > 0;
  const [selectedService, setSelectedService] = useState(initialService);
  const marketplaceItemFor = (svc) => {
    const serviceKey = String((svc && svc.k) || '');
    const product = (window.YEYE.marketplaceProducts || []).find((item) => (
      item.key === serviceKey || (Array.isArray(item.serviceKeys) && item.serviceKeys.includes(serviceKey))
    ));
    if (!product || product.comingSoon) return null;
    const variant = (product.variants || []).find((item) => item.serviceKey === serviceKey)
      || (product.variants || []).find((item) => item.key === (serviceKey.endsWith('_diy') ? 'diy' : 'professional'))
      || (product.variants || []).find((item) => item.key === 'standard');
    if (!variant) return null;
    return {
      productKey: product.key,
      priceId: variant.priceId || product.priceId,
      productId: variant.productId || product.productId,
      variantKey: variant.key,
      serviceKey: variant.serviceKey || serviceKey,
      name: serviceTitle(svc, t),
      variantLabel: t(variant.labelTk),
      uiPrice: variant.uiPrice,
      uiCurrency: variant.uiCurrency,
      qty: 1,
    };
  };
  const addMarketplaceItem = (svc) => {
    const cartItem = marketplaceItemFor(svc);
    if (!cartItem || !window.YEYE_CART) return;
    window.YEYE_CART.addItem(cartItem);
    if (window.YEYE_TOAST) window.YEYE_TOAST(t('cart.added'));
    onClose();
    if (window.YEYE_CART_OPEN) window.YEYE_CART_OPEN();
  };
  const icon = window.YEYE.catIcon[s.cat];
  const price = servicePrice(selectedService, currency);
  const entry = (window.YEYE_DISCLAIMERS || {})[selectedService.k] || (selectedService.parentK ? (window.YEYE_DISCLAIMERS || {})[selectedService.parentK] : null);
  const disclaimer = entry ? (entry[lang] || entry.en || entry.tr) : null;
  const [agreed, setAgreed] = useState(false);
  const [step, setStep] = useState(hasVariants ? 'variant' : 'review');
  const contactEmail = ((liveContact && liveContact.email) || (contact && contact.email) || '').trim();
  const variantDeliverables = Array.isArray(selectedService.deliverables)
    ? selectedService.deliverables.map((d) => (d && (d[lang] || d.en || d.tr || d.cs)) || '').filter(Boolean)
    : null;
  const deliverables = variantDeliverables && variantDeliverables.length ? variantDeliverables.slice(0, 4)
    : (disclaimer && disclaimer.included ? disclaimer.included.slice(0, 4) : [t('mod.b1'), t('mod.b2'), t('mod.b3')]);
  const buildPayUrl = () => {
    if (!selectedService.payUrl) return null;
    const src = liveContact || contact || {};
    const first = src.firstName || src.first_name || (src.name ? String(src.name).split(' ')[0] : '') || '';
    const last = src.lastName || src.last_name || (src.name ? String(src.name).split(' ').slice(1).join(' ') : '') || '';
    const email = src.email || '';
    const phone = src.phone || '';
    const params = new URLSearchParams();
    if (first) params.set('first_name', first);
    if (last) params.set('last_name', last);
    if (email) params.set('email', email);
    if (phone) params.set('phone', phone);
    const qs = params.toString();
    return qs ? selectedService.payUrl + (selectedService.payUrl.includes('?') ? '&' : '?') + qs : selectedService.payUrl;
  };
  const goToPayment = () => {
    const url = buildPayUrl();
    if (url) window.open(url, '_blank', 'noopener');
    onConfirm(selectedService);
  };
  const handleConfirm = () => {
    if (selectedService.payUrl) setStep('emailNotice');
    else { onConfirm(selectedService); }
  };
  const variantDescription = (variant) => {
    if (variant.description) return variant.description[lang] || variant.description.en || variant.description.tr || variant.description.cs;
    return disclaimer ? disclaimer.intro : ((s.description && (s.description[lang] || s.description.en)) || serviceDesc(s, t));
  };
	  const requestVariantTicket = (variant) => {
	    if (onRequestTicket) {
	      onRequestTicket({
	        catK: 'sales',
	        subject: t('ticket.serviceSubject') + ': ' + serviceTitle(s, t) + ' - ' + (variant.k.endsWith('_diy') ? t('svc.variant.diy') : t('svc.variant.professional')),
	      });
	    }
	  };
  if (step === 'variant') {
    const professional = s.variants[0];
    const diy = s.variants[1];
    return (
      <Modal icon={icon} title={serviceTitle(s, t)} sub={t('svc.variant.chooserTitle')} onClose={onClose}
        footer={<Btn variant="ghost" onClick={onClose}>{t('c.cancel')}</Btn>} wide>
        <div className="flex col gap-16">
          <div>
            <div className="h3">{t('svc.variant.chooserTitle')}</div>
            <div className="muted" style={{ fontSize: 13.5 }}>{t('svc.variant.chooserSub')}</div>
          </div>
          <div className="grid g-12" style={{ gap: 16 }}>
            {[professional, diy].filter(Boolean).map((variant) => {
              const isDiy = variant.k.endsWith('_diy');
              return (
                <div className="col-6" key={variant.k}>
                  <Card className="card-pad" style={{ height: '100%', display: 'flex', flexDirection: 'column', gap: 12 }}>
                    <div className="flex ac gap-10">
                      <div className="hd-ic" style={{ width: 38, height: 38, borderRadius: 10 }}><Icon name={isDiy ? 'list-checks' : 'sparkles'} size={18} /></div>
                      <div className="strong" style={{ fontSize: 14 }}>{isDiy ? t('svc.variant.diy') : t('svc.variant.professional')}</div>
                    </div>
                    <div className="strong tnum" style={{ fontSize: 20 }}>{variant.price}</div>
                    <div className="muted grow" style={{ fontSize: 13.5, lineHeight: 1.55 }}>{variantDescription(variant)}</div>
	                    <div className="flex gap-8 wrap">
	                      {marketplaceItemFor({ ...s, k: variant.k }) ? (
	                        <Btn variant="primary" icon="shopping-cart" onClick={() => addMarketplaceItem({ ...s, k: variant.k, p: variant.price, priceLabel: variant.priceLabel })}>
	                          {t('cart.addToCart')}
	                        </Btn>
	                      ) : (
	                        <Btn variant="primary" icon="send" onClick={() => requestVariantTicket(variant)}>
	                          {t('svc.supportRequest')}
	                        </Btn>
	                      )}
	                    </div>
                  </Card>
                </div>
              );
            })}
          </div>
        </div>
      </Modal>
    );
  }
  if (step === 'emailNotice') {
    return (
      <Modal icon="mail-check" title={t('pay.confirmEmail')} sub={t('pay.useEmail')} onClose={onClose}
        footer={<>
          <Btn variant="ghost" onClick={() => setStep('review')}>{t('c.back')}</Btn>
          <Btn variant="primary" icon="arrow-right" onClick={goToPayment}>{t('pay.continue')}</Btn>
        </>}>
        <div className="flex col gap-16">
          <div className="alert warn">
            <div className="a-ic"><Icon name="alert-triangle" size={16} /></div>
            <div className="grow">
              <div className="a-t">{t('pay.emailWarningTitle')}</div>
              <div className="a-s">{t('pay.emailWarningBody')}</div>
            </div>
          </div>
          <div style={{ background: 'var(--surface-2)', border: '1px solid var(--line)', borderRadius: 'var(--r-lg)', padding: '18px 20px' }}>
            <div className="eyebrow mb-8">{t('pay.registeredEmail')}</div>
            <div className="strong" style={{ fontSize: 18, wordBreak: 'break-all' }}>{contactEmail || '—'}</div>
          </div>
          <div className="muted" style={{ fontSize: 13, lineHeight: 1.5 }}>
            {t('pay.redirectNote')}
          </div>
        </div>
      </Modal>
    );
  }
  const cartBtn = marketplaceItemFor(selectedService) ? (
    <Btn variant="primary" icon="shopping-cart" onClick={() => addMarketplaceItem(selectedService)}>{t('cart.addToCart')}</Btn>
  ) : <Btn variant="primary" icon="send" onClick={() => onRequestTicket && onRequestTicket({ catK: 'sales', subject: t('ticket.serviceSubject') + ': ' + serviceTitle(selectedService, t) })}>{t('svc.supportRequest')}</Btn>;
  return (
    <Modal icon={icon} title={serviceTitle(s, t)} sub={serviceCategoryTitle(s, t) + ' · ' + price} onClose={onClose}
      footer={<><Btn variant="ghost" onClick={onClose}>{t('c.cancel')}</Btn>{cartBtn}</>}>
      <div className="flex col gap-14 mb-16">
        <div>
          <div className="eyebrow mb-8">{t('svc.description')}</div>
          <div className="muted" style={{ fontSize: 13.5, lineHeight: 1.55 }}>{disclaimer ? disclaimer.intro : ((selectedService.description && (selectedService.description[lang] || selectedService.description.en)) || serviceDesc(selectedService, t))}</div>
        </div>
        <div>
          <div className="eyebrow mb-8">{t('svc.price')}</div>
          <div className="strong tnum" style={{ fontSize: 18 }}>{price}</div>
        </div>
        <div>
          <div className="eyebrow mb-8">{t('svc.deliverables')}</div>
          <div className="flex col gap-8">
            {deliverables.map((x, i) => (
              <div key={i} className="flex ac gap-10"><div className="hd-ic" style={{ width: 28, height: 28, borderRadius: 8 }}><Icon name="check" size={15} /></div><span style={{ fontSize: 13.5 }}>{x}</span></div>
            ))}
          </div>
        </div>
      </div>
      {disclaimer ? (
        <>
          <ServiceDisclaimer data={disclaimer} t={t} />
          <label className="flex ac gap-10 mt-16" style={{ cursor: 'pointer', userSelect: 'none' }}>
            <input type="checkbox" checked={agreed} onChange={(e) => setAgreed(e.target.checked)} style={{ width: 18, height: 18, cursor: 'pointer' }} />
            <span style={{ fontSize: 13.5, fontWeight: 600 }}>{t('mod.iAgree')}</span>
          </label>
        </>
      ) : (
        <div className="alert info mt-20"><div className="a-ic"><Icon name="info" size={16} /></div><div className="grow"><div className="a-t">{t('mod.noCharge')}</div><div className="a-s">{t('mod.noChargeDesc')}</div></div></div>
      )}
    </Modal>
  );
}

/* ---------- Upload modal ---------- */
function UploadModal({ contactId, initialFolderK, initialSubName, onUploaded, onClose }) {
  const { t } = useT();
  const [drag, setDrag] = useState(false);
  const [file, setFile] = useState(null);
  const [uploading, setUploading] = useState(false);
  const [progress, setProgress] = useState(0);
  const [error, setError] = useState('');
  const inputRef = useRef(null);
  const folders = (window.YEYE && window.YEYE.folders) || [];
  const [folderK, setFolderK] = useState(initialFolderK || (folders[0] && folders[0].tk) || '');
  const selectedFolder = folders.find((folder) => folder.tk === folderK) || folders[0] || { subs: [] };
  const [subK, setSubK] = useState((selectedFolder.subs[0] && selectedFolder.subs[0].tk) || 'other');
  const changeFolder = (nextFolderK) => {
    const nextFolder = folders.find((folder) => folder.tk === nextFolderK) || { subs: [] };
    setFolderK(nextFolderK);
    setSubK((nextFolder.subs[0] && nextFolder.subs[0].tk) || 'other');
  };
  const pickFiles = (files) => {
    const nextFile = files && files[0];
    if (nextFile) {
      setFile(nextFile);
      setError('');
    }
  };
  const submit = async () => {
    if (uploading) return;
    if (!file) {
      if (inputRef.current) inputRef.current.click();
      return;
    }
    if (!contactId || !window.YEYE_BACKEND || !window.YEYE_BACKEND.uploadArchiveFile) {
      setError('backend-not-configured');
      return;
    }
    const toSubName = typeof folderTkToSubName === 'function'
      ? folderTkToSubName
      : (tk) => String(tk || '').replace(/^f_/, '').replace(/\b\w/g, (s) => s.toUpperCase());
    setUploading(true); setProgress(0); setError('');
    try {
      const subfolderName = initialSubName || toSubName(folderK);
      const uploadRes = await window.YEYE_BACKEND.uploadArchiveFile({ contactId, subName: subfolderName, file, onProgress: setProgress });
      const uploadedFile = (uploadRes && (uploadRes.file || uploadRes)) || {};
      if (window.YEYE_AUDIT && window.YEYE_AUDIT.log) {
        window.YEYE_AUDIT.log('document.uploaded', contactId, {
          folderId: (uploadRes && (uploadRes.subId || uploadRes.folderId)) || null,
          subfolderName,
          fileName: uploadedFile.name || file.name,
          fileSize: file.size,
        });
      }
      if (onUploaded) await Promise.resolve(onUploaded(uploadedFile, uploadRes));
      onClose();
    } catch (e) {
      console.warn('Archive upload failed.', e);
      setError(e.message || 'upload-failed');
      setUploading(false);
    }
  };
  return (
    <Modal icon="upload" title={t('mod.upTitle')} sub={t('mod.upSub')} onClose={onClose}
      footer={<><Btn variant="ghost" onClick={onClose}>{t('c.cancel')}</Btn><Btn variant="primary" icon={uploading ? "loader" : "upload"} onClick={submit}>{uploading ? progress + '%' : t('mod.uploadFile')}</Btn></>}>
      <Field label={t('mod.folder')}>
        <select className="select" value={folderK} onChange={(e) => changeFolder(e.target.value)}>
          {folders.map((folder) => <option key={folder.tk} value={folder.tk}>{t(folder.tk)}</option>)}
        </select>
      </Field>
      <Field label={t('mod.subtype')}>
        <select className="select" value={subK} onChange={(e) => setSubK(e.target.value)}>
          {(selectedFolder.subs || []).map((sub) => <option key={sub.tk} value={sub.tk}>{t(sub.tk)}</option>)}
          <option value="other">{t('mod.other')}</option>
        </select>
      </Field>
      <div className="muted" style={{ fontSize: 12.5, marginTop: -2 }}>{t('mod.uploadHelp')}</div>
      <div className="mt-16" onClick={() => inputRef.current && inputRef.current.click()} onDragOver={(e) => { e.preventDefault(); setDrag(true); }} onDragLeave={() => setDrag(false)} onDrop={(e) => { e.preventDefault(); setDrag(false); pickFiles(e.dataTransfer.files); }}
        style={{ border: '2px dashed ' + (drag ? 'var(--brand-400)' : 'var(--line)'), background: drag ? 'var(--brand-50)' : 'var(--surface-2)', borderRadius: 'var(--r-lg)', padding: '34px 20px', textAlign: 'center', transition: '.15s' }}>
        <input ref={inputRef} type="file" style={{ display: 'none' }} onChange={(e) => pickFiles(e.target.files)} />
        <div className="hd-ic" style={{ width: 48, height: 48, margin: '0 auto 12px', borderRadius: 13 }}><Icon name="cloud-upload" size={24} /></div>
        <div className="strong">{file ? file.name : t('mod.dragTitle')}</div>
        <div className="muted" style={{ fontSize: 13, marginTop: 4 }}>{t('mod.or')} <span style={{ color: 'var(--brand-600)', fontWeight: 600 }}>{t('c.browse')}</span></div>
      </div>
      {uploading && <div className="mt-12"><div style={{ height: 4, background: "var(--line)", borderRadius: 2, overflow: "hidden" }}><div style={{ height: "100%", width: progress + "%", background: "var(--brand-500)", transition: ".2s" }} /></div></div>}
      {error && <div className="alert bad mt-12"><div className="a-ic"><Icon name="alert-triangle" size={16} /></div><div className="grow"><div className="a-t">{error}</div></div></div>}
    </Modal>
  );
}

function TaskAttachmentModal({ task, contact, onClose }) {
  const { t } = useT();
  const [files, setFiles] = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const [uploading, setUploading] = React.useState(false);
  const [progress, setProgress] = React.useState(0);
  const [error, setError] = React.useState(null);
  const inputRef = React.useRef(null);
  const contactId = contact && contact.id;
  const taskId = task && (task.id || task.ghlId || task.k);
  const taskTitle = task && (task.title || task.tk || "");
  async function refresh() {
    setLoading(true); setError(null);
    try { const r = await window.YEYE_BACKEND.listTaskFiles({ contactId, taskId, taskTitle }); setFiles((r && r.files) || []); }
    catch (e) { setError(e.message || "list-failed"); }
    finally { setLoading(false); }
  }
  React.useEffect(() => { if (contactId && taskId) refresh(); else setLoading(false); }, [contactId, taskId]);
  async function handleFiles(fileList) {
    if (!fileList || !fileList.length) return;
    if (!contactId || !taskId) { setError("missing-ids"); return; }
    setUploading(true); setError(null); setProgress(0);
    for (const f of Array.from(fileList)) {
      if (f.size > 25 * 1024 * 1024) { setError("too-large"); setUploading(false); return; }
      try { await window.YEYE_BACKEND.uploadTaskFile({ contactId, taskId, taskTitle, file: f, onProgress: setProgress }); }
      catch (e) { setError(e.message || "upload-failed"); setUploading(false); return; }
    }
    setUploading(false); setProgress(0);
    await refresh();
  }
  const fmtSize = (b) => { if (!b) return ""; const n = Number(b); if (n < 1024) return n + " B"; if (n < 1048576) return (n/1024).toFixed(1) + " KB"; return (n/1048576).toFixed(1) + " MB"; };
  return (
    <Modal icon="paperclip" title={taskTitle || t("task.attach.title")} sub={t("task.attach.sub")} onClose={onClose}
      footer={<Btn variant="ghost" onClick={onClose}>{t("c.close")}</Btn>} wide>
      <div className="flex col gap-14">
        <div style={{border: "2px dashed var(--line)", borderRadius: 12, padding: "20px", textAlign: "center", background: "var(--surface-2)"}}
          onDragOver={(e) => e.preventDefault()}
          onDrop={(e) => { e.preventDefault(); handleFiles(e.dataTransfer.files); }}>
          <div className="hd-ic" style={{width: 40, height: 40, margin: "0 auto 10px", borderRadius: 12}}><Icon name="cloud-upload" size={20} /></div>
          <div className="strong">{t("task.attach.drop")}</div>
          <div className="muted" style={{fontSize: 13, marginTop: 4}}>{t("mod.or")} <span style={{color: "var(--brand-600)", fontWeight: 600, cursor: "pointer"}} onClick={() => inputRef.current && inputRef.current.click()}>{t("c.browse")}</span></div>
          <input ref={inputRef} type="file" multiple style={{display: "none"}} onChange={(e) => handleFiles(e.target.files)} />
          {uploading && <div style={{marginTop: 12}}><div style={{height: 4, background: "var(--line)", borderRadius: 2, overflow: "hidden"}}><div style={{height: "100%", width: progress + "%", background: "var(--brand-500)", transition: ".2s"}} /></div><div className="muted" style={{fontSize: 12, marginTop: 6}}>{t("task.attach.uploading")} {progress}%</div></div>}
        </div>
        {error && <div className="alert bad"><div className="a-ic"><Icon name="alert-triangle" size={16} /></div><div className="grow"><div className="a-t">{t("task.attach.err." + error) || error}</div></div></div>}
        <div>
          <div className="strong" style={{marginBottom: 8}}>{t("task.attach.filesList")} ({files.length})</div>
          {loading ? <div className="muted" style={{fontSize: 13}}>{t("c.loading")}</div> :
            files.length === 0 ? <div className="muted" style={{fontSize: 13}}>{t("task.attach.empty")}</div> :
            <div className="rowlist">{files.map((f) => (
              <div className="lrow" key={f.id}>
                <Icon name="file-text" size={16} />
                <div className="l-bd"><div className="l-t">{f.name}</div><div className="l-s">{fmtSize(f.size)} • {(new Date(f.createdTime)).toLocaleDateString()}</div></div>
                <Btn variant="ghost" size="sm" icon="external-link" onClick={() => window.open(f.webViewLink, "_blank")}>{t("c.open")}</Btn>
              </div>
            ))}</div>
          }
        </div>
      </div>
    </Modal>
  );
}

/* ---------- New ticket modal ---------- */
function TicketModal({ onClose, preset, onSubmit }) {
  const { t } = useT();
  const cats = window.YEYE.ticketCats || ['immigration', 'billing', 'documents', 'it'];
  const [catK, setCatK] = useState((preset && preset.catK) || 'immigration');
  const [subject, setSubject] = useState((preset && preset.subject) || '');
  const [details, setDetails] = useState((preset && preset.details) || '');
  const [deadline, setDeadline] = useState((preset && preset.deadline) || '');
  const [priority, setPriority] = useState('normal');
  const submit = () => {
    onSubmit({ catK, subject: subject.trim() || t('mod.subjectPh'), details, priority, deadline });
  };
  return (
    <Modal icon="life-buoy" title={t('mod.ticketTitle')} onClose={onClose}
      footer={<><Btn variant="ghost" onClick={onClose}>{t('c.cancel')}</Btn><Btn variant="primary" icon="send" onClick={submit}>{t('mod.submit')}</Btn></>}>
      <div className="flex col gap-14">
        <Field label={t('col.category')}><select className="select" value={catK} onChange={(e) => setCatK(e.target.value)}>{cats.map((c) => <option key={c} value={c}>{t('cat.' + c)}</option>)}</select></Field>
        <Field label={t('mod.subject')}><input className="input" value={subject} onChange={(e) => setSubject(e.target.value)} placeholder={t('mod.subjectPh')} /></Field>
        <Field label={t('mod.detailsL')}><textarea className="ta" value={details} onChange={(e) => setDetails(e.target.value)} placeholder={t('mod.detailsPh')} /></Field>
        <Field label={t('mod.deadline')}><input className="input" type="date" value={deadline} onChange={(e) => setDeadline(e.target.value)} /></Field>
        <Field label={t('mod.priority')}><div className="flex gap-8">{['normal', 'high', 'urgent'].map((p) => <button key={p} type="button" className={'chip ' + (priority === p ? 'on' : '')} onClick={() => setPriority(p)}>{t('mod.' + p)}</button>)}</div></Field>
      </div>
    </Modal>
  );
}

function CalendarEventModal({ onClose, onSubmit }) {
  const { t } = useT();
  const today = new Date().toISOString().slice(0, 10);
  const [title, setTitle] = useState('');
  const [date, setDate] = useState(today);
  const [time, setTime] = useState('09:00');
  const [loc, setLoc] = useState('');
  const submit = () => {
    const parts = eventDateParts(date);
    onSubmit({
      d: parts.day,
      date,
      dateLabel: parts.dateLabel,
      key: 'custom',
      title: title.trim() || t('mod.eventTitlePh'),
      loc: loc.trim() || t('mod.eventLocPh'),
      time,
      tone: 'brand',
      ic: 'calendar-plus',
    });
  };
  return (
    <Modal icon="calendar-plus" title={t('mod.eventTitle')} onClose={onClose}
      footer={<><Btn variant="ghost" onClick={onClose}>{t('c.cancel')}</Btn><Btn variant="primary" icon="plus" onClick={submit}>{t('mod.addEvent')}</Btn></>}>
      <div className="flex col gap-14">
        <Field label={t('col.subject')}><input className="input" value={title} onChange={(e) => setTitle(e.target.value)} placeholder={t('mod.eventTitlePh')} /></Field>
        <Field label={t('col.date')}><input className="input" type="date" value={date} onChange={(e) => setDate(e.target.value)} /></Field>
        <Field label={t('mod.time')}><input className="input" type="time" value={time} onChange={(e) => setTime(e.target.value)} /></Field>
        <Field label={t('mod.location')}><input className="input" value={loc} onChange={(e) => setLoc(e.target.value)} placeholder={t('mod.eventLocPh')} /></Field>
      </div>
    </Modal>
  );
}

function TaskModal({ onClose, preset, onSubmit }) {
  const { t } = useT();
  const cats = window.YEYE.ticketCats || ['immigration', 'billing', 'documents', 'it'];
  const [catK, setCatK] = useState((preset && preset.catK) || cats[0] || 'immigration');
  const [title, setTitle] = useState((preset && preset.title) || '');
  const [details, setDetails] = useState((preset && preset.details) || '');
  const [deadline, setDeadline] = useState((preset && preset.deadline) || '');
  const submit = () => {
    onSubmit({
      catK,
      title: title.trim() || t('mod.taskTitlePh'),
      details: details.trim(),
      deadline,
      done: false,
      tone: 'warn',
    });
  };
  return (
    <Modal icon="circle-plus" title={t('mod.taskTitle')} onClose={onClose}
      footer={<><Btn variant="ghost" onClick={onClose}>{t('c.cancel')}</Btn><Btn variant="primary" icon="plus" onClick={submit}>{t('mod.addTask')}</Btn></>}>
      <div className="flex col gap-14">
        <Field label={t('col.category')}><select className="select" value={catK} onChange={(e) => setCatK(e.target.value)}>{cats.map((c) => <option key={c} value={c}>{t('cat.' + c)}</option>)}</select></Field>
        <Field label={t('mod.subject')}><input className="input" value={title} onChange={(e) => setTitle(e.target.value)} placeholder={t('mod.taskTitlePh')} /></Field>
        <Field label={t('mod.detailsL')}><textarea className="ta" value={details} onChange={(e) => setDetails(e.target.value)} placeholder={t('mod.detailsPh')} /></Field>
        <Field label={t('mod.deadline')}><input className="input" type="date" value={deadline} onChange={(e) => setDeadline(e.target.value)} /></Field>
      </div>
    </Modal>
  );
}

function ServiceManageModal({ item, onClose, onStatus, openTicket, showToast }) {
  const { t } = useT();
  const inputRef = useRef(null);
  const service = item && item.service;
  const purchase = (item && item.purchase) || {};
  const status = purchase.status || 'active';
  if (!service) return null;
  const upload = () => {
    if (inputRef.current) inputRef.current.click();
  };
  const cancel = () => {
    if (confirm(t('svc.cancelConfirm'))) onStatus(purchase.k || service.k, 'cancelled');
  };
  return (
    <Modal icon="settings" title={t('svc.manageTitle')} sub={serviceTitle(service, t)} onClose={onClose}
      footer={<><Btn variant="ghost" onClick={onClose}>{t('c.close')}</Btn><Btn variant="primary" icon="life-buoy" onClick={() => openTicket(service)}>{t('svc.supportRequest')}</Btn></>}>
      <div className="flex col gap-16">
        <Field label={t('svc.updateStatus')}>
          <select className="select" value={status} onChange={(e) => onStatus(purchase.k || service.k, e.target.value)}>
            {['active', 'paused', 'cancelled'].map((s) => <option key={s} value={s}>{t('st.' + s)}</option>)}
          </select>
        </Field>
        <div className="flex gap-10 wrap">
          <Btn variant="ghost" icon="upload" onClick={upload}>{t('svc.uploadDoc')}</Btn>
          <Btn variant="ghost" icon="life-buoy" onClick={() => openTicket(service)}>{t('svc.supportRequest')}</Btn>
          <Btn variant="ghost" icon="x-circle" onClick={cancel}>{t('svc.cancelService')}</Btn>
        </div>
        <input ref={inputRef} type="file" style={{ display: 'none' }} onChange={() => showToast(t('toast.uploadQueued'))} />
      </div>
    </Modal>
  );
}

function Toast({ text }) {
  if (!text) return null;
  return (
    <div style={{ position: 'fixed', right: 22, bottom: 22, zIndex: 120, background: 'var(--ink-900)', color: '#fff', borderRadius: 10, padding: '11px 14px', boxShadow: 'var(--sh-lg)', fontSize: 13.5, fontWeight: 600 }}>
      {text}
    </div>
  );
}

/* ---------- Notifications panel ---------- */
const NOTIF_TASK_KEY = 'yeye_notif_seen_task_ids_v1';
const NOTIF_PROCESS_KEY = 'yeye_notif_seen_process_step_v1';

function readNotificationTaskIds() {
  try { return new Set(JSON.parse(localStorage.getItem(NOTIF_TASK_KEY) || '[]').map(String)); }
  catch (_) { return new Set(); }
}

function readNotificationProcessSteps(processTypes) {
  try {
    const stored = JSON.parse(localStorage.getItem(NOTIF_PROCESS_KEY) || '{}');
    if (stored && typeof stored === 'object' && !Array.isArray(stored)) return stored;
  } catch (_) {
    const legacy = String(localStorage.getItem(NOTIF_PROCESS_KEY) || '');
    if (legacy && processTypes[0]) return { [processTypes[0]]: legacy };
  }
  return {};
}

function hasUnreadPortalNotifications(tasks, contact) {
  const seenTaskIds = readNotificationTaskIds();
  const hasNewTask = (tasks || []).some((task) => {
    const id = String(task.id || task.ghlTaskId || '');
    return id && !seenTaskIds.has(id);
  });
  if (hasNewTask) return true;
  const tracker = window.PROCESS_TRACKER;
  const processTypes = tracker && contact && tracker.detectProcessTypes ? tracker.detectProcessTypes(contact) : [];
  const seenProcessSteps = readNotificationProcessSteps(processTypes);
  return processTypes.some((processType) => {
    const previousKey = seenProcessSteps[processType];
    if (!previousKey) return false;
    const doc = tracker.processDocFromContact(contact, processType, { completedStages: {} });
    const previousIndex = tracker.PROCESS_STAGES.indexOf(previousKey);
    const currentIndex = tracker.PROCESS_STAGES.indexOf(doc.currentStage);
    return previousIndex >= 0 && currentIndex > previousIndex;
  });
}

function NotifPanel({ onClose, go, tasks, contact }) {
  const { t, lang } = useT();
  const tracker = window.PROCESS_TRACKER;
  const processTypes = tracker && contact && tracker.detectProcessTypes ? tracker.detectProcessTypes(contact) : [];
  const processSnapshots = processTypes.map((processType) => {
    const doc = tracker.processDocFromContact(contact, processType, { completedStages: {} });
    const rows = tracker.getProcessStatus(doc, t, lang) || [];
    const current = rows.find((row) => row.isCurrent) || rows[rows.length - 1];
    return current ? {
      processType,
      key: current.key,
      label: current.label,
      index: tracker.PROCESS_STAGES.indexOf(current.key),
    } : null;
  }).filter(Boolean);
  const currentTaskIds = (tasks || []).map((task) => String(task.id || task.ghlTaskId || '')).filter(Boolean);
  const seenTaskIds = readNotificationTaskIds();
  const seenProcessSteps = readNotificationProcessSteps(processTypes);
  const taskItems = (tasks || []).filter((task) => {
    const id = String(task.id || task.ghlTaskId || '');
    return id && !seenTaskIds.has(id);
  }).map((task) => ({
    id: 'task-' + String(task.id || task.ghlTaskId),
    ic: 'circle-check-big',
    ty: 'info',
    title: t('notif.newTask').replace('{title}', task.title || '—'),
    timestamp: task.createdAt || new Date().toISOString(),
    page: 'tasks',
  }));
  const processItems = processSnapshots.filter((snapshot) => {
    const previousKey = seenProcessSteps[snapshot.processType];
    if (!previousKey) return false;
    const previousIndex = tracker.PROCESS_STAGES.indexOf(previousKey);
    return previousIndex >= 0 && snapshot.index > previousIndex;
  }).map((snapshot) => ({
    id: 'process-' + snapshot.processType + '-' + snapshot.key,
    ic: 'check',
    ty: 'ok',
    title: t('notif.processAdvanced').replace('{step}', snapshot.label),
    timestamp: new Date().toISOString(),
    page: 'dashboard',
  }));
  const items = taskItems.concat(processItems).sort((a, b) => (
    (new Date(b.timestamp).getTime() || 0) - (new Date(a.timestamp).getTime() || 0)
  ));
  const markAllRead = React.useCallback(() => {
    const allSeenTaskIds = Array.from(new Set([...seenTaskIds, ...currentTaskIds]));
    try { localStorage.setItem(NOTIF_TASK_KEY, JSON.stringify(allSeenTaskIds)); } catch (_) {}
    const steps = {};
    processSnapshots.forEach((snapshot) => { steps[snapshot.processType] = snapshot.key; });
    try { localStorage.setItem(NOTIF_PROCESS_KEY, JSON.stringify(steps)); } catch (_) {}
  }, [currentTaskIds.join('|'), processSnapshots.map((snapshot) => snapshot.processType + ':' + snapshot.key).join('|')]);
  React.useEffect(() => { markAllRead(); }, [markAllRead]);
  const col = { bad: 'var(--bad)', warn: 'var(--warn)', ok: 'var(--ok)', info: 'var(--info)' };
  const bg = { bad: 'var(--bad-bg)', warn: 'var(--warn-bg)', ok: 'var(--ok-bg)', info: 'var(--info-bg)' };
  return (
    <Panel title={t('sec.notifications')} onClose={onClose} footer={<Btn variant="ghost" className="btn-block" onClick={() => { markAllRead(); onClose(); }}>{t('c.markRead')}</Btn>}>
      <div className="rowlist" style={{ margin: '-6px 0' }}>
        {items.length === 0 ? (
          <div className="lrow">
            <div className="l-ic" style={{ background: 'var(--surface-3)', color: 'var(--ink-300)' }}><Icon name="bell-off" size={18} /></div>
            <div className="l-bd"><div className="l-t" style={{ fontSize: 13, fontWeight: 500 }}>{t('notif.empty')}</div></div>
          </div>
        ) : items.map((n) => (
          <div key={n.id} className="lrow pointer" onClick={() => { onClose(); go(n.page); }}>
            <div className="l-ic" style={{ background: bg[n.ty], color: col[n.ty] }}><Icon name={n.ic} size={18} /></div>
            <div className="l-bd"><div className="l-t" style={{ fontSize: 13, fontWeight: 500 }}>{n.title}</div><div className="l-s">{new Date(n.timestamp).toLocaleString()}</div></div>
          </div>
        ))}
      </div>
    </Panel>
  );
}

/* ============================================================
   APP ROOT
   ============================================================ */
const TITLES = {
  dashboard: ['Dashboard', null], services: ['Services', 'Purchase & manage services'],
  documents: ['My Docs', null], appointments: ['Appointments', null],
  messages: ['Messages', null], tasks: ['My Tasks', null], invoices: ['Payments', null],
  reports: ['Reports', null], support: ['Support', null], profile: ['Profile', null], settings: ['Settings', null],
};

async function bridgeExpatTicketToFirestore(_localTicket, _ticket, _currentExpatUser) {
  // Firestore bridge removed in v24 — ticket flow now runs purely through GHL tasks.
}

function readContactValue(contact, matcher) {
  const fields = (contact && contact.customFields) || [];
  for (const f of fields) {
    const hay = String((f.key || '') + ' ' + (f.name || '') + ' ' + (f.id || '')).toLowerCase();
    if (matcher(hay)) return String(f.value || '').toLowerCase();
  }
  return '';
}

function hasManagerApproval(contact) {
  const tags = ((contact && contact.tags) || []).map((x) => String(x).toLowerCase());
  if (tags.some((x) => /manager.*(approved|confirmed)|approved.*manager|request_approved|hr_approved/.test(x))) return true;
  const val = readContactValue(contact, (k) => k.includes('manager') && (k.includes('approval') || k.includes('status')));
  return /approved|confirmed|onay|schval/.test(val);
}

function isFreshPortalContact(contact) {
  if (!contact) return true;
  const contactId = String(contact.contactId || contact.id || '');
  if (contactId === 'testblue-demo-20260619') return true;
  const tags = [
    ...((contact.tags || [])),
    ...((contact.raw && contact.raw.tags) || []),
  ].map((x) => String(x || '').toLowerCase().trim()).filter(Boolean);
  const meaningful = tags.filter((tag) => tag !== 'expat_dashboard' && tag !== 'portal_signup' && tag !== 'expat_dashboard_signup');
  if (!meaningful.length) return true;
  return !meaningful.some((tag) => (
    /blue[_ -]?card|employee[_ -]?card|family|business|osvc|relocation|payment|paid|purchased|form_created|process|document/.test(tag)
  ));
}
window.isFreshPortalContact = isFreshPortalContact;

const YEYE_OPTIMISTIC_EDIT_MAX_AGE = 60000;

function optimisticValuesMatch(left, right) {
  if (left == null && right == null) return true;
  return String(left == null ? '' : left) === String(right == null ? '' : right);
}

function contactCustomField(contact, field) {
  const raw = (contact && contact.raw) || contact || {};
  const fields = (contact && contact.customFields) || raw.customFields || [];
  if (!Array.isArray(fields)) return null;
  const expectedId = String((field && field.id) || '');
  const expectedKey = String((field && field.key) || '').replace(/^contact\./, '').toLowerCase();
  return fields.find((candidate) => {
    const id = String(candidate && (candidate.id || candidate.fieldId || candidate.customFieldId) || '');
    const key = String(candidate && (candidate.key || candidate.fieldKey || candidate.name) || '')
      .replace(/^contact\./, '')
      .toLowerCase();
    return (expectedId && id === expectedId) || (expectedKey && key === expectedKey);
  }) || null;
}

function reconcileOptimisticEdits(previous, contact, now = Date.now()) {
  if (!contact) return previous;
  const contactId = String(contact.contactId || contact.id || '');
  const pending = previous.get(contactId);
  if (!pending) return previous;
  const raw = contact.raw || contact;
  const updates = {};
  Object.entries(pending.updates || {}).forEach(([key, edit]) => {
    const freshValue = contact[key] !== undefined ? contact[key] : raw[key];
    if (edit.expiresAt > now && !optimisticValuesMatch(freshValue, edit.value)) updates[key] = edit;
  });
  const customFieldUpdates = {};
  Object.entries(pending.customFieldUpdates || {}).forEach(([identity, edit]) => {
    const freshField = contactCustomField(contact, edit);
    if (edit.expiresAt > now && !optimisticValuesMatch(freshField && freshField.value, edit.value)) {
      customFieldUpdates[identity] = edit;
    }
  });
  const next = new Map(previous);
  if (Object.keys(updates).length || Object.keys(customFieldUpdates).length) {
    next.set(contactId, { updates, customFieldUpdates });
  } else {
    next.delete(contactId);
  }
  return next;
}

function mergeOptimisticContact(contact, pending) {
  if (!contact || !pending) return contact;
  const merged = { ...contact };
  Object.entries(pending.updates || {}).forEach(([key, edit]) => {
    merged[key] = edit.value;
  });
  const customEdits = Object.values(pending.customFieldUpdates || {});
  if (!customEdits.length) return merged;
  const mergeFields = (fields) => {
    const next = Array.isArray(fields) ? fields.map((field) => ({ ...field })) : [];
    customEdits.forEach((edit) => {
      const expectedId = String(edit.id || '');
      const expectedKey = String(edit.key || '').replace(/^contact\./, '').toLowerCase();
      const index = next.findIndex((field) => {
        const id = String(field && (field.id || field.fieldId || field.customFieldId) || '');
        const key = String(field && (field.key || field.fieldKey || field.name) || '')
          .replace(/^contact\./, '')
          .toLowerCase();
        return (expectedId && id === expectedId) || (expectedKey && key === expectedKey);
      });
      if (index >= 0) {
        next[index] = { ...next[index], value: edit.value };
      } else {
        next.push({
          id: edit.id,
          fieldId: edit.id,
          key: String(edit.key || '').replace(/^contact\./, ''),
          value: edit.value,
        });
      }
    });
    return next;
  };
  merged.customFields = mergeFields(contact.customFields || (contact.raw && contact.raw.customFields));
  if (contact.raw && contact.raw !== contact) {
    merged.raw = { ...contact.raw, customFields: mergeFields(contact.raw.customFields || contact.customFields) };
  }
  return merged;
}

function App() {
  const [authState, setAuthState] = useState(() => window.YEYE_AUTH.getState());
  useEffect(() => window.YEYE_AUTH.subscribe(setAuthState), []);
  const [optimisticEdits, setOptimisticEdits] = useState(() => new Map());
  const applyOptimisticUpdate = React.useCallback((contactId, patch) => {
    const id = String(contactId || '');
    if (!id || !patch) return;
    const expiresAt = Date.now() + YEYE_OPTIMISTIC_EDIT_MAX_AGE;
    setOptimisticEdits((previous) => {
      const next = new Map(previous);
      const current = next.get(id) || { updates: {}, customFieldUpdates: {} };
      const updates = { ...current.updates };
      Object.entries(patch.updates || {}).forEach(([key, value]) => {
        updates[key] = { value, expiresAt };
      });
      const customFieldUpdates = { ...current.customFieldUpdates };
      (patch.customFieldUpdates || []).forEach((field) => {
        const identity = String(field.id || '') || String(field.key || '').toLowerCase();
        if (identity) customFieldUpdates[identity] = { ...field, expiresAt };
      });
      if (Object.keys(updates).length || Object.keys(customFieldUpdates).length) {
        next.set(id, { updates, customFieldUpdates });
      } else {
        next.delete(id);
      }
      return next;
    });
  }, []);
  const rawContactId = String((authState.contact && (authState.contact.contactId || authState.contact.id)) || '');
  const optimisticContact = React.useMemo(
    () => mergeOptimisticContact(authState.contact, optimisticEdits.get(rawContactId)),
    [authState.contact, optimisticEdits, rawContactId]
  );
  useEffect(() => {
    setOptimisticEdits((previous) => reconcileOptimisticEdits(previous, authState.contact));
  }, [authState.contact]);
  useEffect(() => {
    let nextExpiry = Infinity;
    optimisticEdits.forEach((pending) => {
      Object.values(pending.updates || {}).forEach((edit) => { nextExpiry = Math.min(nextExpiry, edit.expiresAt); });
      Object.values(pending.customFieldUpdates || {}).forEach((edit) => { nextExpiry = Math.min(nextExpiry, edit.expiresAt); });
    });
    if (!Number.isFinite(nextExpiry)) return undefined;
    const timer = window.setTimeout(() => {
      setOptimisticEdits((previous) => {
        let next = previous;
        previous.forEach((_, contactId) => {
          const contact = contactId === rawContactId ? authState.contact : { id: contactId };
          next = reconcileOptimisticEdits(next, contact, Date.now());
        });
        return next;
      });
    }, Math.max(0, nextExpiry - Date.now()) + 10);
    return () => window.clearTimeout(timer);
  }, [optimisticEdits, authState.contact, rawContactId]);
  useEffect(() => {
    window.YEYE_CONTACT_STATE = {
      applyOptimisticUpdate,
      getContact: () => optimisticContact,
    };
    return () => {
      if (window.YEYE_CONTACT_STATE && window.YEYE_CONTACT_STATE.applyOptimisticUpdate === applyOptimisticUpdate) {
        delete window.YEYE_CONTACT_STATE;
      }
    };
  }, [applyOptimisticUpdate, optimisticContact]);
  const initialBlankProfile = (() => {
    const snap = window.YEYE_AUTH.getState();
    return isFreshPortalContact(snap.contact);
  })();
  const [clientType, setClientType] = useState('employee_card');
  const [branding, setBranding] = useState('individual');
  const [page, setPage] = useState('dashboard');
  const [, refreshWelcome] = useState(0);
  const [mini, setMini] = useState(false);
  const [drawer, setDrawer] = useState(false);
  const cart = useCartState();
  const [cartOpen, setCartOpen] = useState(false);
  const [checkoutOpen, setCheckoutOpen] = useState(false);
  const [modal, setModal] = useState(null);
  const [modalData, setModalData] = useState(null);
  const [tickets, setTickets] = useState(() => initialBlankProfile ? [] : window.YEYE.tickets.slice());
  const [calendarEvents, setCalendarEvents] = useState(() => initialBlankProfile ? [] : (typeof CAL_EVENTS !== 'undefined' ? CAL_EVENTS.slice() : []));
  const [tasks, setTasks] = useState(() => []);
  const [userTasks, setUserTasks] = useState(() => {
    try { return JSON.parse(localStorage.getItem('yeye-user-tasks') || '[]'); } catch (_) { return []; }
  });
  const [unpaidInvoiceCount, setUnpaidInvoiceCount] = useState(0);
  useEffect(() => {
    const handler = (e) => setUnpaidInvoiceCount(Number((e.detail && e.detail.unpaid) || 0));
    window.addEventListener('yeye:invoices-changed', handler);
    return () => window.removeEventListener('yeye:invoices-changed', handler);
  }, []);
  const [taskSync, setTaskSync] = useState({ syncing: false, error: '', lastSync: null });
  const [purchasedServices, setPurchasedServices] = useState([]);
  const [calendarSync, setCalendarSync] = useState({
    connected: !!(authState.calendarToken && authState.calendarExpiresAt > Date.now()),
    syncing: false,
    error: '',
  });
  const [fieldMappings, setFieldMappings] = useState([]);
  const [contactSync, setContactSync] = useState({ syncing: false, lastSync: null, error: '' });
  const [toast, setToast] = useState('');
  const toastTimer = useRef(null);
  const checkoutReturnHandled = useRef(false);
  const [notif, setNotif] = useState(false);
  const [lang, setLang] = useState(() => initialPortalLang());
  const [currency, setCurrency] = useState(() => localStorage.getItem('yeye-currency') || 'EUR');
  const authed = window.YEYE_AUTH.isAuthed();
  const checking = !!authState.gateChecking;
  const [contactLoadingTimedOut, setContactLoadingTimedOut] = useState(false);

  useEffect(() => {
    if (!authed || authState.contact) {
      setContactLoadingTimedOut(false);
      return undefined;
    }
    setContactLoadingTimedOut(false);
    const timer = window.setTimeout(() => setContactLoadingTimedOut(true), 10000);
    return () => window.clearTimeout(timer);
  }, [authed, !!authState.contact, authState.user && authState.user.email]);

  const t = React.useMemo(() => ((k) => (window.I18N[lang] && window.I18N[lang][k]) || window.I18N.en[k] || k), [lang]);
  const dict = window.I18N[lang] || window.I18N.en;
  const currentEmail = String((authState.user && authState.user.email) || (optimisticContact && optimisticContact.email) || '').toLowerCase();
  const isAdminUser = /@yeyeagency\.com$|^support@yeyeagency\.com$/.test(currentEmail);
  const setLangP = (l) => {
    setLang(l);
    localStorage.setItem('yeye-lang', l);
    if (window.YEYE_PREFS) window.YEYE_PREFS.pushToCloud({ language: l });
  };
  const setCurrencyP = (c) => {
    setCurrency(c);
    localStorage.setItem('yeye-currency', c);
    if (window.YEYE_PREFS) window.YEYE_PREFS.pushToCloud({ currency: c });
  };
  const addUserTask = async (input) => {
    const payload = typeof input === 'string' ? { title: input } : (input || {});
    const clean = String(payload.title || '').trim();
    if (!clean) return;
    const dueLabel = payload.deadline
      ? new Date(payload.deadline).toLocaleDateString()
      : new Date().toLocaleDateString();
    const localId = 'SELF-' + Date.now();
    const nextTask = {
      id: localId,
      title: clean,
      category: payload.catK || 'other',
      details: payload.details || '',
      due: payload.deadline || '',
      dueRaw: payload.deadline || '',
      done: false,
      dateLabel: dueLabel,
      source: 'self',
      syncing: true,
      localOnly: true,
    };
    setUserTasks((prev) => {
      const next = [nextTask, ...prev];
      try { localStorage.setItem('yeye-user-tasks', JSON.stringify(next)); } catch (_) {}
      return next;
    });
    try {
      const contact = window.YEYE_AUTH && window.YEYE_AUTH.getState
        ? (window.YEYE_AUTH.getState().contact || authState.contact)
        : authState.contact;
      const email = (authState.user && authState.user.email) || (contact && contact.email) || '';
      const contactId = contact && (contact.contactId || contact.id);
      if (!contactId || !email) throw new Error('missing-contact');
      const url = GHL_TASK_SYNC_BASE + '/contacts/' + encodeURIComponent(contactId) + '/tasks';
      const res = await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          email,
          category: nextTask.category,
          title: clean,
          details: nextTask.details,
          priority: 'normal',
          dueDate: nextTask.due,
          source: 'self',
        }),
      });
      if (!res.ok) {
        const msg = await res.text().catch(() => '');
        console.warn('GHL self-task create failed', res.status, msg);
        throw new Error('ghl-self-task-' + res.status);
      }
      const data = await res.json().catch(() => ({}));
      setUserTasks((prev) => {
        const next = prev.map((row) => row.id === localId
          ? { ...row, ghlTaskId: data.ghl_task_id, assigneeEmail: data.assignee_email, syncing: false, syncedAt: Date.now(), localOnly: !data.ghl_task_id }
          : row);
        try { localStorage.setItem('yeye-user-tasks', JSON.stringify(next)); } catch (_) {}
        return next;
      });
      syncGhlTasks();
    } catch (err) {
      console.warn('Self-task GHL sync failed', err);
      setUserTasks((prev) => {
        const next = prev.map((row) => row.id === localId
          ? { ...row, syncing: false, syncError: String((err && err.message) || 'sync-failed') }
          : row);
        try { localStorage.setItem('yeye-user-tasks', JSON.stringify(next)); } catch (_) {}
        return next;
      });
    }
  };
  const userPurchaseKey = ((authState.user && authState.user.email) || (optimisticContact && optimisticContact.email) || 'guest').toLowerCase();
  const activeContactId = String((optimisticContact && (optimisticContact.contactId || optimisticContact.id)) || '');
  const contactPaidServices = window.YEYE_PURCHASES && window.YEYE_PURCHASES.getPaidServices
    ? window.YEYE_PURCHASES.getPaidServices(optimisticContact)
    : [];
  const blankProfile = isFreshPortalContact(optimisticContact) && contactPaidServices.length === 0;
  const showToast = (msg) => {
    setToast(msg);
    window.clearTimeout(toastTimer.current);
    toastTimer.current = window.setTimeout(() => setToast(''), 2400);
  };
  useEffect(() => { window.YEYE_TOAST = showToast; return () => { if (window.YEYE_TOAST === showToast) window.YEYE_TOAST = null; }; }, []);
  useEffect(() => {
    window.YEYE_CART_OPEN = () => setCartOpen(true);
    return () => { window.YEYE_CART_OPEN = null; };
  }, []);
  useEffect(() => {
    if (checkoutReturnHandled.current || !window.YEYE_BACKEND || !window.YEYE_BACKEND.getStripeCheckoutSession) return;
    const params = new URLSearchParams(window.location.search || '');
    const sessionId = params.get('checkout_session_id');
    if (!sessionId) return;
    checkoutReturnHandled.current = true;
    (async () => {
      try {
        const statusResult = await window.YEYE_BACKEND.getStripeCheckoutSession(sessionId);
        if (statusResult && (statusResult.status === 'complete' || statusResult.paymentStatus === 'paid')) {
          const paidItems = (window.YEYE_CART && window.YEYE_CART.getState && window.YEYE_CART.getState().items) || [];
          if (paidItems.length) {
            const existing = readPurchaseStore()[userPurchaseKey] || [];
	            const normalizeKey = typeof window.YEYE_NORMALIZE_SERVICE_KEY === 'function' ? window.YEYE_NORMALIZE_SERVICE_KEY : (key) => key;
	            const byKey = new Map((existing || []).map((p) => [normalizeKey(p.k), p]));
	            paidItems.forEach((item) => {
	              const rawKey = cartPurchaseKey(item);
	              const key = normalizeKey(rawKey);
	              if (key && !byKey.has(key)) {
	                byKey.set(key, { id: 'PS-stripe-' + key + '-' + Date.now(), k: rawKey, status: 'active', source: 'stripe_checkout', purchasedAt: new Date().toISOString() });
	              }
	            });
            const next = Array.from(byKey.values());
            const store = readPurchaseStore();
            store[userPurchaseKey] = next;
            localStorage.setItem(PURCHASE_KEY, JSON.stringify(store));
            setPurchasedServices(next);
          }
          if (window.YEYE_CART) window.YEYE_CART.clear();
          setCartOpen(false);
          setCheckoutOpen(false);
          setPage('invoices');
          showToast(t('cart.autoCleared') || 'Payment received - cart cleared.');
        }
      } catch (_) {
        // Stripe webhook handles the durable backend state; this is only UI cleanup.
      } finally {
        try {
          const url = new URL(window.location.href);
          url.searchParams.delete('checkout_session_id');
          window.history.replaceState({}, '', url.pathname + url.search + url.hash);
        } catch (_) {}
      }
    })();
  }, [t, userPurchaseKey]);

  // Derive case type from GHL tags (falls back to employee_card)
  const deriveCaseType = (tags) => {
    const ts = (tags || []).map((x) => String(x).toLowerCase());
    if (ts.some((x) => x.startsWith('blue_card') || x.includes('bluecard'))) return 'blue_card';
    if (ts.some((x) => x.startsWith('family'))) return 'family_reunion';
    if (ts.some((x) => x.startsWith('business'))) return 'business_visa';
    if (ts.some((x) => x.startsWith('osvc'))) return 'osvc';
    if (ts.some((x) => x.startsWith('relocation'))) return 'relocation';
    if (ts.some((x) => x.startsWith('employee_card') || x.includes('employee_onboarding'))) return 'employee_card';
    return 'employee_card';
  };

  // Build live case-type data from GHL contact + mock template
  const buildLiveCt = (mockCt, contact) => {
    if (!contact) return mockCt;
    const cap = (w) => w ? w.charAt(0).toUpperCase() + w.slice(1) : '';
    const full = [contact.firstName, contact.lastName].filter(Boolean).join(' ').split(' ').map(cap).join(' ').trim();
    const addrField = (contact.customFields || []).find((f) => /correspondence|address|adres/i.test(f.key || ''));
    let region = mockCt.region;
    if (addrField && typeof addrField.value === 'string') {
      const m = addrField.value.match(/([A-Za-zÀ-ž\s]+)\/([A-Za-zÀ-ž\s]+)\s*$/);
      if (m) region = cap(m[1].trim()) + ', ' + cap(m[2].trim());
    }
    const caseId = 'YE-' + String(contact.contactId || '').slice(0, 8).toUpperCase();
    return { ...mockCt, who: full || mockCt.who, region, caseId };
  };

  // Live client-type derived from contact tags; user can still switch via the demo ClientTypeMenu
  const derivedKey = optimisticContact ? deriveCaseType(optimisticContact.tags) : clientType;
  const effectiveKey = blankProfile ? null : (clientType === 'employee_card' && optimisticContact ? derivedKey : clientType);
  const contactDisplayName = optimisticContact
    ? [optimisticContact.firstName, optimisticContact.lastName].filter(Boolean).join(' ').trim()
    : '';
  const mockCt = effectiveKey
    ? (window.YEYE.clientTypes[effectiveKey] || window.YEYE.clientTypes.employee_card)
    : (window.YEYE.clientTypes.blank || {
      key: 'blank', icon: 'user-round', who: contactDisplayName, region: '',
      org: null, caseId: '', steps: null, curr: '', currTone: '', pct: 0,
    });
  const ctData = blankProfile ? { ...mockCt, who: contactDisplayName } : buildLiveCt(mockCt, optimisticContact);
  const managerApproved = hasManagerApproval(optimisticContact);
  const liveCtData = React.useMemo(() => {
    if (!managerApproved || effectiveKey !== 'employee_card') return ctData;
    return { ...ctData, curr: 'Approved', currTone: 'ok', pct: Math.max(ctData.pct || 0, 70), managerApproved: true };
  }, [ctData, managerApproved, effectiveKey]);

  const go = (k) => { if (k === 'logout') { window.YEYE_AUTH.signOut(); return; } setPage(k); window.scrollTo && window.scrollTo(0, 0); const c = document.querySelector('.content'); if (c) c.scrollTop = 0; };
  window.YEYE_GO = go;
  const openModal = (kind, data) => { setModalData(data || null); setModal(kind); };
  window.YEYE_OPEN_MODAL = openModal;

  useEffect(() => {
    const store = readPurchaseStore();
    if (blankProfile && store[userPurchaseKey]) {
      delete store[userPurchaseKey];
      localStorage.setItem(PURCHASE_KEY, JSON.stringify(store));
    }
    setPurchasedServices(store[userPurchaseKey] || []);
  }, [userPurchaseKey, blankProfile]);

  // Sync purchased services from GHL contact.
  // Source of truth: contact.customFields[paid_services] (JSON). Legacy fallback: *_payment_received tags.
  // Full replace of ghl_tag-sourced entries so removing a tag/record in GHL also removes it here.
  useEffect(() => {
    const contact = optimisticContact;
    if (!contact) return;
    const paidList = window.YEYE_PURCHASES && window.YEYE_PURCHASES.getPaidServices
      ? window.YEYE_PURCHASES.getPaidServices(contact)
      : [];
    const derived = paidList.map((rec) => ({
      id: 'PS-' + (rec.source === 'paid_services' ? 'field-' : 'tag-') + rec.k,
      k: rec.k,
      status: rec.status || 'active',
      source: rec.source || 'paid_services',
      purchasedAt: rec.paidAt || new Date().toISOString(),
      invoiceId: rec.invoiceId || '',
      productId: rec.productId || '',
      amount: rec.amount || '',
      currency: rec.currency || '',
    }));
    setPurchasedServices((prev) => {
      const kept = (prev || []).filter((p) => p.source !== 'ghl_tag' && p.source !== 'paid_services');
      const byKey = new Map();
      kept.forEach((p) => byKey.set(p.k, p));
      derived.forEach((d) => { byKey.set(d.k, d); });
      const next = Array.from(byKey.values());
      try {
        const store = readPurchaseStore();
        store[userPurchaseKey] = next;
        localStorage.setItem(PURCHASE_KEY, JSON.stringify(store));
      } catch (_) {}
      return next;
    });
  }, [optimisticContact, userPurchaseKey]);

  // Stripe-return force refresh — when URL has ?refresh=1 (checkout success),
  // pull fresh contact from GHL so newly-appended paid_services shows immediately.
  useEffect(() => {
    if (typeof window === 'undefined') return;
    const params = new URLSearchParams(window.location.search || '');
    if (params.get('refresh') !== '1') return;
    if (window.YEYE_AUTH && window.YEYE_AUTH.refreshContactFromGhl) {
      window.YEYE_AUTH.refreshContactFromGhl().catch(() => {});
    } else if (refreshContact) {
      refreshContact();
    }
    params.delete('refresh');
    const next = params.toString();
    const url = window.location.pathname + (next ? '?' + next : '') + window.location.hash;
    try { window.history.replaceState({}, '', url); } catch (_) {}
  }, []);

  useEffect(() => {
    if (!blankProfile) return;
    setTickets([]);
    setCalendarEvents([]);
  }, [blankProfile]);

  // When a GHL tag flips a service to "paid/owned", drop matching cart items so
  // they no longer show as "payment pending" in the user's cart.
  useEffect(() => {
    if (!window.YEYE_CART) return;
    const ownedKeys = new Set((purchasedServices || [])
      .map((p) => (typeof window.YEYE_NORMALIZE_SERVICE_KEY === 'function' ? window.YEYE_NORMALIZE_SERVICE_KEY(p.k) : p.k))
      .filter(Boolean));
    if (!ownedKeys.size) return;
    const currentItems = (window.YEYE_CART.getState().items || []);
    const filtered = currentItems.filter((item) => {
      const key = typeof window.YEYE_NORMALIZE_SERVICE_KEY === 'function' ? window.YEYE_NORMALIZE_SERVICE_KEY(item.productKey) : item.productKey;
      return !ownedKeys.has(key);
    });
    if (filtered.length !== currentItems.length) {
      const removedItems = currentItems.filter((it) => !filtered.includes(it));
      removedItems.forEach((it) => window.YEYE_CART.removeItem(it.id));
      if (removedItems.length && window.YEYE_TOAST) {
        const l = (localStorage.getItem('yeye-lang') || 'en').toLowerCase();
        const msg = (window.I18N && window.I18N[l] && window.I18N[l]['cart.autoCleared']) || 'Payment received — item removed from cart.';
        window.YEYE_TOAST(msg);
      }
    }
  }, [purchasedServices]);

  useEffect(() => {
    if (!authed || checking) return;
    const contact = authState.contact;
    if (!contact || !(contact.contactId || contact.id)) return;
    syncGhlTasks();
  }, [authed, checking, blankProfile, authState.contact?.contactId, authState.contact?.id, authState.user?.email]);

  useEffect(() => {
    const connected = !!(authState.calendarToken && authState.calendarExpiresAt > Date.now());
    setCalendarSync((prev) => ({ ...prev, connected }));
  }, [authState.calendarToken, authState.calendarExpiresAt]);

  useEffect(() => {
    if (!window.YEYE_FIELD_MAPPING || !window.YEYE_FIELD_MAPPING.load) return undefined;
    let cancelled = false;
    window.YEYE_FIELD_MAPPING.load()
      .then((rows) => { if (!cancelled) setFieldMappings(rows || []); })
      .catch((err) => console.warn('Failed to load Blue/Employee field mapping.', err));
    return () => { cancelled = true; };
  }, []);

  const persistPurchases = (next) => {
    const store = readPurchaseStore();
    store[userPurchaseKey] = next;
    localStorage.setItem(PURCHASE_KEY, JSON.stringify(store));
    setPurchasedServices(next);
  };
  const markPaidItemsPurchased = (items) => {
    const paidItems = Array.isArray(items) ? items : [];
    if (!paidItems.length) return;
	    const normalizeKey = typeof window.YEYE_NORMALIZE_SERVICE_KEY === 'function' ? window.YEYE_NORMALIZE_SERVICE_KEY : (key) => key;
	    const byKey = new Map((purchasedServices || []).map((p) => [normalizeKey(p.k), p]));
	    paidItems.forEach((item) => {
	      const rawKey = cartPurchaseKey(item);
	      const key = normalizeKey(rawKey);
	      if (key && !byKey.has(key)) {
	        byKey.set(key, { id: 'PS-stripe-' + key + '-' + Date.now(), k: rawKey, status: 'active', source: 'stripe_checkout', purchasedAt: new Date().toISOString() });
	      }
	    });
    persistPurchases(Array.from(byKey.values()));
  };
  const addPurchasedService = (service) => {
    const exists = purchasedServices.some((x) => x.k === service.k);
    const next = exists ? purchasedServices : [{ id: 'PS-' + Date.now(), k: service.k, status: 'active', purchasedAt: new Date().toISOString() }, ...purchasedServices];
    persistPurchases(next);
    setModal(null);
    showToast(t('toast.servicePurchased'));
    setPage('invoices');
  };
  const updatePurchasedStatus = (serviceKey, status) => {
    const existing = purchasedServices.find((x) => x.k === serviceKey) || { id: 'PS-' + Date.now(), k: serviceKey, purchasedAt: new Date().toISOString() };
    const rest = purchasedServices.filter((x) => x.k !== serviceKey);
    persistPurchases([{ ...existing, status }, ...rest]);
    setModalData((d) => d ? { ...d, purchase: { ...existing, status } } : d);
    showToast(t('toast.statusUpdated'));
  };
  const addTicket = async (ticket) => {
    const nextId = 'SUP-' + String(3200 + tickets.length + 1);
    const localTicket = {
      id: nextId,
      tk: 'custom',
      subject: ticket.subject,
      catK: ticket.catK,
      st: 'open',
      stc: 'info',
      upd: 'now',
      details: ticket.details,
      priority: ticket.priority,
      deadline: ticket.deadline || '',
    };
    setTickets((prev) => [localTicket, ...prev]);
    setModal(null);
    showToast(t('toast.ticketCreated'));
    bridgeExpatTicketToFirestore(localTicket, ticket, authState.user || optimisticContact);
    try {
      const contact = window.YEYE_AUTH.getState().contact || authState.contact;
      const email = (authState.user && authState.user.email) || (contact && contact.email) || '';
      const contactId = contact && (contact.contactId || contact.id);
      if (!contactId || !email) return;
      const url = GHL_TASK_SYNC_BASE + '/contacts/' + encodeURIComponent(contactId) + '/tasks';
      const res = await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          email,
          category: ticket.catK,
          title: '[' + (ticket.catK || 'other') + '] ' + (ticket.subject || ''),
          details: ticket.details || '',
          priority: ticket.priority || 'normal',
          dueDate: ticket.deadline || '',
        }),
      });
      if (!res.ok) {
        const msg = await res.text().catch(() => '');
        console.warn('GHL task create failed', res.status, msg);
        throw new Error('ghl-task-create-' + res.status);
      }
      const data = await res.json().catch(() => ({}));
      setTickets((prev) => prev.map((row) => row.id === nextId ? { ...row, ghlTaskId: data.ghl_task_id, assigneeEmail: data.assignee_email } : row));
      syncGhlTasks();
    } catch (err) {
      console.warn('Ticket GHL sync failed', err);
      // Local + Firestore side already succeeded — no user-facing raw error.
      // Show friendly notice only if the local ticket also failed elsewhere; otherwise stay silent.
    }
  };
  const upsertCalendarEvents = (incoming) => {
    setCalendarEvents((prev) => {
      const byId = new Map();
      incoming.concat(prev).forEach((ev) => {
        const id = ev.id || [ev.key, ev.title, ev.date, ev.time].filter(Boolean).join(':') || String(Math.random());
        if (!byId.has(id)) byId.set(id, ev);
      });
      return Array.from(byId.values());
    });
  };
  const fetchGoogleCalendarEvents = async (token) => {
    const accessToken = token || (window.YEYE_AUTH.getCalendarToken && window.YEYE_AUTH.getCalendarToken());
    if (!accessToken) throw new Error(t('cal.needConnect'));
    const now = new Date();
    const later = new Date(now.getTime() + 90 * 24 * 60 * 60 * 1000);
    const url = 'https://www.googleapis.com/calendar/v3/calendars/primary/events'
      + '?singleEvents=true&orderBy=startTime&maxResults=20'
      + '&timeMin=' + encodeURIComponent(now.toISOString())
      + '&timeMax=' + encodeURIComponent(later.toISOString());
    const res = await fetch(url, { headers: { Authorization: 'Bearer ' + accessToken } });
    if (!res.ok) throw new Error('Google Calendar ' + res.status);
    const data = await res.json();
    const rows = (data.items || []).filter((item) => item.status !== 'cancelled').map((item) => {
      const startRaw = (item.start && (item.start.dateTime || item.start.date)) || '';
      const start = new Date(item.start && item.start.dateTime ? item.start.dateTime : startRaw + 'T09:00:00');
      const safe = Number.isNaN(start.getTime()) ? new Date() : start;
      return {
        id: 'google:' + item.id,
        key: 'google',
        title: item.summary || t('cal.googleEvent'),
        loc: item.location || 'Google Calendar',
        d: safe.getDate(),
        date: safe.toISOString().slice(0, 10),
        dateLabel: safe.toLocaleDateString(undefined, { day: 'numeric', month: 'short' }),
        time: item.start && item.start.date ? t('cal.allDay') : safe.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
        tag: 'confirmed',
        tone: 'ok',
        tagc: 'ok',
        ic: 'calendar-check',
      };
    });
    upsertCalendarEvents(rows);
    return rows;
  };
  const connectGoogleCalendar = async () => {
    setCalendarSync({ connected: calendarSync.connected, syncing: true, error: '' });
    try {
      const token = await window.YEYE_AUTH.connectGoogleCalendar();
      await fetchGoogleCalendarEvents(token);
      setCalendarSync({ connected: true, syncing: false, error: '' });
      showToast(t('toast.calendarConnected'));
    } catch (err) {
      const msg = err && err.message ? err.message : String(err || '');
      setCalendarSync({ connected: false, syncing: false, error: msg });
      showToast(msg);
    }
  };
  const syncGoogleCalendar = async () => {
    setCalendarSync((prev) => ({ ...prev, syncing: true, error: '' }));
    try {
      await fetchGoogleCalendarEvents();
      setCalendarSync({ connected: true, syncing: false, error: '' });
      showToast(t('toast.calendarSynced'));
    } catch (err) {
      const msg = err && err.message ? err.message : String(err || '');
      setCalendarSync((prev) => ({ ...prev, syncing: false, error: msg }));
      showToast(msg);
    }
  };
  const addCalendarEvent = async (event) => {
    setCalendarEvents((prev) => [event, ...prev]);
    const token = window.YEYE_AUTH.getCalendarToken && window.YEYE_AUTH.getCalendarToken();
    if (token && event.date) {
      try {
        const start = new Date(event.date + 'T' + (event.time || '09:00') + ':00');
        const end = new Date(start.getTime() + 60 * 60 * 1000);
        const body = {
          summary: event.title,
          location: event.loc,
          start: { dateTime: start.toISOString(), timeZone: browserTimeZone() },
          end: { dateTime: end.toISOString(), timeZone: browserTimeZone() },
        };
        await fetch('https://www.googleapis.com/calendar/v3/calendars/primary/events', {
          method: 'POST',
          headers: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' },
          body: JSON.stringify(body),
        });
      } catch (err) {
        console.warn('Google Calendar event create failed', err);
        showToast(t('toast.eventLocalOnly'));
      }
    }
    setModal(null);
    showToast(t('toast.eventCreated'));
  };
  const addTask = (task) => {
    setTasks((prev) => [normalizePortalTask(task), ...prev]);
    setModal(null);
    showToast(t('toast.taskCreated'));
  };
  const syncGhlTasks = async () => {
    const contact = window.YEYE_AUTH.getState().contact || authState.contact;
    const email = (authState.user && authState.user.email) || (contact && contact.email) || '';
    const contactId = contact && (contact.contactId || contact.id);
    if (!contactId || !email) {
      setTasks([]);
      return [];
    }
    setTaskSync((prev) => ({ ...prev, syncing: true, error: '' }));
    try {
      const url = GHL_TASK_SYNC_BASE + '/contacts/' + encodeURIComponent(contactId) + '/tasks?email=' + encodeURIComponent(email);
      const res = await fetch(url);
      if (!res.ok) throw new Error('GHL task sync failed (' + res.status + ')');
      const data = await res.json();
      const rows = (data.items || []).map(normalizePortalTask);
      setTasks(rows);
      setTaskSync({ syncing: false, error: '', lastSync: new Date().toISOString() });
      return rows;
    } catch (err) {
      const msg = err && err.message ? err.message : String(err || '');
      setTaskSync({ syncing: false, error: msg, lastSync: new Date().toISOString() });
      return [];
    }
  };
  const toggleGhlTask = async (task) => {
    const nextDone = !task.done;
    if (task && task.localOnly) {
      setUserTasks((prev) => {
        const next = prev.map((row) => row.id === task.id ? { ...row, done: nextDone } : row);
        try { localStorage.setItem('yeye-user-tasks', JSON.stringify(next)); } catch (_) {}
        return next;
      });
      return;
    }
    setTasks((prev) => prev.map((row) => row.id === task.id ? { ...row, done: nextDone, tone: nextDone ? 'ok' : row.tone } : row));
    if (!task.ghlTaskId || !task.contactId) return;
    try {
      const email = (authState.user && authState.user.email) || '';
      const url = GHL_TASK_SYNC_BASE + '/contacts/' + encodeURIComponent(task.contactId) + '/tasks/' + encodeURIComponent(task.ghlTaskId) + '/completed';
      const res = await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ completed: nextDone, email }),
      });
      if (!res.ok) throw new Error('GHL task update failed (' + res.status + ')');
    } catch (err) {
      setTasks((prev) => prev.map((row) => row.id === task.id ? { ...row, done: task.done } : row));
      showToast(err && err.message ? err.message : String(err || 'GHL task update failed'));
    }
  };
  const openServiceTicket = (service) => {
    openModal('ticket', { catK: 'it', subject: t('ticket.serviceSubject') + ': ' + serviceTitle(service, t) });
  };
  const refreshContact = React.useCallback(async () => {
    if (!window.YEYE_AUTH.refreshContactFromGhl) return null;
    setContactSync((prev) => ({ ...prev, syncing: true, error: '' }));
    try {
      const contact = await window.YEYE_AUTH.refreshContactFromGhl();
      setOptimisticEdits((previous) => reconcileOptimisticEdits(previous, contact));
      setContactSync({ syncing: false, lastSync: new Date().toISOString(), error: '' });
      return contact;
    } catch (err) {
      setContactSync({ syncing: false, lastSync: new Date().toISOString(), error: err && err.message ? err.message : String(err || '') });
      return null;
    }
  }, []);

  useEffect(() => {
    if (!authed || checking || !authState.user?.email) return;
    const contact = authState.contact;
    const customFields = contact?.customFields;
    const hasLiveFields = Array.isArray(customFields) && customFields.length > 0;
    const isDemoContact = String(contact?.id || '').startsWith('testblue-demo');
    if (!contact || !hasLiveFields || isDemoContact) {
      refreshContact();
    }
  }, [authed, checking, authState.user?.email, authState.contact?.id]);

  // when client type changes, jump to dashboard; employer-sponsored branding only valid for sponsored types
  useEffect(() => { setPage('dashboard'); if (!liveCtData.org && branding === 'corporate') setBranding('individual'); }, [clientType]);

  // when contact loads, sync clientType to derived
  useEffect(() => {
    if (optimisticContact) {
      const k = deriveCaseType(optimisticContact.tags);
      if (k !== clientType) setClientType(k);
    }
  }, [optimisticContact]);

  const hasDiyServices = (purchasedServices || []).some((purchase) => (
    typeof window.YEYE_IS_DIY_PURCHASE === 'function' && window.YEYE_IS_DIY_PURCHASE(purchase)
  ));

  const renderPage = () => {
    if (page === 'dashboard') return <ExpatDashboard ct={liveCtData} go={go} openModal={openModal} calendarEvents={calendarEvents} tasks={tasks} purchasedServices={purchasedServices} blankProfile={blankProfile} calendarSync={calendarSync} onConnectGoogleCalendar={connectGoogleCalendar} onSyncGoogleCalendar={syncGoogleCalendar} onToggleTask={toggleGhlTask} />;
    switch (page) {
      case 'services': return <ServicesPage openModal={openModal} purchasedServices={purchasedServices} blankProfile={blankProfile} />;
      case 'formSubmission': return <FormSubmissionPage ct={liveCtData} contact={optimisticContact} fieldMappings={fieldMappings} onRefresh={refreshContact} onOptimisticUpdate={applyOptimisticUpdate} syncState={contactSync} purchasedServices={purchasedServices} openModal={openModal} onCompleteProfile={() => go('formSubmission')} />;
      case 'purchasedServices': return <PurchasedServicesPage openModal={openModal} purchasedServices={purchasedServices} go={go} blankProfile={blankProfile} />;
      case 'documents': return <DocumentsPage openModal={openModal} blankProfile={blankProfile} contact={optimisticContact} onRefresh={refreshContact} purchasedServices={purchasedServices} fieldMappings={fieldMappings} onCompleteProfile={() => go('formSubmission')} />;
      case 'docgen': return <DocGeneratorPage purchasedServices={purchasedServices} contact={optimisticContact} fieldMappings={fieldMappings} onRefresh={refreshContact} onOptimisticUpdate={applyOptimisticUpdate} />;
      case 'invoices': return <InvoicesPage contact={optimisticContact} go={go} openModal={openModal} onCheckout={() => setCheckoutOpen(true)} />;
      case 'support': return <SupportPage openModal={openModal} tickets={tickets} tasks={tasks} go={go} />;
      case 'appointments': return <AppointmentsPage events={calendarEvents} openModal={openModal} />;
      case 'tasks': return <TasksPage tasks={tasks.concat(userTasks)} openModal={openModal} onSync={syncGhlTasks} syncState={taskSync} onToggleTask={toggleGhlTask} contact={optimisticContact} isAdmin={isAdminUser} onAddSelfTask={addUserTask} />;
      case 'reports': return <ReportsPage />;
      case 'settings': return <SettingsPage />;
      default: return <SimplePage icon="circle-dashed" title={t('pg.soon')} sub={t('pg.soonSub')} />;
    }
  };

  if (!authed) {
    return (
        <LangCtx.Provider value={{ lang, t, dict, currency, setLang: setLangP, setCurrency: setCurrencyP }}>
        <LoginScreen error={authState.error} checking={checking} />
      </LangCtx.Provider>
    );
  }

  if (authState.mustChangePassword) {
    return (
        <LangCtx.Provider value={{ lang, t, dict, currency, setLang: setLangP, setCurrency: setCurrencyP }}>
        <ForceChangePasswordScreen email={authState.user && authState.user.email} />
      </LangCtx.Provider>
    );
  }

  // While logged-in but contact still loading, show a full-screen loading splash so we don't flash the dashboard.
  // The timeout prevents a failed GHL lookup from trapping the user on this screen forever.
  if (authed && !authState.contact && !contactLoadingTimedOut) {
    return (
        <LangCtx.Provider value={{ lang, t, dict, currency, setLang: setLangP, setCurrency: setCurrencyP }}>
        <WelcomeSplash />
      </LangCtx.Provider>
    );
  }

  if (window.shouldShowWelcome && window.shouldShowWelcome(optimisticContact)) {
    return (
        <LangCtx.Provider value={{ lang, t, dict, currency, setLang: setLangP, setCurrency: setCurrencyP }}>
        <WelcomePage contact={optimisticContact} onDismiss={(target) => {
          setPage(target === 'services' ? 'services' : 'dashboard');
          refreshWelcome((value) => value + 1);
        }} />
      </LangCtx.Provider>
    );
  }

  return (
    <LangCtx.Provider value={{ lang, t, dict, currency, setLang: setLangP, setCurrency: setCurrencyP }}>
    <div className="app">
      <div className={'scrim ' + (drawer ? 'show' : '')} onClick={() => setDrawer(false)} />
      <Sidebar nav={window.YEYE.nav.map((it) => {
        if (it.key === 'tasks') {
          const TICKET_TITLE_RE = /^\[[^\]]+\]\s*/;
          const openCount = tasks.filter((t) => !t.done && !TICKET_TITLE_RE.test(String(t.title || ''))).length;
          return openCount > 0 ? { ...it, badge: openCount } : it;
        }
        if (it.key === 'formSubmission' && window.YEYE_PROFILE && (purchasedServices || []).length > 0) {
          const missing = window.YEYE_PROFILE.missing(optimisticContact || {});
          return missing.length > 0 ? { ...it, badge: missing.length, amber: true } : it;
        }
        if (it.key === 'invoices') {
          return unpaidInvoiceCount > 0 ? { ...it, badge: unpaidInvoiceCount, amber: true } : it;
        }
        return it;
      })} active={page} onNav={go} mini={mini} open={drawer} onClose={() => setDrawer(false)} ct={liveCtData} hasDiyServices={hasDiyServices} />
      <div className="main">
        <Topbar
          title={page === 'dashboard' ? t('nav.dashboard') : t('nav.' + page)} sub={page === 'dashboard' && !blankProfile ? t('ct.' + effectiveKey) : null}
          ct={liveCtData} branding={branding} onBranding={setBranding}
          ctKey={clientType} onCt={setClientType} lang={lang} onLang={setLangP} currency={currency} onCurrency={setCurrencyP}
          onToggleSidebar={() => setMini((m) => !m)} onHamburger={() => setDrawer(true)}
          onNotif={() => setNotif(true)} onSearch={() => {}} onNav={go}
          notifDot={hasUnreadPortalNotifications(tasks, optimisticContact)}
          cartCount={cart.items.length} onCart={() => setCartOpen(true)} />
        <div className="content">
          <div className="page">
            <CompleteProfileBanner contact={optimisticContact} lang={lang} onGoto={() => go('formSubmission')} currentPage={page} blankProfile={blankProfile} purchasedServices={purchasedServices} />
            {renderPage()}
          </div>
        </div>
      </div>

      {modal === 'service' && <ServiceModal service={modalData} contact={optimisticContact} liveContact={liveCtData} onClose={() => setModal(null)} onConfirm={addPurchasedService} onRequestTicket={(preset) => openModal('ticket', preset)} />}
      {modal === 'upload' && <UploadModal contactId={(modalData && modalData.contactId) || (optimisticContact && optimisticContact.id)} initialFolderK={modalData && modalData.folderK} initialSubName={modalData && modalData.subName} onUploaded={modalData && modalData.onUploaded} onClose={() => setModal(null)} />}
      {modal === 'taskAttachment' && <TaskAttachmentModal task={modalData && modalData.task} contact={modalData && modalData.contact} onClose={() => setModal(null)} />}
      {modal === 'ticket' && <TicketModal preset={modalData} onClose={() => setModal(null)} onSubmit={addTicket} />}
      {modal === 'calendarEvent' && <CalendarEventModal onClose={() => setModal(null)} onSubmit={addCalendarEvent} />}
      {modal === 'task' && <TaskModal preset={modalData} onClose={() => setModal(null)} onSubmit={(task) => { addUserTask(task); setModal(null); showToast(t('task.addedToast') !== 'task.addedToast' ? t('task.addedToast') : t('mod.addTask')); }} />}
      {modal === 'manageService' && <ServiceManageModal item={modalData} onClose={() => setModal(null)} onStatus={updatePurchasedStatus} openTicket={openServiceTicket} showToast={showToast} />}
      <CartDrawer
        open={cartOpen}
        onClose={() => setCartOpen(false)}
        onContinueShopping={() => go('services')}
        onCheckout={() => { setCartOpen(false); setCheckoutOpen(true); }}
      />
      <CheckoutModal
        open={checkoutOpen}
        onClose={() => setCheckoutOpen(false)}
        onSuccess={(result) => markPaidItemsPurchased(result && result.paidItems)}
        onNeedProfile={() => go('formSubmission')}
      />
      {notif && <NotifPanel onClose={() => setNotif(false)} go={go} tasks={tasks} contact={optimisticContact} />}
      <Toast text={toast} />
      <MobileDocGeneratorBar active={page} onNav={go} hasDiyServices={hasDiyServices} />
    </div>
    </LangCtx.Provider>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
