/* ============================================================
   SVLM — App shell (preview build): state, routing, render.
   Ported from the design's app.jsx, rewired to real data instead of
   mock SEED_* arrays.
   Slice 1: AuthScreen (real login), HomeScreen (real services),
            DetailScreen (real service detail).
   Slice 2 (this pass): TabBar (Home/Services/Products/Account),
            ServicesTab (real services grouped by group_id),
            ProductsTab + ProductDetail (real /api/products —
            the design project itself has no product screens, since
            it only covers services/handyman, so these are built
            fresh in the same visual language rather than ported).
            Minimal AccountScreen so the 4th tab isn't a dead end.
   Booking/Checkout/Payment/Confirm/Tracking/Bookings/Chat/Complaint/
   Invoice/Handyman/Review are still the next slice.
   ============================================================ */
const { useState, useEffect, useMemo } = React;
const Pr = window.PALETTE;
const Sr = window.SHAPE;
const SHr = window.SHADOW;

function App() {
  const [lang, setLangState] = useState(getLang('customer'));
  const [stack, setStack] = useState([{ name: 'tab-home', params: {} }]);
  const [auth, setAuth] = useState(null);
  // Read once here, above the loading/error returns, so the hook order never changes between renders.
  const wide = useIsDesktop();
  const [allServices, setAllServices] = useState([]);
  const [cities, setCities] = useState([]);
  const [city, setCity] = useState(null);
  const [allProducts, setProducts] = useState([]);
  const [productCategories, setProductCategories] = useState([]);
  const [banners, setBanners] = useState([]);
  const [promotions, setPromotions] = useState([]);
  const [blogPosts, setBlogPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [loadError, setLoadError] = useState(null);
  const [oauthError, setOauthError] = useState(null);
  // Cart is client-side only until checkout, same as public/index.html's `cart` — shares the
  // 'svlm_cart' localStorage key so nothing is lost switching between the two builds.
  const [cart, setCart] = useState(() => JSON.parse(localStorage.getItem('svlm_cart') || '[]'));
  // OAuth-callback / already-signed-in visits skip the language+onboarding greeting entirely,
  // same as public/index.html's `skipPreLoginScreens` — computed once, before the hash gets
  // stripped below, so GreetingGate sees the right value on first render.
  const [skipGreeting] = useState(() => window.location.hash.startsWith('#token=') || !!new URLSearchParams(window.location.search).get('oauth_error') || !!localStorage.getItem('svlm_token'));

  const cur = stack[stack.length - 1];

  useEffect(() => {
    // OAuth callback (routes/oauth.js) redirects here with #token=... on success, or
    // ?oauth_error=... on failure — same contract public/index.html's init() already handles.
    const params = new URLSearchParams(window.location.search);
    if (params.get('oauth_error')) setOauthError(params.get('oauth_error'));
    const hashToken = window.location.hash.startsWith('#token=') ? decodeURIComponent(window.location.hash.slice(7)) : null;
    // ?service=<id> — the link ShareService()/DetailScreen's Share button hands out (see ui.jsx) —
    // opens straight to that service's detail screen once the catalog has loaded (below), rather
    // than dumping a shared link on the generic Home screen.
    const sharedServiceId = params.get('service');
    window.history.replaceState({}, '', window.location.pathname);

    async function boot() {
      if (hashToken) {
        localStorage.setItem('svlm_token', hashToken);
        try {
          const customer = await api('/customers/me');
          localStorage.setItem('svlm_customer', JSON.stringify(customer));
          setAuth({ method: 'oauth', name: customer.name, phone: customer.phone, customer });
        } catch (e) {
          localStorage.removeItem('svlm_token');
          setOauthError('Could not complete sign-in. Please try again.');
        }
      } else {
        const existingToken = localStorage.getItem('svlm_token');
        const existingCustomer = localStorage.getItem('svlm_customer');
        if (existingToken && existingCustomer) {
          const customer = JSON.parse(existingCustomer);
          setAuth({ method: 'phone', name: customer.name, phone: customer.phone, customer });
        }
      }
    }
    boot();

    loadRealData()
      .then(({ services, cities, products, productCategories, banners, promotions, blogPosts }) => {
        setAllServices(services); setCities(cities); setCity(cities[0] || null);
        setProducts(products); setProductCategories(productCategories);
        setBanners(banners); setPromotions(promotions); setBlogPosts(blogPosts);
        setLoading(false);
        if (sharedServiceId && services.some(s => s.id === sharedServiceId)) {
          setStack([{ name: 'tab-home', params: {} }, { name: 'detail', params: { id: sharedServiceId } }]);
        }
      })
      .catch(e => { setLoadError(e.message); setLoading(false); });
  }, []);

  // Ops can restrict a service to only certain cities (see PUT /admin/services/:id/cities) —
  // narrow to the selected city here, once, rather than at every screen that lists services.
  const services = useMemo(
    () => allServices.filter(s => !city || s.availableCityIds.includes(city.id)),
    [allServices, city]
  );
  // Same per-city restriction, for products (see PUT /admin/products/:id/cities).
  const products = useMemo(
    () => allProducts.filter(p => !city || p.availableCityIds.includes(city.id)),
    [allProducts, city]
  );

  const TAB_ROOTS = ['tab-home', 'tab-services', 'tab-products', 'tab-account'];
  const saveCart = (next) => { setCart(next); localStorage.setItem('svlm_cart', JSON.stringify(next)); };
  const app = {
    lang, city, auth, services, cities, products, productCategories, banners, promotions, blogPosts, cart,
    setCity,
    addToCart: (item) => saveCart([...cart, item]),
    // addToCart called twice in the same handler would lose the first item — both calls close
    // over the same pre-update `cart` from this render, so the second call's [...cart, item]
    // overwrites rather than stacks. Needed for the Move-in/Move-out + A/C Cleaning bundle
    // (BookingScreen), which adds two cart items — one per resulting booking — in one submit.
    addItemsToCart: (items) => saveCart([...cart, ...items]),
    removeFromCart: (i) => saveCart(cart.filter((_, idx) => idx !== i)),
    adjustCartQty: (i, delta) => saveCart(cart.map((it, idx) => idx === i ? { ...it, quantity: Math.max(1, Number(it.quantity || 1) + delta) } : it)),
    clearCart: () => saveCart([]),
    setLang: (l) => { setLangState(l); setLanguage('customer', l); },
    toggleLang: () => { const next = lang === 'th' ? 'en' : 'th'; setLangState(next); setLanguage('customer', next); },
    pick: (en, th) => (lang === 'th' ? th : en),
    t: (key) => appT(key),
    login: (info) => setAuth(info),
    logout: () => { localStorage.removeItem('svlm_token'); localStorage.removeItem('svlm_customer'); setAuth(null); setStack([{ name: 'tab-home', params: {} }]); },
    priceVat: (s) => vatInc(s.from),
    priceVatOriginal: (s) => (s.fromOriginal ? vatInc(s.fromOriginal) : null),
    go: (name, params = {}) => setStack(s => (TAB_ROOTS.includes(name) ? [{ name, params }] : [...s, { name, params }])),
    back: () => setStack(s => (s.length > 1 ? s.slice(0, -1) : s)),
  };
  window.__app = app;

  if (loading) {
    return <div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', color: Pr.muted, fontWeight: 700 }}>Loading…</div>;
  }
  if (loadError) {
    return <div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#d23a3a', fontWeight: 700, padding: 24, textAlign: 'center' }}>Failed to load: {loadError}</div>;
  }
  if (!auth) {
    return <GreetingGate skip={skipGreeting}><AuthScreen app={app} oauthError={oauthError} /></GreetingGate>;
  }

  const isTab = TAB_ROOTS.includes(cur.name);
  let body;
  switch (cur.name) {
    case 'tab-home': body = <HomeScreen app={app} />; break;
    case 'tab-services': body = <ServicesTab app={app} params={cur.params} />; break;
    case 'tab-products': body = <ProductsTab app={app} />; break;
    case 'tab-account': body = <AccountScreenFull app={app} />; break;
    case 'detail': body = <DetailScreenLoader app={app} params={cur.params} />; break;
    case 'product-detail': body = <ProductDetail app={app} params={cur.params} />; break;
    case 'blog-detail': body = <BlogDetail app={app} params={cur.params} />; break;
    case 'booking': body = <BookingScreen app={app} params={cur.params} />; break;
    case 'checkout': body = <CheckoutScreen app={app} />; break;
    case 'confirm': body = <ConfirmScreen app={app} />; break;
    case 'payment-qr': body = <PaymentQrScreen app={app} params={cur.params} />; break;
    case 'bookings': body = <MyBookingsScreen app={app} />; break;
    case 'chat': body = <ChatThreadsScreen app={app} />; break;
    case 'chat-thread': body = <ChatThreadScreen app={app} params={cur.params} />; break;
    case 'complaint': body = <ComplaintScreen app={app} />; break;
    case 'handyman-hub': body = <HandymanHub app={app} />; break;
    case 'review': body = <ReviewScreen app={app} params={cur.params} />; break;
    case 'menu-category-grid': body = <MenuCategoryGrid app={app} params={cur.params} />; break;
    case 'subservice-list': body = <SubserviceList app={app} params={cur.params} />; break;
    case 'notifications': body = <NotificationCenterScreen app={app} />; break;
    default: body = <HomeScreen app={app} />;
  }
  const activeTab = cur.name.replace('tab-', '');
  return (
    <div style={{ minHeight: '100vh', background: Pr.bg }}>
      <div style={{
        maxWidth: wide ? 1180 : 480, margin: '0 auto', display: wide ? 'flex' : 'block',
        gap: wide ? 28 : 0, alignItems: 'flex-start', padding: wide ? '0 22px' : 0,
      }}>
        {wide && isTab && <SideNav app={app} active={activeTab} />}
        {/* The bottom bar floats over the content, so on phones the last section needs room to
            clear it. On desktop the sidebar takes that job and the padding would just be a gap. */}
        <div style={{ flex: 1, minWidth: 0, paddingBottom: (!wide && isTab) ? 96 : 0 }}>{body}</div>
      </div>
      {!wide && isTab && <TabBar app={app} active={activeTab} />}
    </div>
  );
}

/* Icon-button chrome. On the navy band a solid white button punches a hole in the colour, so the
   dark variant uses a translucent white fill and hairline instead. */
function iconBtnStyle(dark) {
  return {
    width: 40, height: 40, borderRadius: Sr.tile, flexShrink: 0, cursor: 'pointer',
    display: 'flex', alignItems: 'center', justifyContent: 'center',
    border: '1px solid ' + (dark ? 'rgba(255,255,255,0.20)' : Pr.line),
    background: dark ? 'rgba(255,255,255,0.10)' : '#fff',
  };
}

/* ---------- Bottom tab bar (phones and tablets).
   The reference design floats a dark bar and lifts the active tab out of it on a disc. That lift
   is what makes the current tab readable at a glance against a dark bar, so it is kept — ours is
   navy rather than near-black, and the disc is SVLM orange. The disc is positioned absolutely
   rather than by margin so the labels stay on one baseline whichever tab is active. ---------- */
function TabBar({ app, active }) {
  const tabs = [
    ['home', 'home', app.t('tab_home')],
    ['services', 'grid', app.t('tab_services')],
    ['products', 'box', app.t('tab_products')],
    ['account', 'user', app.t('tab_account')],
  ];
  return (
    <div style={{ position: 'fixed', left: '50%', transform: 'translateX(-50%)',
      bottom: 'calc(14px + env(safe-area-inset-bottom))', zIndex: 40,
      width: 'calc(100% - 32px)', maxWidth: 448,
      background: Pr.navy, borderRadius: Sr.nav, boxShadow: SHr.nav }}>
      <div style={{ display: 'flex', padding: '6px 6px 4px' }}>
        {tabs.map(([k, ic, lbl]) => {
          const on = active === k;
          return (
            <button key={k} onClick={() => app.go('tab-' + k)} aria-current={on ? 'page' : undefined}
              style={{ flex: 1, height: 54, background: 'none', border: 'none', cursor: 'pointer',
                position: 'relative', fontFamily: 'inherit', padding: 0, WebkitTapHighlightColor: 'transparent' }}>
              <span style={{
                position: 'absolute', left: '50%', transform: 'translateX(-50%)',
                top: on ? -20 : 7, width: on ? 46 : 34, height: on ? 46 : 34, borderRadius: '50%',
                display: 'flex', alignItems: 'center', justifyContent: 'center',
                background: on ? Pr.primary : 'transparent',
                border: on ? '4px solid ' + Pr.navy : 'none',
                boxShadow: on ? SHr.brand : 'none',
                transition: 'top .18s ease, width .18s ease, height .18s ease, background .18s ease',
              }}>
                <Icon name={ic} size={on ? 21 : 20} color={on ? '#fff' : 'rgba(255,255,255,0.60)'} sw={on ? 2.1 : 1.8} />
              </span>
              <span style={{ position: 'absolute', left: 0, right: 0, bottom: 5, fontSize: 10.5,
                fontWeight: on ? 800 : 600, color: on ? '#fff' : 'rgba(255,255,255,0.60)' }}>{lbl}</span>
            </button>
          );
        })}
      </div>
    </div>
  );
}

/* ---------- Desktop navigation.
   The reference kit is a 375px phone design with no desktop layout to copy, so this is ours: on a
   wide screen a phone-shaped column strands the content in the middle third of the display, and a
   floating bottom bar is the wrong instinct for a pointer. Same four destinations, same category
   shortcuts as the home screen, laid down the side where they can stay visible. ---------- */
function SideNav({ app, active }) {
  const L = app.lang;
  const tabs = [
    ['home', 'home', app.t('tab_home')],
    ['services', 'grid', app.t('tab_services')],
    ['products', 'box', app.t('tab_products')],
    ['account', 'user', app.t('tab_account')],
  ];
  return (
    <aside style={{ width: 240, flexShrink: 0, position: 'sticky', top: 0, padding: '24px 0 28px', alignSelf: 'flex-start' }}>
      <div style={{ padding: '0 10px 20px' }}><Brand h={38} /></div>
      <nav style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
        {tabs.map(([k, ic, lbl]) => {
          const on = active === k;
          return (
            <button key={k} onClick={() => app.go('tab-' + k)} aria-current={on ? 'page' : undefined}
              style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 14px',
                borderRadius: Sr.tile, border: 'none', cursor: 'pointer', fontFamily: 'inherit',
                background: on ? Pr.primary : 'transparent', color: on ? '#fff' : Pr.muted,
                fontWeight: on ? 800 : 600, fontSize: 14.5, textAlign: 'left',
                boxShadow: on ? SHr.brand : 'none' }}>
              <Icon name={ic} size={20} color={on ? '#fff' : Pr.muted} sw={on ? 2.1 : 1.8} />{lbl}
            </button>
          );
        })}
      </nav>
      <div style={{ margin: '24px 0 12px', padding: '0 14px', fontSize: 11, fontWeight: 800,
        color: Pr.faint, letterSpacing: 0.6, textTransform: 'uppercase' }}>{app.t('choose_service')}</div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 10, padding: '0 8px' }}>
        {menuCategories(app).map(c => (
          <RoundTile key={c.key} icon={c.icon} label={c.label} tone={c.tone} lang={L}
            onClick={() => app.go('menu-category-grid', { menuKey: c.key })} />
        ))}
      </div>
    </aside>
  );
}

/* ---------- City picker bottom sheet ---------- */
function Sheet({ children, onClose, title }) {
  return (
    <div onClick={onClose} style={{ position: 'fixed', inset: 0, zIndex: 80, background: 'rgba(11,37,69,0.42)', display: 'flex', alignItems: 'flex-end', animation: 'fade .2s ease' }}>
      <div onClick={e => e.stopPropagation()} style={{ width: '100%', maxWidth: 480, margin: '0 auto', background: '#fff', borderRadius: '26px 26px 0 0', padding: '10px 20px 30px', maxHeight: '82%', overflow: 'auto', animation: 'slideUp .26s cubic-bezier(.2,.8,.2,1)' }}>
        <div style={{ width: 40, height: 5, borderRadius: 99, background: Pr.line, margin: '4px auto 14px' }} />
        {title && <h3 style={{ margin: '0 0 16px', fontSize: 19, fontWeight: 800, color: Pr.ink }}>{title}</h3>}
        {children}
      </div>
    </div>
  );
}

function CityPicker({ app, onClose }) {
  return (
    <Sheet onClose={onClose} title={app.t('service_in') || 'Service in'}>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
        {app.cities.map(c => {
          const on = app.city && c.id === app.city.id;
          return (
            <button key={c.id} onClick={() => { app.setCity(c); onClose(); }} style={{
              display: 'flex', alignItems: 'center', gap: 13, padding: '13px 14px', borderRadius: 16,
              border: on ? `1.5px solid ${Pr.primary}` : `1.5px solid ${Pr.line}`,
              background: on ? Pr.tintBlue : '#fff', cursor: 'pointer', textAlign: 'left', fontFamily: 'inherit', width: '100%',
            }}>
              <div style={{ width: 42, height: 42, borderRadius: 13, background: on ? Pr.primary : Pr.tintBlue, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
                <Icon name="pin" size={22} color={on ? '#fff' : Pr.primary} />
              </div>
              <div style={{ flex: 1 }}>
                <div style={{ fontWeight: 700, color: Pr.ink, fontSize: 15.5 }}>{app.pick(c.en, c.th)}</div>
                <div style={{ fontSize: 12.5, color: Pr.faint, fontWeight: 500 }}>{c.hub} · {c.hours}</div>
              </div>
              {on && <Icon name="check" size={20} color={Pr.primary} sw={2.4} />}
            </button>
          );
        })}
      </div>
    </Sheet>
  );
}

/* ---------- Notifications bell + sheet — real GET/PATCH /customers/me/notifications ---------- */
// The cart had no entry point at all: adding an item jumped straight to checkout, and anyone who
// browsed on afterwards had no way back to it. The items were still sitting in localStorage,
// invisible, so a half-built order was silently abandoned. This is the way back — and the count is
// what tells you there's something waiting in the first place.
function CartButton({ app, dark }) {
  const count = app.cart.reduce((sum, item) => sum + Number(item.quantity || 1), 0);
  if (count === 0) return null;
  return (
    <button
      onClick={() => app.go('checkout')}
      aria-label={app.t('cart_title') || 'Cart'}
      style={{ ...iconBtnStyle(dark), position: 'relative' }}
    >
      <Icon name="cart" size={20} color={dark ? '#fff' : Pr.ink} />
      <span style={{ position: 'absolute', top: -6, right: -6, minWidth: 18, height: 18, padding: '0 5px', borderRadius: 99, background: Pr.primary, color: '#fff', fontSize: 11, fontWeight: 800, display: 'flex', alignItems: 'center', justifyContent: 'center', border: '2px solid ' + (dark ? Pr.navy : '#fff') }}>
        {count > 9 ? '9+' : count}
      </span>
    </button>
  );
}

function NotificationsBell({ app, dark }) {
  const [open, setOpen] = useState(false);
  const [items, setItems] = useState([]);
  useEffect(() => { api('/customers/me/notifications').then(setItems).catch(() => {}); }, []);
  const unread = items.filter(n => !n.is_read).length;
  const openSheet = async () => {
    setOpen(true);
    if (unread > 0) {
      await api('/customers/me/notifications/read', { method: 'PATCH' }).catch(() => {});
      setItems(its => its.map(n => ({ ...n, is_read: true })));
    }
  };
  return (
    <>
      <button onClick={openSheet} aria-label={app.t('notifications')} style={{ ...iconBtnStyle(dark), position: 'relative' }}>
        <Icon name="bell" size={20} color={dark ? '#fff' : Pr.ink} />
        {unread > 0 && <span style={{ position: 'absolute', top: 9, right: 10, width: 7, height: 7, borderRadius: 99, background: Pr.primary, border: '1.5px solid ' + (dark ? Pr.navy : '#fff') }} />}
      </button>
      {open && (
        <Sheet onClose={() => setOpen(false)} title={app.t('notifications') || 'Notifications'}>
          {items.length === 0
            ? <p style={{ fontSize: 13.5, color: Pr.faint, textAlign: 'center', padding: '20px 0' }}>{app.lang === 'th' ? 'ยังไม่มีการแจ้งเตือน' : 'No notifications yet'}</p>
            : items.slice(0, 8).map(n => (
              <div key={n.id} style={{ padding: '12px 0', borderBottom: `1px solid ${Pr.lineSoft}` }}>
                <div style={{ fontSize: 13.5, color: Pr.ink, fontWeight: 600 }}>{n.title || n.message}</div>
                <div style={{ fontSize: 11, color: Pr.faint, marginTop: 4 }}>{new Date(n.created_at).toLocaleString()}</div>
              </div>
            ))}
          <button onClick={() => { setOpen(false); app.go('notifications'); }} style={{ width: '100%', marginTop: 10, padding: '11px', borderRadius: 12, border: 'none', background: Pr.tintBlue, color: Pr.primary, fontWeight: 700, fontSize: 13, cursor: 'pointer', fontFamily: 'inherit' }}>
            {app.t('notif_see_all') || 'See all'}
          </button>
        </Sheet>
      )}
    </>
  );
}

/* ---------- Notification Center — full list, filters, search, per-item actions ----------
   Reached via NotificationsBell's "See all" or Profile → Notification settings' sibling entry.
   Server does the heavy filtering (category/date/read/archived/search — see routes/notifications.js);
   this screen just drives the query string and renders whatever comes back. ---------- */
function NotificationCenterScreen({ app }) {
  const [tab, setTab] = useState('all'); // all | unread | archived | favorites
  const [category, setCategory] = useState('');
  const [q, setQ] = useState('');
  const [categories, setCategories] = useState([]);
  const [items, setItems] = useState(null);

  useEffect(() => { api('/notification-categories').then(setCategories).catch(() => {}); }, []);

  const load = () => {
    const params = new URLSearchParams();
    if (category) params.set('category', category);
    if (q.trim()) params.set('q', q.trim());
    if (tab === 'unread') params.set('is_read', 'false');
    if (tab === 'archived') params.set('archived', 'true');
    api(`/customers/me/notifications?${params.toString()}`).then((rows) => {
      setItems(tab === 'favorites' ? rows.filter((n) => n.is_favorited) : rows);
    }).catch(() => setItems([]));
  };
  useEffect(load, [tab, category, q]);

  const patch = async (id, body) => { await api(`/customers/me/notifications/${id}`, { method: 'PATCH', body: JSON.stringify(body) }); load(); };
  const remove = async (id) => {
    if (!window.confirm(app.lang === 'th' ? 'ลบการแจ้งเตือนนี้?' : 'Delete this notification?')) return;
    await api(`/customers/me/notifications/${id}`, { method: 'DELETE' }); load();
  };
  const markAllRead = async () => { await api('/customers/me/notifications/read', { method: 'PATCH', body: JSON.stringify(category ? { category } : {}) }); load(); };

  return (
    <div>
      <AppHeader onBack={app.back} title={app.t('notifications') || 'Notifications'} />
      <div style={{ padding: '0 20px 10px' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, background: '#fff', borderRadius: 14, padding: '0 14px', height: 44, border: `1px solid ${Pr.line}`, marginBottom: 12 }}>
          <Icon name="search" size={17} color={Pr.faint} />
          <input value={q} onChange={(e) => setQ(e.target.value)} placeholder={app.t('notif_search_placeholder') || 'Search notifications...'}
            style={{ flex: 1, border: 'none', outline: 'none', fontFamily: 'inherit', fontSize: 13.5, color: Pr.ink, background: 'none' }} />
        </div>
        <div style={{ display: 'flex', gap: 8, overflowX: 'auto', paddingBottom: 4 }}>
          {['all', 'unread', 'archived', 'favorites'].map((k) => (
            <Chip key={k} on={tab === k} onClick={() => setTab(k)}>
              {k === 'all' ? (app.t('notif_all') || 'All')
                : k === 'unread' ? (app.t('notif_unread') || 'Unread')
                : k === 'archived' ? (app.t('notif_archived') || 'Archived')
                : (app.t('notif_favorites') || 'Favorites')}
            </Chip>
          ))}
        </div>
        {categories.length > 0 && (
          <div style={{ display: 'flex', gap: 8, overflowX: 'auto', padding: '10px 0 2px' }}>
            <Chip on={category === ''} onClick={() => setCategory('')}>{app.lang === 'th' ? 'ทุกหมวด' : 'All categories'}</Chip>
            {categories.map((c) => (
              <Chip key={c.id} on={category === c.id} onClick={() => setCategory(c.id)}>{app.pick(c.label_en, c.label_th)}</Chip>
            ))}
          </div>
        )}
      </div>

      <div style={{ padding: '4px 20px 40px' }}>
        {tab === 'unread' && items && items.length > 0 && (
          <button onClick={markAllRead} style={{ background: 'none', border: 'none', color: Pr.primary, fontWeight: 700, fontSize: 12.5, cursor: 'pointer', fontFamily: 'inherit', marginBottom: 10, padding: 0 }}>
            {app.t('notif_mark_all_read') || 'Mark all as read'}
          </button>
        )}
        {items === null ? null : items.length === 0 ? (
          <div style={{ textAlign: 'center', padding: '60px 0', color: Pr.faint }}>
            <Icon name="bell" size={38} color={Pr.line} />
            <div style={{ marginTop: 12, fontSize: 13.5, fontWeight: 600 }}>{app.t('notif_empty') || 'Nothing here yet.'}</div>
          </div>
        ) : items.map((n) => (
          <Card key={n.id} style={{ marginBottom: 10, opacity: n.is_read ? 0.8 : 1 }}>
            <div style={{ display: 'flex', gap: 10 }}>
              {!n.is_read && <div style={{ width: 7, height: 7, borderRadius: 99, background: Pr.primary, marginTop: 6, flexShrink: 0 }} />}
              <div style={{ flex: 1, minWidth: 0 }} onClick={() => !n.is_read && patch(n.id, { is_read: true })}>
                <div style={{ fontSize: 13.5, fontWeight: n.is_read ? 600 : 800, color: Pr.ink }}>{n.message}</div>
                <div style={{ fontSize: 11, color: Pr.faint, marginTop: 4 }}>{new Date(n.created_at).toLocaleString()}</div>
              </div>
            </div>
            <div style={{ display: 'flex', gap: 14, marginTop: 10, paddingTop: 10, borderTop: `1px solid ${Pr.lineSoft}` }}>
              <button onClick={() => patch(n.id, { is_favorited: !n.is_favorited })} style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 0, display: 'flex', alignItems: 'center', gap: 5 }}>
                <Icon name="star" size={15} color={n.is_favorited ? Pr.primary : Pr.faint} fill={n.is_favorited ? Pr.primary : 'none'} />
                <span style={{ fontSize: 11.5, color: n.is_favorited ? Pr.primary : Pr.faint, fontWeight: 700 }}>{n.is_favorited ? (app.t('notif_unfavorite') || 'Unfavorite') : (app.t('notif_favorite') || 'Favorite')}</span>
              </button>
              <button onClick={() => patch(n.id, { is_archived: !n.is_archived })} style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 0, display: 'flex', alignItems: 'center', gap: 5 }}>
                <Icon name="archive" size={15} color={Pr.faint} />
                <span style={{ fontSize: 11.5, color: Pr.faint, fontWeight: 700 }}>{n.is_archived ? (app.t('notif_unarchive') || 'Unarchive') : (app.t('notif_archive') || 'Archive')}</span>
              </button>
              <button onClick={() => remove(n.id)} style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 0, display: 'flex', alignItems: 'center', gap: 5, marginLeft: 'auto' }}>
                <Icon name="trash" size={15} color="#d23a3a" />
                <span style={{ fontSize: 11.5, color: '#d23a3a', fontWeight: 700 }}>{app.t('notif_delete') || 'Delete'}</span>
              </button>
            </div>
          </Card>
        ))}
      </div>
    </div>
  );
}

/* The five entry points into the catalogue. Shared by the home screen's category cards and the
   desktop sidebar so the two can never drift apart. */
function menuCategories(app) {
  return [
    { key: 'cleaning', icon: 'sparkle', label: app.t('cat_cleaning'), tone: 'orange' },
    { key: 'handyman', icon: 'wrench', label: app.t('cat_handyman'), tone: 'navy' },
    { key: 'movehouse', icon: 'truck', label: app.t('cat_movehouse'), tone: 'mint' },
    { key: 'corporate', icon: 'building', label: app.t('cat_corporate'), tone: 'rose' },
    { key: 'products', icon: 'box', label: app.t('tab_products'), tone: 'orange' },
  ];
}

/* t() returns the key itself when a string is missing, so `t(k) || fallback` can never fire —
   comparing against the key is the only way to detect a gap. See i18n.js. */
function labelOr(app, key, fallback) {
  const v = app.t(key);
  return v === key ? fallback : v;
}

function HomeScreen({ app }) {
  const [q, setQ] = useState('');
  const [cityOpen, setCityOpen] = useState(false);
  const [cat, setCat] = useState('all');
  const wide = useIsDesktop();
  const L = app.lang;

  // The chip row filters the catalogue by group. Groups are read off the catalogue itself rather
  // than hard-coded, so a group added in Ops appears here without a code change.
  const groups = useMemo(() => {
    const out = [];
    app.services.forEach((s) => { if (s.group && out.indexOf(s.group) === -1) out.push(s.group); });
    return out;
  }, [app.services]);

  const list = app.services.filter((s) => (cat === 'all' || s.group === cat)
    && (!q || app.pick(s.en, s.th).toLowerCase().includes(q.toLowerCase())));
  const popular = [...app.services].filter(s => s.isPopular).sort((a, b) => a.popularSort - b.popularSort).slice(0, 8);
  const firstName = app.auth && app.auth.name ? String(app.auth.name).trim().split(' ')[0] : '';

  // Sits on the navy band, above the sheet. On desktop the sidebar already carries the brand and a
  // Services link, so the band keeps only the controls that have nowhere else to live.
  const header = (
    <div style={{ padding: '12px 20px 20px', display: 'flex', alignItems: 'center', gap: 9 }}>
      {!wide && (
        <button onClick={() => app.go('tab-services')} aria-label={app.t('tab_services')} style={iconBtnStyle(true)}>
          <Icon name="grid" size={20} color="#fff" />
        </button>
      )}
      <div style={{ flex: 1, display: 'flex', justifyContent: 'center', minWidth: 0 }}>{!wide && <Brand h={30} />}</div>
      <button onClick={app.toggleLang} style={{ ...iconBtnStyle(true), width: 'auto', padding: '0 11px', gap: 6, fontFamily: 'inherit', fontWeight: 800, fontSize: 12.5, color: '#fff' }}>
        <Icon name="globe" size={16} color="#fff" />{L === 'th' ? 'ไทย' : L.toUpperCase()}
      </button>
      <CartButton app={app} dark />
      <NotificationsBell app={app} dark />
    </div>
  );

  const sheet = (
    <div>
      <div style={{ padding: '24px 20px 0' }}>
        <h1 style={{ margin: 0, ...txt('h1', L), color: Pr.ink }}>
          {app.t('home_welcome')}{firstName ? ' ' + firstName : ''}
        </h1>
        <p style={{ margin: '2px 0 0', ...txt('sub', L), color: Pr.muted }}>{app.t('home_welcome_sub')}</p>
      </div>

      <div style={{ padding: '16px 20px 0', display: 'flex', alignItems: 'center', gap: 10 }}>
        <button onClick={() => setCityOpen(true)} style={{ display: 'flex', alignItems: 'center', gap: 9, background: 'none', border: 'none', padding: 0, cursor: 'pointer', textAlign: 'left', minWidth: 0 }}>
          <span style={{ width: 40, height: 40, borderRadius: Sr.tile, background: Pr.primarySoft, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
            <Icon name="pin" size={20} color={Pr.primary} />
          </span>
          <span style={{ minWidth: 0 }}>
            <span style={{ display: 'block', fontSize: 11.5, color: Pr.faint, fontWeight: 600 }}>{app.t('service_in')}</span>
            <span style={{ display: 'flex', alignItems: 'center', gap: 3, ...txt('h5', L), color: Pr.ink }}>
              {app.city ? app.pick(app.city.en, app.city.th) : ''}<Icon name="chevD" size={16} color={Pr.muted} />
            </span>
          </span>
        </button>
      </div>

      <div style={{ padding: '14px 20px 0' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, background: Pr.bg, borderRadius: Sr.card, padding: '0 14px', height: 50, border: `1px solid ${Pr.line}` }}>
          <Icon name="search" size={19} color={Pr.faint} />
          <input value={q} onChange={e => setQ(e.target.value)} placeholder={app.t('search_services_placeholder')}
            style={{ flex: 1, border: 'none', outline: 'none', fontFamily: 'inherit', fontSize: 14.5, color: Pr.ink, background: 'none', minWidth: 0 }} />
        </div>
      </div>

      {/* Category cards — the reference design's tinted panel with the icon in a paler disc.
          Still routes into the same menu_categories grid as before. */}
      <div style={{ padding: '20px 20px 0' }}>
        {/* Five cards in a three-column grid would leave a hole in the second row, so they wrap
            and grow instead: the last row's two cards widen to fill the width and the block reads as
            deliberate. On desktop all five sit in one row. */}
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 10 }}>
          {menuCategories(app).map(c => (
            <span key={c.key} style={{ flex: '1 1 ' + (wide ? 'calc(20% - 8px)' : 'calc(33.333% - 7px)'), display: 'flex' }}>
              <CatCard icon={c.icon} label={c.label} tone={c.tone} lang={L}
                onClick={() => app.go('menu-category-grid', { menuKey: c.key })} />
            </span>
          ))}
        </div>
      </div>

      {/* Hero — real, Ops-uploaded banners from GET /api/banners?placement=home (routes/banners.js).
          Ops designs the graphic itself (text baked in), so this is a clickable image carousel
          rather than a code-rendered hero card. */}
      {app.banners.length > 0 && (
        <div style={{ padding: '22px 0 0' }}>
          <div style={{ padding: '0 20px' }}><SectionHead title={app.t('offers_news')} /></div>
          <div style={{ display: 'flex', gap: 12, overflowX: 'auto', padding: '0 20px 4px', scrollbarWidth: 'none' }}>
            {app.banners.map(b => (
              <a key={b.id} href={b.link_url || undefined} target={b.link_url ? '_blank' : undefined} rel="noopener noreferrer"
                style={{ flexShrink: 0, width: wide ? 320 : '85%', maxWidth: 340, display: 'block' }}>
                <img src={b.image_url} alt="" style={{ width: '100%', height: 140, borderRadius: Sr.card, objectFit: 'cover', display: 'block' }} />
              </a>
            ))}
          </div>
        </div>
      )}

      {/* Promotions — real, Ops-published coupons from GET /api/promotions/active. */}
      {app.promotions.length > 0 && (
        <div style={{ padding: '18px 0 0' }}>
          <div style={{ padding: '0 20px' }}><SectionHead title={app.t('promotions')} /></div>
          <div style={{ display: 'flex', gap: 12, overflowX: 'auto', padding: '0 20px 4px', scrollbarWidth: 'none' }}>
            {app.promotions.map(p => (
              <div key={p.id} style={{ flexShrink: 0, width: 232, borderRadius: Sr.card, padding: 16, background: 'linear-gradient(135deg, ' + Pr.navy2 + ', ' + Pr.navy + ')', color: '#fff' }}>
                <div style={{ fontSize: 24, fontWeight: 800, letterSpacing: -0.6 }}>{p.kind === 'percent' ? p.value + '%' : baht(p.value)} OFF</div>
                <div style={{ fontSize: 12.5, color: 'rgba(255,255,255,0.85)', margin: '4px 0 10px', lineHeight: L === 'th' ? 1.75 : 1.4 }}>{app.pick(p.en, p.th)}</div>
                <div style={{ display: 'inline-block', background: Pr.primary, borderRadius: Sr.pill, padding: '4px 11px', fontSize: 11, fontWeight: 800, letterSpacing: 0.5 }}>{p.id}</div>
              </div>
            ))}
          </div>
        </div>
      )}

      {/* Popular — real services with is_popular set (Ops toggles this per-service), sorted by
          popular_sort_order. Kept as photo cards rather than the reference's icon tiles, because
          Ops uploads a real image per service and discarding it would be a downgrade. */}
      {popular.length > 0 && (
        <div style={{ padding: '18px 0 0' }}>
          <div style={{ padding: '0 20px' }}><SectionHead title={app.t('popular_services_title')} /></div>
          <div style={{ display: 'flex', gap: 14, overflowX: 'auto', padding: '2px 20px 6px', scrollbarWidth: 'none' }}>
            {popular.map(s => (
              <div key={s.id} onClick={() => app.go('detail', { id: s.id })} style={{ width: 188, flexShrink: 0, cursor: 'pointer' }}>
                <Card pad={0} style={{ overflow: 'hidden' }}>
                  <div style={{ position: 'relative' }}>
                    <Photo src={s.image_url} label={s.en.toUpperCase()} h={108} r={0} tone={s.group === 'cleaning' ? 'blue' : 'mint'} />
                    <div style={{ position: 'absolute', top: 10, left: 10 }}><SvcTile name={s.icon} size={38} on /></div>
                  </div>
                  <div style={{ padding: '11px 13px 13px' }}>
                    <div style={{ ...txt('h5', L), color: Pr.ink, marginBottom: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{app.pick(s.en, s.th)}</div>
                    <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
                      <span style={{ fontSize: 13, color: Pr.muted }}><ServicePrice app={app} s={s} /></span>
                      <Stars value={s.rating} />
                    </div>
                  </div>
                </Card>
              </div>
            ))}
          </div>
        </div>
      )}

      {/* Blog / recommend — real posts from GET /api/blog (routes/blog.js), managed by Ops. */}
      {app.blogPosts.length > 0 && (
        <div style={{ padding: '18px 0 0' }}>
          <div style={{ padding: '0 20px' }}><SectionHead title={app.t('blog_title')} /></div>
          <div style={{ display: 'flex', gap: 12, overflowX: 'auto', padding: '0 20px 4px', scrollbarWidth: 'none' }}>
            {app.blogPosts.map(post => (
              <div key={post.id} onClick={() => app.go('blog-detail', { id: post.id })} style={{ flexShrink: 0, width: 180, cursor: 'pointer' }}>
                <Card pad={0} style={{ overflow: 'hidden' }}>
                  <Photo src={post.image_url} label="" h={100} r={0} />
                  <div style={{ padding: '10px 12px 12px', ...txt('small', L), color: Pr.ink, minHeight: 48 }}>{app.pick(post.titleEn, post.titleTh)}</div>
                </Card>
              </div>
            ))}
          </div>
        </div>
      )}

      {/* Full catalogue, with the reference design's chip row wired to a real filter over
          `group` rather than a decorative one. */}
      <div style={{ padding: '20px 20px 0' }}>
        <SectionHead title={app.t('all_services_title')} />
        {groups.length > 1 && (
          <div style={{ display: 'flex', gap: 8, overflowX: 'auto', paddingBottom: 14, scrollbarWidth: 'none' }}>
            <Chip on={cat === 'all'} onClick={() => setCat('all')}>{app.t('filter_all')}</Chip>
            {groups.map(g => (
              <Chip key={g} on={cat === g} onClick={() => setCat(g)}>{labelOr(app, 'cat_' + g, g)}</Chip>
            ))}
          </div>
        )}
        {list.length === 0
          ? <p style={{ ...txt('mini', L), color: Pr.faint, textAlign: 'center', padding: '26px 0' }}>{app.t('no_results')}</p>
          : (
            <div style={{ display: 'grid', gridTemplateColumns: wide ? 'repeat(3, 1fr)' : '1fr 1fr', gap: 12 }}>
              {list.map(s => <ServiceCard key={s.id} app={app} s={s} />)}
            </div>
          )}
      </div>

      {/* Trust strip — vetted/warranty are generic, always-true claims; city count is real. */}
      <div style={{ padding: '20px 20px 0' }}>
        <Card style={{ display: 'flex', gap: 4 }}>
          {[['badge', app.t('trust_vetted')], ['shield', app.t('trust_warranty')], ['pin', app.cities.length + ' ' + app.t('cities_word')]].map(([ic, lb], i) => (
            <React.Fragment key={ic}>
              {i > 0 && <div style={{ width: 1, background: Pr.line, margin: '4px 0' }} />}
              <div style={{ flex: 1, textAlign: 'center' }}>
                <Icon name={ic} size={22} color={Pr.primary} />
                <div style={{ ...txt('small', L), color: Pr.ink, marginTop: 5 }}>{lb}</div>
              </div>
            </React.Fragment>
          ))}
        </Card>
      </div>

      {/* Official SVLM social links — plain outbound links, not the per-service Share button
          (DetailScreen) which shares one specific service via the OS share sheet instead. */}
      <div style={{ padding: '18px 20px 28px', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10 }}>
        <div style={{ ...txt('small', L), color: Pr.faint }}>{app.t('follow_us')}</div>
        <div style={{ display: 'flex', gap: 14 }}>
          {[['facebook', 'https://www.facebook.com/share/1LgGJqjkPd/?mibextid=wwXIfr'],
            ['instagram', 'https://www.instagram.com/svlmthailand?igsh=MWdubGk4bjJid2Nhaw%3D%3D&utm_source=qr'],
            ['tiktok', 'https://www.tiktok.com/@svlmthailand?_r=1&_t=ZS-98SdwlWIh7d']].map(([ic, href]) => (
            <a key={ic} href={href} target="_blank" rel="noopener noreferrer"
              style={{ width: 40, height: 40, borderRadius: Sr.chip, background: Pr.primarySoft, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
              <Icon name={ic} size={20} color={Pr.primary} />
            </a>
          ))}
        </div>
      </div>
    </div>
  );

  return (
    <React.Fragment>
      <NavyTop lang={L} sheet={sheet}>{header}</NavyTop>
      {cityOpen && <CityPicker app={app} onClose={() => setCityOpen(false)} />}
    </React.Fragment>
  );
}

function BlogDetail({ app, params }) {
  const post = app.blogPosts.find(p => p.id === params.id);
  if (!post) return null;
  return (
    <div>
      <AppHeader onBack={app.back} title={app.pick(post.titleEn, post.titleTh)} />
      <div style={{ padding: '0 20px 40px' }}>
        <Photo src={post.image_url} label="" h={200} r={18} />
        <p style={{ fontSize: 14.5, color: Pr.ink, lineHeight: 1.7, marginTop: 18, whiteSpace: 'pre-wrap' }}>{app.pick(post.bodyEn, post.bodyTh)}</p>
      </div>
    </div>
  );
}

function ServiceCard({ app, s }) {
  return (
    <Card pad={14} onClick={() => app.go('detail', { id: s.id })}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 12 }}>
        <SvcTile name={s.icon} size={46} />
        <Stars value={s.rating} />
      </div>
      <div style={{ fontWeight: 800, fontSize: 14.5, color: Pr.ink, lineHeight: 1.2 }}>{app.pick(s.en, s.th)}</div>
      <div style={{ fontSize: 11.5, color: Pr.faint, margin: '3px 0 10px', lineHeight: 1.3, minHeight: 30 }}>{app.pick(s.tagEn, s.tagTh)}</div>
      <span style={{ fontSize: 13.5, color: Pr.ink }}><ServicePrice app={app} s={s} /></span>
    </Card>
  );
}

/* ---------- Services tab — every real service, grouped by group_id (cleaning/handyman/
   corporate), same card style as Home. ---------- */
function ServicesTab({ app, params }) {
  // Tapping a category tile on Home passes {group: 'cleaning'|'handyman'|'corporate'} to land
  // pre-filtered here; the bottom-nav Services tab itself passes no params, showing everything.
  const [filterGroup, setFilterGroup] = useState(params?.group || 'all');
  const [q, setQ] = useState('');
  const allGroups = ['cleaning', 'handyman', 'corporate'].filter(g => app.services.some(s => s.group === g));
  const groups = filterGroup === 'all' ? allGroups : allGroups.filter(g => g === filterGroup);
  return (
    <div>
      <AppHeader title={app.t('all_services_title') || 'All services'} />
      <div style={{ padding: '0 20px 14px' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, background: '#fff', borderRadius: 16, padding: '0 14px', height: 50, border: `1px solid ${Pr.line}` }}>
          <Icon name="grid" size={19} color={Pr.faint} />
          <input value={q} onChange={e => setQ(e.target.value)} placeholder={app.t('search_services_placeholder') || 'Search services...'} style={{ flex: 1, border: 'none', outline: 'none', fontFamily: 'inherit', fontSize: 14.5, color: Pr.ink, background: 'none' }} />
        </div>
      </div>
      <div style={{ padding: '0 20px 4px', display: 'flex', gap: 8 }}>
        <Chip on={filterGroup === 'all'} onClick={() => setFilterGroup('all')}>{app.t('cat_all') || 'All'}</Chip>
        {allGroups.map(g => {
          const label = GROUP_LABELS[g] || { en: g, th: g };
          return <Chip key={g} on={filterGroup === g} onClick={() => setFilterGroup(g)}>{app.pick(label.en, label.th)}</Chip>;
        })}
      </div>
      <div style={{ padding: '0 20px' }}>
        {groups.map(g => {
          const list = app.services.filter(s => s.group === g && (!q || app.pick(s.en, s.th).toLowerCase().includes(q.toLowerCase())));
          if (list.length === 0) return null;
          const label = GROUP_LABELS[g] || { en: g, th: g };
          return (
            <div key={g} style={{ marginBottom: 22 }}>
              <SectionHead title={app.pick(label.en, label.th)} />
              <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
                {list.map(s => <ServiceCard key={s.id} app={app} s={s} />)}
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
}

/* ---------- Products tab — category picker + real products grid. No design-project source
   screen exists for this (the design only covers services/handyman) — built fresh, reusing the
   same Card/SvcTile/Photo visual language as everywhere else in this preview. ---------- */
function ProductsTab({ app }) {
  const [cat, setCat] = useState('all');
  const list = app.products.filter(p => cat === 'all' || p.categoryId === cat);
  return (
    <div>
      <AppHeader title={app.t('tab_products') || 'Products'} />
      <div style={{ padding: '0 20px' }}>
        <div style={{ display: 'flex', gap: 9, marginBottom: 16, overflowX: 'auto', paddingBottom: 2 }}>
          <Chip on={cat === 'all'} onClick={() => setCat('all')}>{app.t('cat_all') || 'All'}</Chip>
          {app.productCategories.map(c => (
            <Chip key={c.id} on={cat === c.id} onClick={() => setCat(c.id)}>{app.pick(c.en, c.th)}</Chip>
          ))}
        </div>
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
          {list.map(p => (
            <Card key={p.id} pad={0} style={{ overflow: 'hidden' }} onClick={() => app.go('product-detail', { id: p.id })}>
              <Photo src={p.image_url} label={p.en.toUpperCase()} h={120} r={0} tone="mint" />
              <div style={{ padding: '11px 13px 13px' }}>
                <div style={{ fontWeight: 700, fontSize: 13.5, color: Pr.ink, lineHeight: 1.25, minHeight: 34 }}>{app.pick(p.en, p.th)}</div>
                <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 6 }}>
                  <PriceTag price={p.price} original={p.priceOriginal} />
                  {p.stock <= 0 && <span style={{ fontSize: 10.5, fontWeight: 700, color: '#d23a3a' }}>{app.lang === 'th' ? 'หมด' : 'Out of stock'}</span>}
                </div>
              </div>
            </Card>
          ))}
        </div>
        {list.length === 0 && (
          <div style={{ textAlign: 'center', padding: '50px 0', color: Pr.faint }}>
            <Icon name="box" size={42} color={Pr.line} />
            <div style={{ marginTop: 12, fontSize: 14, fontWeight: 600 }}>{app.lang === 'th' ? 'ไม่มีสินค้าในหมวดนี้' : 'No products in this category'}</div>
          </div>
        )}
      </div>
    </div>
  );
}

function ProductDetail({ app, params }) {
  const p = app.products.find(x => x.id === params.id);
  // Declared before the early return below — a hook after a conditional return would change hook
  // order between renders.
  const [added, setAdded] = useState(false);
  useEffect(() => {
    if (!added) return;
    const t = setTimeout(() => setAdded(false), 1800);
    return () => clearTimeout(t);
  }, [added]);
  if (!p) return null;
  return (
    <div>
      <AppHeader onBack={app.back} title={app.pick(p.en, p.th)} right={<CartButton app={app} />} />
      <div style={{ padding: '0 20px 100px' }}>
        <Photo src={p.image_url} label={p.en.toUpperCase()} h={220} r={18} tone="mint" />
        <div style={{ marginTop: 18, fontSize: 20, fontWeight: 800, color: Pr.ink, lineHeight: 1.3 }}>{app.pick(p.en, p.th)}</div>
        <div style={{ marginTop: 6, fontSize: 13, color: p.stock > 0 ? Pr.mint : '#d23a3a', fontWeight: 700 }}>
          {p.stock > 0 ? `${app.lang === 'th' ? 'มีสินค้า' : 'In stock'}: ${p.stock}` : (app.lang === 'th' ? 'สินค้าหมด' : 'Out of stock')}
        </div>
      </div>
      <BottomBar>
        <div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
          <div style={{ fontSize: 22, fontWeight: 800, color: Pr.ink }}><PriceTag price={p.price} original={p.priceOriginal} size="lg" /></div>
          {/* Deliberately does NOT jump to checkout. Doing so made it impossible to buy two
              different products in one order without backing out by hand, which is the whole point
              of having a cart. The header's cart button is the way through once you're done. */}
          <Btn icon="cart" disabled={p.stock <= 0} style={{ flex: 1 }} onClick={() => {
            app.addToCart({ product_id: p.id, quantity: 1, unit_price_thb: p.price, _product_name: app.pick(p.en, p.th) });
            setAdded(true);
          }}>{added
            ? (app.t('added_to_cart') || (app.lang === 'th' ? 'เพิ่มแล้ว' : 'Added'))
            : (app.lang === 'th' ? 'เพิ่มลงตะกร้า' : 'Add to cart')}</Btn>
        </div>
      </BottomBar>
    </div>
  );
}

/* Fetches full tiers/addons/includes for one service before rendering DetailScreen. */
function DetailScreenLoader({ app, params }) {
  const [full, setFull] = useState(null);
  useEffect(() => { hydrateServiceDetail(params.id).then(setFull); }, [params.id]);
  if (!full) return <div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', color: Pr.muted }}>Loading…</div>;
  return <DetailScreen app={app} service={full} />;
}

function DetailScreen({ app, service: s }) {
  const onShare = async () => {
    const url = `${window.location.origin}${window.location.pathname}?service=${s.id}`;
    const title = app.pick(s.en, s.th);
    const text = `${title} — ${app.t('from_price') || 'From'} ${baht(app.priceVat(s))}`;
    const result = await shareLink(url, title, text);
    if (result === 'copied') alert(app.lang === 'th' ? 'คัดลอกลิงก์แล้ว วางเพื่อแชร์ได้เลย' : 'Link copied — paste it anywhere to share.');
    else if (result === 'failed') alert(app.lang === 'th' ? 'แชร์ไม่สำเร็จ ลองอีกครั้ง' : 'Could not share. Please try again.');
  };
  return (
    <div style={{ paddingBottom: 110 }}>
      {/* hero photo — floating back/share buttons, content sheet overlaps the bottom edge */}
      <div style={{ position: 'relative' }}>
        <Photo src={s.image_url} label={s.en.toUpperCase()} h={230} r={0} tone={s.group === 'cleaning' ? 'blue' : 'mint'} />
        <button onClick={app.back} style={{ position: 'absolute', top: 52, left: 16, width: 42, height: 42, borderRadius: 14, background: 'rgba(255,255,255,0.92)', border: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', backdropFilter: 'blur(6px)' }}>
          <Icon name="chevL" size={22} color={Pr.ink} />
        </button>
        <button onClick={onShare} style={{ position: 'absolute', top: 52, right: 16, width: 42, height: 42, borderRadius: 14, background: 'rgba(255,255,255,0.92)', border: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', backdropFilter: 'blur(6px)' }}>
          <Icon name="share" size={20} color={Pr.ink} />
        </button>
      </div>
      <div style={{ padding: '18px 20px 0', marginTop: -28, position: 'relative', background: Pr.bg, borderRadius: '24px 24px 0 0' }}>
        <div style={{ display: 'flex', alignItems: 'flex-start', gap: 14 }}>
          <SvcTile name={s.icon} size={56} on />
          <div style={{ flex: 1 }}>
            <h2 style={{ margin: '2px 0 4px', fontSize: 22, fontWeight: 800, color: Pr.ink, letterSpacing: -0.5 }}>{app.pick(s.en, s.th)}</h2>
            <div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
              <Stars value={s.rating} jobs={s.jobs} />
              <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 12.5, color: Pr.muted, fontWeight: 600 }}><Icon name="clock" size={14} color={Pr.muted} /> {s.dur}</span>
            </div>
          </div>
        </div>
        <p style={{ fontSize: 14, color: Pr.muted, lineHeight: 1.5, margin: '14px 0 18px' }}>{app.pick(s.tagEn, s.tagTh)}</p>
        <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 20 }}>
          <div style={{ display: 'inline-flex', alignItems: 'center', gap: 6, background: Pr.tintBlue, color: Pr.primary, borderRadius: 10, padding: '7px 11px', fontSize: 12, fontWeight: 700 }}>
            <Icon name="badge" size={15} color={Pr.primary} /> {app.lang === 'th' ? 'ทีมผ่านการอบรม' : 'Trained crew'}
          </div>
        </div>
        {s.includesEn.length > 0 && (
          <>
            <SectionHead title={app.t('whats_included') || "What's included"} />
            <Card style={{ marginBottom: 20 }}>
              <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
                {s.includesEn.map((it, i) => (
                  <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 11 }}>
                    <div style={{ width: 24, height: 24, borderRadius: 8, background: Pr.mintSoft, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
                      <Icon name="check" size={15} color={Pr.mint} sw={2.6} />
                    </div>
                    <span style={{ fontSize: 14, color: Pr.ink, fontWeight: 500 }}>{app.pick(it, s.includesTh[i])}</span>
                  </div>
                ))}
              </div>
            </Card>
          </>
        )}
        <SectionHead title={app.lang === 'th' ? 'ราคา' : 'Pricing'} />
        <Card>
          {/* A transport-priced service carries a single placeholder tier at ฿0, because the real
              charge is the distance-based delivery fee worked out once the customer enters their
              addresses. Listing "Standard ฿0" here reads as free — the same trap ServicePrice
              already avoids in the catalogue, which this page was missed out of. */}
          {isTransportPriced(s) ? (
            <div style={{ padding: '10px 0', fontSize: 13, color: Pr.muted }}>
              {app.t('price_by_distance') || 'Priced by distance'}
              <div style={{ fontSize: 12, color: Pr.faint, marginTop: 4 }}>
                {app.lang === 'th'
                  ? 'ราคาขึ้นกับระยะทางและประเภทรถ ระบบจะคำนวณให้เมื่อท่านกรอกที่อยู่'
                  : 'The price depends on the distance and the vehicle, and is calculated once you enter your addresses.'}
              </div>
            </div>
          ) : s.tiers.map((t, i) => (
            <div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '10px 0', borderBottom: i < s.tiers.length - 1 ? `1px solid ${Pr.lineSoft}` : 'none' }}>
              <div>
                <div style={{ fontWeight: 700, fontSize: 14, color: Pr.ink }}>{t.label}</div>
                {(t.subEn || t.subTh) && <div style={{ fontSize: 12, color: Pr.faint }}>{app.pick(t.subEn, t.subTh)}</div>}
              </div>
              <div style={{ fontWeight: 800, fontSize: 15.5 }}><PriceTag price={vatInc(t.price)} original={t.priceOriginal ? vatInc(t.priceOriginal) : null} /></div>
            </div>
          ))}
        </Card>
      </div>
      <BottomBar>
        <div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
          <div>
            {isTransportPriced(s) ? (
              <div style={{ fontSize: 12.5, color: Pr.muted, fontWeight: 700, maxWidth: 110, lineHeight: 1.25 }}>
                {app.t('price_by_distance') || 'Priced by distance'}
              </div>
            ) : (
              <>
                <div style={{ fontSize: 11.5, color: Pr.faint, fontWeight: 600 }}>{app.t('from_price') || 'From'}</div>
                <div style={{ fontSize: 22, fontWeight: 800, lineHeight: 1 }}><PriceTag price={app.priceVat(s)} original={app.priceVatOriginal(s)} size="lg" /></div>
              </>
            )}
          </div>
          <Btn icon="cal" style={{ flex: 1 }} onClick={() => app.go('booking', { id: s.id })}>{app.t('book_now') || 'Book now'}</Btn>
        </div>
      </BottomBar>
    </div>
  );
}

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