PageSourceSearch

https://pizzeria-planeta.pl/js/account.js

js pizzeria-planeta.pl collected 2026-10-08 07:35:09 UTC 51,048 bytes, 1,410 lines download raw bytes

1import {
2  customerLogin,
3  customerRegister,
4  customerLogout,
5  customerMe,
6  customerUpdateProfile,
7  customerFetchOrders,
8  customerRepeatOrder,
9  customerCreateReservation,
10  customerRequestPasswordReset,
11  customerRequestPasswordChange,
12  customerResetPassword,
13  customerVerifyEmail,
14  customerResendVerification,
15  customerDeleteAccount,
16  customerSetNewsletter,
17  customerSetBirthday,
18  customerCompleteMissingProfile,
19  getCustomerUser,
20  isCustomerLoggedIn,
21  clearCustomerSession,
22  initCustomerAccountChrome
23} from './customer-auth.js';
24import { initSocialLogin } from './customer-oauth.js';
25import { fetchLoyaltyDashboard, loyaltyAction, formatReward } from './loyalty-client.js';
26import { renderLoyaltyCalendar, syncCalendarAccountTab } from './loyalty-calendar.js';
27import {
28  buildReferralLink,
29  extractReferralCodeFromSearch,
30  extractAuthTokenFromSearch,
31  formatAccountDate,
32  formatAccountMoney,
33  formatOrderItemsSummary,
34  resolveShopItemDisplay,
35  splitShopByCategory,
36  couponExpiresAt,
37  userNeedsBirthday,
38  userNeedsEmail,
39  userNeedsPhone,
40  userNeedsProfileCompletion,
41  validateRegisterPayload,
42  validatePassword
43} from './account-logic.js';
44import {
45  composeDeliveryAddress,
46  initAddressAutocompleteInRoot,
47  readAddressPartsFromForm
48} from './address-autocomplete.js';
49import {
50  renderFortuneWheel,
51  spinFortuneWheelToPrize,
52  getWheelPrizeIndex,
53  isWheelSpinning,
54  resetWheelCelebration
55} from './fortune-wheel.js';
56import { loadConfig, getConfig, getMenu } from './config.js';
57import { runOrHardReload, showFatalDataLoadError } from './data-load.js';
58import { resolvePublicPath } from './locale.js';
59import { initCookieConsent } from './cookie-consent.js';
60import { initFooterAuthorCredit } from './footer-author.js';
61import {
62  subscribeToNewsletter,
63  syncNewsletterStatus,
64  isNewsletterSubscriber,
65  isNewsletterConfigured,
66  getNewsletterCheckboxLabel,
67  getNewsletterConsentText,
68  setNewsletterSubscription,
69  clearNewsletterSubscription
70} from './newsletter.js';
71
72const $ = (sel, root = document) => root.querySelector(sel);
73
74function escapeHtml(value) {
75  return String(value ?? '')
76    .replaceAll('&', '&')
77    .replaceAll('<', '&lt;')
78    .replaceAll('>', '&gt;')
79    .replaceAll('"', '&quot;');
80}
81
82let loyaltyCache = null;
83
84function formatMoney(n) {
85  return formatAccountMoney(n);
86}
87
88function formatDate(iso) {
89  return formatAccountDate(iso);
90}
91
92function accountOrderTypeLabel(order) {
93  if (order?.type === 'reservation') return 'Rezerwacja stolika';
94  if (order?.type === 'table') return 'Stolik';
95  if (order?.type === 'pickup') return 'Odbiór';
96  return 'Dostawa';
97}
98
99function accountOrderStatusLabel(order) {
100  const status = String(order?.status || '');
101  const map = {
102    completed: 'Zrealizowane',
103    payment_failed: 'Płatność nieudana',
104    payment_pending: 'Oczekuje na płatność',
105    email_failed: 'Błąd e-mail',
106    cancelled: 'Anulowane',
107    reservation_pending: 'Oczekuje na decyzję',
108    reservation_accepted: 'Potwierdzona',
109    reservation_declined: 'Odrzucona'
110  };
111  return map[status] || status || '—';
112}
113
114function showMsg(el, text, isError = false) {
115  if (!el) return;
116  el.hidden = !text;
117  el.textContent = text || '';
118  el.classList.toggle('is-error', !!isError);
119}
120
121function showPanel(name) {
122  document.querySelectorAll('[data-account-panel]').forEach((panel) => {
123    panel.hidden = panel.getAttribute('data-account-panel') !== name;
124  });
125}
126
127function showDashTab(tab) {
128  document.querySelectorAll('[data-dash-tab]').forEach((b) => {
129    b.classList.toggle('is-active', b.getAttribute('data-dash-tab') === tab);
130  });
131  document.querySelectorAll('[data-dash-panel]').forEach((p) => {
132    p.hidden = p.getAttribute('data-dash-panel') !== tab;
133  });
134}
135
136function fillProfileForm(user) {
137  const form = $('#account-profile-form');
138  if (!form || !user) return;
139  form.name.value = user.name || '';
140  form.phone.value = user.phone || '';
141  const pwdBtn = $('#account-password-change-btn');
142  if (pwdBtn) {
143    pwdBtn.textContent = user.hasPassword
144      ? 'Wyślij link do zmiany hasła'
145      : 'Wyślij link do ustawienia hasła';
146  }
147  const emailEl = $('#account-email-display');
148  if (emailEl) {
149    emailEl.textContent = user.email
150      ? user.email
151      : (userNeedsEmail(user) ? 'Brak — uzupełnij w oknie' : '');
152  }
153  const hello = $('#account-hello');
154  if (hello) hello.textContent = user.name ? `Cześć, ${user.name}` : 'Twoje konto';
155
156  const deletePassWrap = $('#account-delete-password-wrap');
157  if (deletePassWrap) {
158    deletePassWrap.hidden = user.hasPassword === false;
159  }
160
161  const bday = $('#account-birthday');
162  const hint = $('#account-birthday-hint');
163  if (bday) {
164    const value = user.birthday ? String(user.birthday).slice(0, 10) : '';
165    bday.value = value;
166    bday.readOnly = true;
167    bday.disabled = true;
168  }
169  if (hint) {
170    hint.textContent = user.birthday
171      ? 'Ustawiona przy zakładaniu konta — nie można zmienić.'
172      : 'Brak daty — uzupełnisz ją w oknie, które pojawi się raz.';
173  }
174}
175
176function showBirthdayModal(visible) {
177  const modal = $('#birthday-modal');
178  if (!modal) return;
179  modal.hidden = !visible;
180  if (!visible && $('#profile-complete-modal')?.hidden !== false) {
181    document.body.classList.toggle('account-modal-open', false);
182  } else if (visible) {
183    document.body.classList.add('account-modal-open');
184  }
185  if (visible) {
186    const input = $('#birthday-modal-input');
187    if (input) {
188      input.value = '';
189      setTimeout(() => input.focus(), 50);
190    }
191    showMsg($('#birthday-modal-msg'), '');
192  }
193}
194
195function showProfileCompleteModal(visible, user = null) {
196  const modal = $('#profile-complete-modal');
197  if (!modal) return;
198  modal.hidden = !visible;
199  document.body.classList.toggle('account-modal-open', !!visible);
200  if (!visible) return;
201
202  const needEmail = userNeedsEmail(user);
203  const needPhone = userNeedsPhone(user);
204  const needBirthday = userNeedsBirthday(user);
205
206  const emailWrap = $('#profile-complete-email-wrap');
207  const phoneWrap = $('#profile-complete-phone-wrap');
208  const birthdayWrap = $('#profile-complete-birthday-wrap');
209  const birthdayNote = $('#profile-complete-birthday-note');
210  const emailInput = $('#profile-complete-email');
211  const phoneInput = $('#profile-complete-phone');
212  const birthdayInput = $('#profile-complete-birthday');
213
214  if (emailWrap) emailWrap.hidden = !needEmail;
215  if (phoneWrap) phoneWrap.hidden = !needPhone;
216  if (birthdayWrap) birthdayWrap.hidden = !needBirthday;
217  if (birthdayNote) birthdayNote.hidden = !needBirthday;
218
219  if (emailInput) {
220    emailInput.required = needEmail;
221    emailInput.value = '';
222  }
223  if (phoneInput) {
224    phoneInput.required = needPhone;
225    phoneInput.value = '';
226  }
227  if (birthdayInput) {
228    birthdayInput.required = needBirthday;
229    birthdayInput.value = '';
230  }
231
232  showMsg($('#profile-complete-modal-msg'), '');
233  const first = needEmail ? emailInput : (needPhone ? phoneInput : birthdayInput);
234  if (first) setTimeout(() => first.focus(), 50);
235}
236
237/** Uzupełnienie e-maila / telefonu / daty urodzenia po Google/FB (gdy brak). */
238async function ensureProfileComplete(user) {
239  if (!userNeedsProfileCompletion(user)) return user;
240
241  // Stary modal tylko-urodziny: gdy brakuje wyłącznie birthday i nie ma nowego modala.
242  const hasCombined = !!$('#profile-complete-modal-form');
243  if (!hasCombined) {
244    return ensureBirthdayOnce(user);
245  }
246
247  return new Promise((resolve) => {
248    const form = $('#profile-complete-modal-form');
249    const msg = $('#profile-complete-modal-msg');
250    if (!form) {
251      resolve(user);
252      return;
253    }
254
255    showProfileCompleteModal(true, user);
256
257    const onSubmit = async (e) => {
258      e.preventDefault();
259      const fd = new FormData(form);
260      showMsg(msg, 'Zapisywanie…');
261      try {
262        const updated = await customerCompleteMissingProfile({
263          email: String(fd.get('email') || '').trim(),
264          phone: String(fd.get('phone') || '').trim(),
265          birthday: String(fd.get('birthday') || '').trim()
266        });
267        form.removeEventListener('submit', onSubmit);
268        showProfileCompleteModal(false);
269        fillProfileForm(updated);
270        resolve(updated);
271      } catch (err) {
272        showMsg(msg, err.message || 'Nie udało się zapisać danych.', true);
273      }
274    };
275
276    form.addEventListener('submit', onSubmit);
277  });
278}
279
280/** Jednorazowe uzupełnienie daty urodzenia (fallback). */
281async function ensureBirthdayOnce(user) {
282  if (!userNeedsBirthday(user)) return user;
283  return new Promise((resolve) => {
284    const form = $('#birthday-modal-form');
285    const msg = $('#birthday-modal-msg');
286    if (!form) {
287      resolve(user);
288      return;
289    }
290
291    showBirthdayModal(true);
292
293    const onSubmit = async (e) => {
294      e.preventDefault();
295      const birthday = String(new FormData(form).get('birthday') || '').trim();
296      showMsg(msg, 'Zapisywanie…');
297      try {
298        const updated = await customerSetBirthday(birthday);
299        form.removeEventListener('submit', onSubmit);
300        showBirthdayModal(false);
301        fillProfileForm(updated);
302        resolve(updated);
303      } catch (err) {
304        showMsg(msg, err.message || 'Nie udało się zapisać daty.', true);
305      }
306    };
307
308    form.addEventListener('submit', onSubmit);
309  });
310}
311
312async function renderOrders() {
313  const listEl = $('#account-orders');
314  const emptyEl = $('#account-orders-empty');
315  if (!listEl) return;
316  listEl.innerHTML = '<p class="account-muted">Ładowanie…</p>';
317  if (emptyEl) emptyEl.hidden = true;
318  try {
319    const orders = await customerFetchOrders({ limit: 40 });
320    if (!orders.length) {
321      listEl.innerHTML = '';
322      if (emptyEl) emptyEl.hidden = false;
323      return;
324    }
325    listEl.innerHTML = orders.map((order) => {
326      const items = formatOrderItemsSummary(order.items || []);
327      const reservationAt = String(order?.meta?.reservationAt || '').trim();
328      const reservationRow = (order?.type === 'reservation' && reservationAt)
329        ? `<p class="account-order__items">Termin: ${formatDate(reservationAt)}</p>`
330        : '';
331      return `
332        <article class="account-order">
333          <div class="account-order__top">
334            <strong>${order.id || ''}</strong>
335            <span>${formatDate(order.createdAt)}</span>
336          </div>
337          <div class="account-order__meta">
338            <span>${accountOrderTypeLabel(order)}</span>
339            <span>${accountOrderStatusLabel(order)}</span>
340            <span>${formatMoney(order.totals?.total)}</span>
341          </div>
342          ${reservationRow}
343          <p class="account-order__items">${order?.type === 'reservation' ? (order?.meta?.notes || '—') : (items || '—')}</p>
344          <div class="account-order__actions">
345            ${order?.type === 'reservation' ? '' : `<button type="button" class="btn btn-secondary btn-sm" data-repeat-id="${order.id}">Powtórz</button>`}
346          </div>
347        </article>`;
348    }).join('');
349    listEl.querySelectorAll('[data-repeat-id]').forEach((btn) => {
350      btn.addEventListener('click', async () => {
351        btn.disabled = true;
352        try {
353          const data = await customerRepeatOrder(btn.getAttribute('data-repeat-id'));
354          localStorage.setItem('planeta_repeat_order', JSON.stringify({
355            items: data.items || [],
356            savedAt: new Date().toISOString()
357          }));
358          window.location.href = 'index.html?repeat=1#menu';
359        } catch (err) {
360          alert(err.message || 'Nie udało się powtórzyć.');
361          btn.disabled = false;
362        }
363      });
364    });
365  } catch (err) {
366    listEl.innerHTML = `<p class="account-msg is-error">${err.message || 'Błąd'}</p>`;
367  }
368}
369
370function bindReservationForm() {
371  const form = $('#account-reservation-form');
372  const msg = $('#account-reservation-msg');
373  if (!form) return;
374  form.addEventListener('submit', async (e) => {
375    e.preventDefault();
376    const fd = new FormData(form);
377    const reservationAt = String(fd.get('reservationAt') || '').trim();
378    const guests = Number(fd.get('guests') || 2);
379    const notes = String(fd.get('notes') || '').trim();
380    showMsg(msg, 'Wysyłanie prośby…');
381    try {
382      await customerCreateReservation({
383        reservationAt,
384        guests,
385        notes,
386        source: 'account_web'
387      });
388      showMsg(msg, 'Wysłano. Odpowiedź pojawi się na koncie i e-mailu.');
389      form.reset();
390      await renderOrders();
391    } catch (err) {
392      showMsg(msg, err.message || 'Nie udało się wysłać prośby.', true);
393    }
394  });
395}
396
397function renderLoyaltyUI(data) {
398  loyaltyCache = data;
399  const points = data.me?.points ?? 0;
400  const badge = $('#account-points-badge');
401  if (badge) badge.textContent = `${points} pkt`;
402
403  const summary = $('#points-summary');
404  if (summary) {
405    summary.textContent = data.enabled
406      ? `Masz ${points} punktów. Za każde 1 zł zamówienia dostajesz ${data.pointsPerZloty ?? 1} pkt.`
407      : 'Program lojalnościowy jest obecnie wyłączony.';
408  }
409
410  const couponsEl = $('#account-coupons');
411  if (couponsEl) {
412    const coupons = data.coupons || [];
413    couponsEl.innerHTML = coupons.length
414      ? coupons.map((c) => {
415        const expires = couponExpiresAt(c);
416        const until = expires
417          ? `<span class="account-coupon__until">Do kiedy: ${escapeHtml(formatDate(expires))}</span>`
418          : '<span class="account-coupon__until account-muted">Bez terminu ważności</span>';
419        return `
420        <div class="account-coupon">
421          <strong>${escapeHtml(c.title || c.code)}</strong>
422          <span>${escapeHtml(formatReward(c))}</span>
423          ${until}
424          <code>${escapeHtml(c.code)}</code>
425          <button type="button" class="btn btn-secondary btn-sm" data-copy-code="${escapeHtml(c.code)}">Kopiuj do koszyka</button>
426        </div>`;
427      }).join('')
428      : '<p class="account-muted">Brak aktywnych kuponów.</p>';
429    couponsEl.querySelectorAll('[data-copy-code]').forEach((btn) => {
430      btn.addEventListener('click', async () => {
431        const code = btn.getAttribute('data-copy-code');
432        const coupon = (data.coupons || []).find((c) => c.code === code);
433        try {
434          localStorage.setItem('promoCode', code);
435        } catch {}
436        if (coupon?.rewardType === 'free_item' && coupon.itemId) {
437          const { queueLoyaltyFreeItem } = await import('./loyalty-free-cart.js');
438          queueLoyaltyFreeItem({
439            itemId: coupon.itemId,
440            couponCode: coupon.code,
441            title: coupon.title || '',
442            size: coupon.itemSize || null
443          });
444        }
445        window.location.href = 'index.html#menu';
446      });
447    });
448  }
449
450  const ledgerEl = $('#account-ledger');
451  if (ledgerEl) {
452    const ledger = Array.isArray(data.ledger) ? data.ledger : [];
453    ledgerEl.innerHTML = ledger.length
454      ? `<ul class="account-ledger">${ledger.map((row) => {
455        const delta = Number(row.delta) || 0;
456        const sign = delta > 0 ? '+' : '';
457        const cls = delta >= 0 ? 'is-plus' : 'is-minus';
458        const label = escapeHtml(row.reasonLabel || row.reason || 'Operacja');
459        const when = escapeHtml(formatDate(row.createdAt));
460        return `<li class="account-ledger__row">
461          <span class="account-ledger__delta ${cls}">${sign}${delta} pkt</span>
462          <span class="account-ledger__meta">
463            <strong>${label}</strong>
464            <span class="account-muted">${when}</span>
465          </span>
466        </li>`;
467      }).join('')}</ul>`
468      : '<p class="account-muted">Brak historii punktów.</p>';
469  }
470
471  const shopCouponsEl = $('#account-shop-coupons');
472  const shopMenuEl = $('#account-shop-menu');
473  if (shopCouponsEl || shopMenuEl) {
474    const menu = getMenu();
475    const { coupons, menu: menuShop } = splitShopByCategory(data.shop || []);
476
477    const bindRedeem = (root) => {
478      root?.querySelectorAll('[data-redeem]').forEach((btn) => {
479        btn.addEventListener('click', async () => {
480          btn.disabled = true;
481          try {
482            const result = await loyaltyAction('redeemShop', { shopItemId: btn.getAttribute('data-redeem') });
483            const coupon = result?.coupon || null;
484            if (coupon?.rewardType === 'free_item' && coupon.itemId) {
485              const { queueLoyaltyFreeItem } = await import('./loyalty-free-cart.js');
486              queueLoyaltyFreeItem({
487                itemId: coupon.itemId,
488                couponCode: coupon.code,
489                title: coupon.title || '',
490                size: coupon.itemSize || null
491              });
492              window.location.href = 'index.html#menu';
493              return;
494            }
495            if (coupon?.code) {
496              try { localStorage.setItem('promoCode', coupon.code); } catch {}
497            }
498            await refreshLoyalty();
499            alert('Nagroda wymieniona — kupon jest w sekcji „Twoje kupony”.');
500          } catch (err) {
501            alert(err.message || 'Nie udało się wymienić.');
502            btn.disabled = false;
503          }
504        });
505      });
506    };
507
508    if (shopCouponsEl) {
509      shopCouponsEl.innerHTML = coupons.length
510        ? coupons.map((s) => {
511          const display = resolveShopItemDisplay(s, menu);
512          const detail = s.rewardType === 'percent'
513            ? `-${s.rewardValue}%`
514            : `-${s.rewardValue} zł`;
515          return `
516          <article class="account-shop-item">
517            <div>
518              <strong>${escapeHtml(display.name)}</strong>
519              <p class="account-muted">${s.costPoints} pkt · ${detail}${s.minOrder ? ` (min. ${s.minOrder} zł)` : ''}</p>
520            </div>
521            <button type="button" class="btn btn-primary btn-sm" data-redeem="${escapeHtml(s.id)}" ${points < s.costPoints ? 'disabled' : ''}>Wymień</button>
522          </article>`;
523        }).join('')
524        : '<p class="account-muted">Brak kuponów w sklepiku.</p>';
525      bindRedeem(shopCouponsEl);
526    }
527
528    if (shopMenuEl) {
529      shopMenuEl.innerHTML = menuShop.length
530        ? menuShop.map((s) => {
531          const display = resolveShopItemDisplay(s, menu);
532          const img = display.image
533            ? `<img class="account-shop-item__img" src="${escapeHtml(resolvePublicPath(display.image))}" alt="" loading="lazy">`
534            : `<div class="account-shop-item__img account-shop-item__img--empty" aria-hidden="true"></div>`;
535          return `
536          <article class="account-shop-item account-shop-item--menu">
537            ${img}
538            <div class="account-shop-item__body">
539              <strong>${escapeHtml(display.name)}</strong>
540              <p class="account-muted">
540${s.costPoints} pkt · gratis${s.minOrder ? ` (min. ${s.minOrder} zł)` : ''}</p>
541              <button type="button" class="btn btn-primary btn-sm" data-redeem="${escapeHtml(s.id)}" ${points < s.costPoints ? 'disabled' : ''}>Wymień</button>
542            </div>
543          </article>`;
544        }).join('')
545        : '<p class="account-muted">Brak pozycji menu w sklepiku.</p>';
546      bindRedeem(shopMenuEl);
547    }
548
549    document.querySelectorAll('[data-shop-tab]').forEach((btn) => {
550      btn.onclick = () => {
551        const tab = btn.getAttribute('data-shop-tab');
552        document.querySelectorAll('[data-shop-tab]').forEach((b) => {
553          const on = b === btn;
554          b.classList.toggle('is-active', on);
555          b.setAttribute('aria-selected', on ? 'true' : 'false');
556        });
557        document.querySelectorAll('[data-shop-panel]').forEach((panel) => {
558          panel.hidden = panel.getAttribute('data-shop-panel') !== tab;
559        });
560      };
561    });
562  }
563
564  const favEl = $('#account-favorites');
565  if (favEl) {
566    const favs = data.favorites || [];
567    favEl.innerHTML = favs.length
568      ? `<ul>${favs.map((id) => `<li><a href="index.html#menu">${id}</a>
569          <button type="button" class="btn btn-secondary btn-sm" data-unfav="${id}">Usuń</button></li>`).join('')}</ul>`
570      : '<p class="account-muted">Brak ulubionych. Dodaj je z menu na stronie głównej.</p>';
571    favEl.querySelectorAll('[data-unfav]').forEach((btn) => {
572      btn.addEventListener('click', async () => {
573        await loyaltyAction('toggleFavorite', { itemId: btn.getAttribute('data-unfav') });
574        await refreshLoyalty();
575      });
576    });
577  }
578
579  const addrForm = $('#account-addresses-form');
580  if (addrForm) {
581    const addrs = data.addresses || [];
582    addrForm.innerHTML = [0, 1, 2].map((i) => {
583      const a = addrs[i] || { label: i === 0 ? 'Dom' : i === 1 ? 'Praca' : `Adres ${i + 1}`, city: '', street: '', number: '', apartment: '' };
584      return `
585        <div class="account-addr-slot input-group input-group--address" data-address-group data-addr-slot="${i}">
586          <label class="account-addr-label">Etykieta
587            <input type="text" name="label${i}" value="${escapeHtml(a.label || '')}" placeholder="Dom / Praca">
588          </label>
589          <div class="address-label-row">
590            <span class="address-group-label">Adres dostawy</span>
591          </div>
592          <div class="address-fields">
593            <div class="address-field address-field--city">
594              <label for="addr${i}-city">Miasto</label>
595              <input type="text" id="addr${i}-city" name="city${i}" data-address-part="city"
596                value="${escapeHtml(a.city || '')}" placeholder="Elbląg" autocomplete="address-level2">
597            </div>
598            <div class="address-field address-field--street">
599              <label for="addr${i}-street">Ulica</label>
600              <input type="text" id="addr${i}-street" name="street${i}" data-address-part="street"
601                value="${escapeHtml(a.street || '')}" placeholder="np. Dąbka" autocomplete="off" autocorrect="off" autocapitalize="words" spellcheck="false" inputmode="text">
602            </div>
603            <div class="address-field address-field--number">
604              <label for="addr${i}-number">Numer</label>
605              <input type="text" id="addr${i}-number" name="number${i}" data-address-part="number"
606                value="${escapeHtml(a.number || '')}" placeholder="140" autocomplete="off">
607            </div>
608            <div class="address-field address-field--apt">
609              <label for="addr${i}-apt">Nr lokalu</label>
610              <input type="text" id="addr${i}-apt" name="apartment${i}" data-address-part="apartment"
611                value="${escapeHtml(a.apartment || '')}" placeholder="opcjonalnie" autocomplete="off">
612            </div>
613          </div>
614          <small class="input-hint">Podaj miasto, ulicę i numer domu. Numer lokalu jest opcjonalny.</small>
615        </div>`;
616    }).join('');
617
618    void (async () => {
619      try {
620        await loadConfig();
621      } catch (err) {
622        console.warn(err);
623      }
624      const config = getConfig();
625      if (!config) return;
626      addrForm.querySelectorAll('[data-address-group]').forEach((group) => {
627        void initAddressAutocompleteInRoot(group, config);
628      });
629    })();
630  }
631
632  const recsEl = $('#account-recs');
633  if (recsEl) {
634    const recs = data.recommendations || [];
635    recsEl.innerHTML = recs.length
636      ? `<ul>${recs.map((r) => `<li><strong>${r.name}</strong> · zamawiane ${r.count}× · <a href="index.html#menu">Do menu</a></li>`).join('')}</ul>`
637      : '<p class="account-muted">Za mało historii, żeby coś polecić — złóż pierwsze zamówienia.</p>';
638  }
639
640  const wheelStage = $('#fortune-wheel-stage');
641  if (wheelStage && !isWheelSpinning()) {
642    const nextPrizes = data.wheel?.prizes || [];
643    const prev = wheelStage._wheelPrizes || [];
644    const same =
645      prev.length === nextPrizes.length
646      && nextPrizes.every((p, i) => p.id === prev[i]?.id && p.label === prev[i]?.label);
647    // Nie przebudowuj koła po spinie, jeśli lista nagród się nie zmieniła — zachowaj kąt i efekt wygranej
648    if (!same || !wheelStage.querySelector('[data-wheel-root]')) {
649      renderFortuneWheel(wheelStage, nextPrizes);
650    } else {
651      wheelStage._wheelPrizes = nextPrizes;
652      wheelStage.dataset.prizeCount = String(nextPrizes.length || 1);
653    }
654  }
655  const wheelStatus = $('#wheel-status');
656  if (wheelStatus && data.wheel) {
657    wheelStatus.textContent = data.wheel.enabled
658      ? `Możesz kręcić raz na ${data.wheel.cooldownDays || 7} dni. Wskaźnik u góry pokazuje wygraną.`
659      : 'Koło fortuny jest wyłączone.';
660  }
661  const spinBtn = $('#wheel-spin');
662  if (spinBtn) {
663    spinBtn.disabled = !data.wheel?.enabled || isWheelSpinning();
664  }
665
666  const refCode = $('#referral-code');
667  const refLink = $('#referral-link');
668  if (refCode) refCode.textContent = data.referral?.code || '—';
669  if (refLink) {
670    const href = buildReferralLink(window.location.origin, data.referral?.code || '');
671    refLink.href = href;
672    refLink.textContent = href;
673  }
674  const refRewards = $('#referral-rewards');
675  const refRules = $('#referral-rules');
676  if (refRewards || refRules) {
677    const ref = data.referral || {};
678    const fmt = (type, value) => {
679      const t = String(type || '').toLowerCase();
680      const v = Number(value) || 0;
681      if (t === 'percent') return `−${v}%`;
682      if (t === 'fixed') return `−${v.toFixed(0)} zł`;
683      return v ? String(v) : 'nagrodę lojalnościową';
684    };
685    if (refRewards) {
686      refRewards.innerHTML = ref.enabled === false
687        ? 'Program poleceń jest obecnie wyłączony.'
688        : `Ty (za polecenie): <strong>${escapeHtml(fmt(ref.referrerType, ref.referrerValue))}</strong><br>
689           Znajomy (za rejestrację z kodem): <strong>${escapeHtml(fmt(ref.refereeType, ref.refereeValue))}</strong>`;
690    }
691    if (refRules) {
692      const days = Number(ref.validDays) || 30;
693      const min = Number(ref.minOrder) || 0;
694      const cooldown = Number(ref.referrerCooldownHours) || 24;
695      refRules.textContent = ref.enabled === false
696        ? ''
697        : `Kupony ważne ${days} dni${min > 0 ? ` · min. zamówienie ${min.toFixed(0)} zł` : ''}. Kod tylko przy rejestracji (raz na konto znajomego). Ty możesz polecać wielu znajomych — Twoja nagroda max raz na ${cooldown} h.`;
698    }
699  }
700
701  const promo = $('#account-personal-promo');
702  if (promo) {
703    if (data.personalPromo) {
704      promo.hidden = false;
705      promo.innerHTML = `<strong>${data.personalPromo.message}</strong>
706        <button type="button" class="btn btn-primary btn-sm" data-copy-code="${data.personalPromo.code}">Użyj kodu ${data.personalPromo.code}</button>`;
707      promo.querySelector('[data-copy-code]')?.addEventListener('click', () => {
708        try { localStorage.setItem('promoCode', data.personalPromo.code); } catch {}
709        window.location.href = 'index.html#menu';
710      });
711    } else {
712      promo.hidden = true;
713      promo.innerHTML = '';
714    }
715  }
716
717  const reminder = $('#account-reminder');
718  if (reminder) {
719    if (data.reminder && !data.personalPromo) {
720      reminder.hidden = false;
721      reminder.innerHTML = `<strong>${data.reminder.message}</strong>
722        <a class="btn btn-secondary btn-sm" href="index.html#menu">Zamów teraz</a>`;
723    } else {
724      reminder.hidden = true;
725      reminder.innerHTML = '';
726    }
727  }
728
729  const bday = $('#account-birthday');
730  if (bday && data.me?.birthday) {
731    bday.value = String(data.me.birthday).slice(0, 10);
732    bday.readOnly = true;
733    bday.disabled = true;
734  }
735
736  const showCalendar = syncCalendarAccountTab(data);
737  if (!showCalendar && document.querySelector('[data-dash-tab="calendar"].is-active')) {
738    showDashTab('points');
739  }
740  const calRoot = $('#account-calendar');
741  if (calRoot && showCalendar) {
742    renderLoyaltyCalendar(calRoot, data.calendar, {
743      onClaimed: async (result) => {
744        const extra = result?.reward?.kind === 'points'
745          ? ` (+${result.reward.points} pkt)`
746          : (result?.reward?.coupon?.code ? ` · kod ${result.reward.coupon.code}` : '');
747        await refreshLoyalty();
748        const msg = $('#loycal-msg');
749        if (msg) {
750          msg.hidden = false;
751          msg.classList.remove('is-error');
752          msg.textContent = (result?.message || 'Nagroda odebrana.') + extra;
753        }
754      }
755    });
756  }
757}
758
759async function refreshLoyalty() {
760  try {
761    const data = await fetchLoyaltyDashboard();
762    renderLoyaltyUI(data);
763  } catch (err) {
764    console.warn('[loyalty]', err);
765  }
766}
767
768async function enterLoggedIn(user) {
769  showPanel('dashboard');
770  fillProfileForm(user);
771  const completed = await ensureProfileComplete(user || getCustomerUser());
772  fillProfileForm(completed || user);
773  showDashTab('orders');
774  await Promise.all([
775    renderOrders(),
776    refreshLoyalty(),
777    refreshProfileNewsletter(completed || user || getCustomerUser())
778  ]);
779}
780
781function socialLegalAccepted() {
782  return !!$('#account-social-legal')?.checked;
783}
784
785function syncSocialLegalGate() {
786  const row = $('#account-social-row');
787  if (!row) return;
788  row.classList.toggle('is-blocked', !socialLegalAccepted());
789}
790
791async function refreshProfileNewsletter(user) {
792  const statusEl = $('#profile-newsletter-status');
793  const wrap = $('#profile-newsletter-wrap');
794  const btn = $('#profile-newsletter-btn');
795  const unsubBtn = $('#profile-newsletter-unsub');
796  if (!statusEl) return;
797
798  const email = String(user?.email || getCustomerUser()?.email || '').trim();
799  if (!email) {
800    statusEl.textContent = 'Brak e-maila na koncie — newsletter niedostępny.';
801    if (wrap) wrap.hidden = true;
802    if (btn) btn.hidden = true;
803    if (unsubBtn) unsubBtn.hidden = true;
804    return;
805  }
806
807  const config = getConfig() || await loadConfig();
808  if (!isNewsletterConfigured(config)) {
809    statusEl.textContent = 'Newsletter jest obecnie wyłączony.';
810    if (wrap) wrap.hidden = true;
811    if (btn) btn.hidden = true;
812    if (unsubBtn) unsubBtn.hidden = true;
813    return;
814  }
815
816  statusEl.textContent = 'Sprawdzanie statusu…';
817  const subscribed = await syncNewsletterStatus(email) || isNewsletterSubscriber(email);
818  if (subscribed) {
819    statusEl.textContent = 'Jesteś zapisany do newslettera. Możesz wypisać się poniżej.';
820    if (wrap) wrap.hidden = true;
821    if (btn) btn.hidden = true;
822    if (unsubBtn) unsubBtn.hidden = false;
823    return;
824  }
825
826  statusEl.textContent = 'Nie jesteś jeszcze zapisany — możesz dołączyć poniżej.';
827  if (wrap) {
828    wrap.hidden = false;
829    const strong = wrap.querySelector('strong');
830    const small = wrap.querySelector('small');
831    if (strong) strong.textContent = getNewsletterCheckboxLabel(config);
832    if (small) small.textContent = getNewsletterConsentText(config);
833  }
834  if (btn) btn.hidden = false;
835  if (unsubBtn) unsubBtn.hidden = true;
836}
837
838function showAuthForm(name) {
839  document.querySelectorAll('[data-account-tab]').forEach((b) => {
840    const tab = b.getAttribute('data-account-tab');
841    b.classList.toggle('is-active', tab === name || (name === 'forgot' && tab === 'login') || (name === 'reset' && tab === 'login'));
842  });
843  document.querySelectorAll('[data-account-form]').forEach((form) => {
844    form.hidden = form.getAttribute('data-account-form') !== name;
845  });
846  const verifyBanner = $('#account-verify-banner');
847  if (verifyBanner) verifyBanner.hidden = true;
848  const loginVerify = $('#account-login-verify');
849  if (loginVerify && name !== 'login') loginVerify.hidden = true;
850  const registerVerify = $('#account-register-verify');
851  if (registerVerify && name !== 'register') registerVerify.hidden = true;
852}
853
854function cleanAuthQueryParams() {
855  try {
856    const u = new URL(window.location.href);
857    let changed = false;
858    for (const key of ['verify', 'reset']) {
859      if (u.searchParams.has(key)) {
860        u.searchParams.delete(key);
861        changed = true;
862      }
863    }
864    if (changed) {
865      window.history.replaceState(null, '', u.pathname + (u.search || '') + (u.hash || ''));
866    }
867  } catch {}
868}
869
870async function handleVerifyFromUrl() {
871  const found = extractAuthTokenFromSearch(window.location.search, ['verify']);
872  if (!found?.token) return false;
873
874  showPanel('auth');
875  showAuthForm('login');
876  const banner = $('#account-verify-banner');
877  const msg = $('#account-verify-msg');
878  if (banner) banner.hidden = false;
879  document.querySelectorAll('[data-account-form]').forEach((form) => { form.hidden = true; });
880  showMsg(msg, 'Potwierdzanie adresu e-mail…');
881
882  try {
883    const data = await customerVerifyEmail(found.token);
884    cleanAuthQueryParams();
885    showMsg(msg, data.message || 'E-mail potwierdzony. Jesteś zalogowany.');
886    await enterLoggedIn(data.customer || getCustomerUser());
887    const ref = ($('#register-ref')?.value || sessionStorage.getItem('planeta_referral_code') || '').trim();
888    if (ref) {
889      try { await loyaltyAction('applyReferral', { code: ref }); } catch {}
890    }
891    return true;
892  } catch (err) {
893    cleanAuthQueryParams();
894    showMsg(msg, err.message || 'Link weryfikacji jest nieprawidłowy lub wygasł.', true);
895    showAuthForm('login');
896    if (banner) banner.hidden = false;
897    return true;
898  }
899}
900
901function handleResetFromUrl() {
902  const found = extractAuthTokenFromSearch(window.location.search, ['reset']);
903  if (!found?.token) return null;
904  showPanel('auth');
905  showAuthForm('reset');
906  const form = $('#account-reset-form');
907  if (form) form.dataset.resetToken = found.token;
908  return found.token;
909}
910
911async function resendVerificationForEmail(email, msgEl, actionsEl) {
912  const normalized = String(email || '').trim();
913  if (!normalized) {
914    showMsg(msgEl, 'Podaj e-mail, na który mamy wysłać link.', true);
915    return;
916  }
917  showMsg(msgEl, 'Wysyłanie linku…');
918  try {
919    const data = await customerResendVerification(normalized);
920    showMsg(msgEl, data.message || 'Jeśli konto wymaga potwierdzenia, wysłaliśmy nowy link.');
921    if (actionsEl) actionsEl.hidden = false;
922  } catch (err) {
923    showMsg(msgEl, err.message || 'Nie udało się wysłać linku.', true);
924  }
925}
926
927async function subscribeAccountNewsletter({ email, name = '', phone = '', source = 'account' }) {
928  const result = await subscribeToNewsletter({ email, name, phone, source });
929  if (!result.ok) {
930    throw new Error(result.error || 'Nie udało się zapisać do newslettera.');
931  }
932  return result;
933}
934
935function captureReferralFromUrl() {
936  const ref = extractReferralCodeFromSearch(window.location.search);
937  if (ref) {
938    try { sessionStorage.setItem('planeta_referral_code', ref); } catch {}
939    const input = $('#register-ref');
940    if (input) input.value = ref;
941  } else {
942    try {
943      const saved = sessionStorage.getItem('planeta_referral_code');
944      if (saved && $('#register-ref')) $('#register-ref').value = saved;
945    } catch {}
946  }
947}
948
949async function boot() {
950  initCookieConsent();
951  initFooterAuthorCredit();
952  initCustomerAccountChrome();
953  captureReferralFromUrl();
954  try {
955    await runOrHardReload(() => loadConfig());
956  } catch (err) {
957    showFatalDataLoadError(err);
958    return;
959  }
960  const loginForm = $('#account-login-form');
961  const registerForm = $('#account-register-form');
962  const forgotForm = $('#account-forgot-form');
963  const resetForm = $('#account-reset-form');
964  const profileForm = $('#account-profile-form');
965  const logoutBtn = $('#account-logout');
966  const loginMsg = $('#account-login-msg');
967  const registerMsg = $('#account-register-msg');
968  const forgotMsg = $('#account-forgot-msg');
969  const resetMsg = $('#account-reset-msg');
970  const profileMsg = $('#account-profile-msg');
971  const socialMsg = $('#account-social-msg');
972  let pendingVerifyEmail = '';
973  bindReservationForm();
974
975  document.querySelectorAll('[data-account-tab]').forEach((btn) => {
976    btn.addEventListener('click', () => {
977      const tab = btn.getAttribute('data-account-tab');
978      showAuthForm(tab);
979      showMsg(loginMsg, '');
980      showMsg(registerMsg, '');
981      showMsg(forgotMsg, '');
982      showMsg(resetMsg, '');
983      const loginVerify = $('#account-login-verify');
984      if (loginVerify) loginVerify.hidden = true;
985      const registerVerify = $('#account-register-verify');
986      if (registerVerify) registerVerify.hidden = true;
987    });
988  });
989
990  document.querySelectorAll('[data-dash-tab]').forEach((btn) => {
991    btn.addEventListener('click', () => showDashTab(btn.getAttribute('data-dash-tab')));
992  });
993
994  void initSocialLogin({
995    googleContainer: $('#google-login-btn'),
996    facebookButton: $('#facebook-login-btn'),
997    onSuccess: async (data) => {
998      if (!socialLegalAccepted()) {
999        showMsg(socialMsg, 'Zaznacz akceptację Regulaminu i Polityki prywatności.', true);
1000        try {
1001          await customerLogout();
1002        } catch {
1003          clearCustomerSession();
1004        }
1005        return;
1006      }
1007      try { localStorage.setItem('planeta_account_legal_ok', '1'); } catch {}
1008      showMsg(socialMsg, '');
1009      await enterLoggedIn(data.customer);
1010      const ref = ($('#register-ref')?.value || sessionStorage.getItem('planeta_referral_code') || '').trim();
1011      if (ref) {
1012        try { await loyaltyAction('applyReferral', { code: ref }); } catch {}
1013      }
1014    },
1015    onError: (err) => showMsg(socialMsg, err?.message || 'Błąd', true)
1016  });
1017
1018  const socialLegal = $('#account-social-legal');
1019  try {
1020    if (socialLegal && localStorage.getItem('planeta_account_legal_ok') === '1') {
1021      socialLegal.checked = true;
1022    }
1023  } catch {}
1024  socialLegal?.addEventListener('change', () => {
1025    if (socialLegal.checked) {
1026      try { localStorage.setItem('planeta_account_legal_ok', '1'); } catch {}
1027    }
1028    syncSocialLegalGate();
1029  });
1030  syncSocialLegalGate();
1031
1032  // Blokada Google/FB bez akceptacji regulaminu
1033  $('#account-social')?.addEventListener('click', (e) => {
1034    if (socialLegalAccepted()) return;
1035    const target = e.target;
1036    if (!(target instanceof Element)) return;
1037    if (target.closest('#account-social-legal') || target.closest('label[for="account-social-legal"]')) return;
1038    if (target.closest('#google-login-btn') || target.closest('#facebook-login-btn') || target.closest('.account-social__row')) {
1039      e.preventDefault();
1040      e.stopPropagation();
1041      showMsg(socialMsg, 'Najpierw zaakceptuj Regulamin i Politykę prywatności.', true);
1042    }
1043  }, true);
1044
1045  $('#account-forgot-toggle')?.addEventListener('click', () => {
1046    const email = String(loginForm?.email?.value || '').trim();
1047    showAuthForm('forgot');
1048    if (forgotForm && email) forgotForm.email.value = email;
1049    showMsg(forgotMsg, '');
1050  });
1051
1052  $('#account-forgot-back')?.addEventListener('click', () => {
1053    showAuthForm('login');
1054  });
1055
1056  forgotForm?.addEventListener('submit', async (e) => {
1057    e.preventDefault();
1058    const fd = new FormData(forgotForm);
1059    showMsg(forgotMsg, 'Wysyłanie…');
1060    try {
1061      const data = await customerRequestPasswordReset(String(fd.get('email') || ''));
1062      showMsg(forgotMsg, data.message || 'Jeśli konto istnieje, wysłaliśmy link do resetu.');
1063    } catch (err) {
1064      showMsg(forgotMsg, err.message || 'Nie udało się wysłać linku.', true);
1065    }
1066  });
1067
1068  resetForm?.addEventListener('submit', async (e) => {
1069    e.preventDefault();
1070    const fd = new FormData(resetForm);
1071    const password = String(fd.get('password') || '');
1072    const passwordConfirm = String(fd.get('passwordConfirm') || '');
1073    const token = String(resetForm.dataset.resetToken || '').trim();
1074    const passErr = validatePassword(password);
1075    if (passErr) {
1076      showMsg(resetMsg, passErr, true);
1077      return;
1078    }
1079    if (password !== passwordConfirm) {
1080      showMsg(resetMsg, 'Hasła nie są takie same.', true);
1081      return;
1082    }
1083    if (!token) {
1084      showMsg(resetMsg, 'Brak tokenu resetu. Poproś o nowy link.', true);
1085      return;
1086    }
1087    showMsg(resetMsg, 'Zapisywanie…');
1088    try {
1089      const data = await customerResetPassword({ token, password });
1090      cleanAuthQueryParams();
1091      delete resetForm.dataset.resetToken;
1092      showMsg(resetMsg, data.message || 'Hasło zostało zmienione. Zaloguj się nowym hasłem.');
1093      setTimeout(() => showAuthForm('login'), 1200);
1094    } catch (err) {
1095      showMsg(resetMsg, err.message || 'Nie udało się zmienić hasła.', true);
1096    }
1097  });
1098
1099  $('#account-resend-verify')?.addEventListener('click', async () => {
1100    const email = pendingVerifyEmail || String(loginForm?.email?.value || '').trim();
1101    await resendVerificationForEmail(email, loginMsg, $('#account-login-verify'));
1102  });
1103
1104  $('#account-register-resend')?.addEventListener('click', async () => {
1105    const email = pendingVerifyEmail || String(registerForm?.email?.value || '').trim();
1106    await resendVerificationForEmail(email, registerMsg, $('#account-register-verify'));
1107  });
1108
1109  loginForm?.addEventListener('submit', async (e) => {
1110    e.preventDefault();
1111    const fd = new FormData(loginForm);
1112    const email = String(fd.get('email') || '');
1113    showMsg(loginMsg, 'Logowanie…');
1114    const loginVerify = $('#account-login-verify');
1115    if (loginVerify) loginVerify.hidden = true;
1116    try {
1117      const data = await customerLogin({
1118        email,
1119        password: String(fd.get('password') || '')
1120      });
1121      pendingVerifyEmail = '';
1122      await enterLoggedIn(data.customer);
1123    } catch (err) {
1124      showMsg(loginMsg, err.message || 'Logowanie nieudane.', true);
1125      if (err?.status === 403) {
1126        pendingVerifyEmail = email;
1127        if (loginVerify) loginVerify.hidden = false;
1128      }
1129    }
1130  });
1131
1132  registerForm?.addEventListener('submit', async (e) => {
1133    e.preventDefault();
1134    const fd = new FormData(registerForm);
1135    const legalAccepted = !!$('#register-legal-accept')?.checked;
1136    const validated = validateRegisterPayload({
1137      name: String(fd.get('name') || ''),
1138      email: String(fd.get('email') || ''),
1139      phone: String(fd.get('phone') || ''),
1140      address: String(fd.get('address') || ''),
1141      birthday: String(fd.get('birthday') || ''),
1142      password: String(fd.get('password') || ''),
1143      legalAccepted,
1144      referralCode: String(fd.get('referralCode') || '')
1145    });
1146    if (!validated.ok) {
1147      showMsg(registerMsg, validated.error || 'Uzupełnij formularz.', true);
1148      if (!legalAccepted) {
1149        $('#register-legal-accept')?.focus();
1150      }
1151      return;
1152    }
1153    showMsg(registerMsg, 'Tworzenie konta…');
1154    const registerVerify = $('#account-register-verify');
1155    if (registerVerify) registerVerify.hidden = true;
1156    try {
1157      const { payload } = validated;
1158      const data = await customerRegister({
1159        name: payload.name,
1160        email: payload.email,
1161        phone: payload.phone,
1162        address: payload.address,
1163        birthday: payload.birthday,
1164        password: payload.password,
1165        legalAccepted: true
1166      });
1167      if ($('#register-newsletter')?.checked) {
1168        try {
1169          await subscribeAccountNewsletter({
1170            email: payload.email,
1171            name: payload.name,
1172            phone: payload.phone,
1173            source: 'account_register'
1174          });
1175        } catch (nlErr) {
1176          console.warn('[newsletter]', nlErr);
1177        }
1178      }
1179      try { localStorage.setItem('planeta_account_legal_ok', '1'); } catch {}
1180
1181      if (data.needsEmailVerification || !data.token) {
1182        pendingVerifyEmail = payload.email;
1183        const mailFailed = data.mailSent === false;
1184        showMsg(
1185          registerMsg,
1186          data.message
1187            || (mailFailed
1188              ? 'Konto utworzone, ale e-mail weryfikacyjny nie wyszedł. Kliknij „Wyślij ponownie”.'
1189              : 'Konto utworzone. Sprawdź skrzynkę e-mail i potwierdź adres, aby się zalogować.'),
1190          mailFailed
1191        );
1192        if (registerVerify) registerVerify.hidden = false;
1193        return;
1194      }
1195
1196      await enterLoggedIn(data.customer);
1197      const ref = payload.referralCode;
1198      if (ref) {
1199        try {
1200          const r = await loyaltyAction('applyReferral', { code: ref });
1201          showMsg(registerMsg, r.message || 'Konto utworzone + kod polecenia.');
1202        } catch (err) {
1203          showMsg(registerMsg, `Konto OK. Kod polecenia: ${err.message}`, true);
1204        }
1205      }
1206    } catch (err) {
1207      showMsg(registerMsg, err.message || 'Rejestracja nieudana.', true);
1208    }
1209  });
1210
1211  profileForm?.addEventListener('submit', async (e) => {
1212    e.preventDefault();
1213    const fd = new FormData(profileForm);
1214    showMsg(profileMsg, 'Zapisywanie…');
1215    try {
1216      const user = await customerUpdateProfile({
1217        name: String(fd.get('name') || ''),
1218        phone: String(fd.get('phone') || '')
1219      });
1220      fillProfileForm(user);
1221      showMsg(profileMsg, 'Zapisano.');
1222      await refreshProfileNewsletter(user);
1223      await refreshLoyalty();
1224    } catch (err) {
1225      showMsg(profileMsg, err.message || 'Błąd zapisu.', true);
1226    }
1227  });
1228
1229  $('#account-password-change-btn')?.addEventListener('click', async () => {
1230    showMsg(profileMsg, 'Wysyłanie linku…');
1231    try {
1232      const data = await customerRequestPasswordChange();
1233      showMsg(profileMsg, data.message || 'Sprawdź e-mail — wysłaliśmy link do zmiany hasła.');
1234    } catch (err) {
1235      showMsg(profileMsg, err.message || 'Nie udało się wysłać linku.', true);
1236    }
1237  });
1238
1239  $('#profile-newsletter-btn')?.addEventListener('click', async () => {
1240    const user = getCustomerUser();
1241    const email = String(user?.email || '').trim();
1242    if (!email) {
1243      showMsg(profileMsg, 'Brak e-maila na koncie.', true);
1244      return;
1245    }
1246    const box = $('#profile-newsletter');
1247    if (box && !box.checked) {
1248      showMsg(profileMsg, 'Zaznacz zgodę na newsletter.', true);
1249      return;
1250    }
1251    showMsg(profileMsg, 'Zapisywanie do newslettera…');
1252    try {
1253      await customerSetNewsletter(true);
1254      setNewsletterSubscription({ email, source: 'account_profile' });
1255      showMsg(profileMsg, 'Zapisano do newslettera.');
1256      await refreshProfileNewsletter(user);
1257    } catch (err) {
1258      try {
1259        await subscribeAccountNewsletter({
1260          email,
1261          name: String(user?.name || ''),
1262          phone: String(user?.phone || ''),
1263          source: 'account_profile'
1264        });
1265        showMsg(profileMsg, 'Zapisano do newslettera.');
1266        await refreshProfileNewsletter(user);
1267      } catch (fallbackErr) {
1268        showMsg(profileMsg, fallbackErr.message || err.message || 'Nie udało się zapisać.', true);
1269      }
1270    }
1271  });
1272
1273  $('#profile-newsletter-unsub')?.addEventListener('click', async () => {
1274    const user = getCustomerUser();
1275    showMsg(profileMsg, 'Wypisywanie…');
1276    try {
1277      await customerSetNewsletter(false);
1278      clearNewsletterSubscription();
1279      showMsg(profileMsg, 'Wypisano z newslettera.');
1280      await refreshProfileNewsletter(user);
1281    } catch (err) {
1282      showMsg(profileMsg, err.message || 'Nie udało się wypisać.', true);
1283    }
1284  });
1285
1286  $('#account-delete-btn')?.addEventListener('click', async () => {
1287    const confirmVal = String($('#account-delete-confirm')?.value || '').trim();
1288    const password = String($('#account-delete-password')?.value || '');
1289    const deleteMsg = $('#account-delete-msg');
1290    const user = getCustomerUser();
1291    if (confirmVal.toUpperCase() !== 'USUN') {
1292      showMsg(deleteMsg, 'Aby usunąć konto, wpisz USUN.', true);
1293      return;
1294    }
1295    if (user?.hasPassword !== false && !password) {
1296      showMsg(deleteMsg, 'Podaj hasło, aby usunąć konto.', true);
1297      return;
1298    }
1299    if (!window.confirm('Na pewno trwale usunąć konto? Tej operacji nie można cofnąć.')) {
1300      return;
1301    }
1302    showMsg(deleteMsg, 'Usuwanie konta…');
1303    try {
1304      const data = await customerDeleteAccount({
1305        confirm: 'USUN',
1306        password: user?.hasPassword === false ? '' : password
1307      });
1308      clearNewsletterSubscription();
1309      showPanel('auth');
1310      showAuthForm('login');
1311      showMsg(loginMsg, data.message || 'Konto zostało usunięte.');
1312    } catch (err) {
1313      showMsg(deleteMsg, err.message || 'Nie udało się usunąć konta.', true);
1314    }
1315  });
1316
1317  $('#account-addresses-form')?.addEventListener('submit', async (e) => {
1318    e.preventDefault();
1319    const form = e.target;
1320    const addresses = [0, 1, 2].map((i) => {
1321      const group = form.querySelector(`[data-addr-slot="${i}"]`);
1322      const parts = group ? readAddressPartsFromForm(group) : { city: '', street: '', number: '', apartment: '' };
1323      return {
1324        label: String(new FormData(form).get(`label${i}`) || ''),
1325        city: parts.city,
1326        street: parts.street,
1327        number: parts.number,
1328        apartment: parts.apartment,
1329        address: composeDeliveryAddress(parts)
1330      };
1331    }).filter((a) => a.city || a.street || a.number || a.address);
1332    try {
1333      await loyaltyAction('saveAddresses', { addresses });
1334      showMsg($('#account-addr-msg'), 'Adresy zapisane.');
1335      await refreshLoyalty();
1336    } catch (err) {
1337      showMsg($('#account-addr-msg'), err.message, true);
1338    }
1339  });
1340
1341  $('#wheel-spin')?.addEventListener('click', async () => {
1342    const btn = $('#wheel-spin');
1343    if (isWheelSpinning()) return;
1344    btn.disabled = true;
1345    showMsg($('#wheel-msg'), '');
1346    resetWheelCelebration();
1347    try {
1348      const data = await loyaltyAction('spinWheel');
1349      const prizes = loyaltyCache?.wheel?.prizes
1350        || $('#fortune-wheel-stage')?._wheelPrizes
1351        || [];
1352      const count = Math.max(prizes.length, 1);
1353      const idx = Number.isInteger(data.prizeIndex)
1354        ? data.prizeIndex
1355        : getWheelPrizeIndex(prizes, data.prize);
1356      await spinFortuneWheelToPrize(idx, count);
1357      const label = data.prize?.label || 'Wynik';
1358      const kind = data.reward?.kind;
1359      let extra = '';
1360      if (kind === 'points') extra = ` (+${data.reward.points} pkt)`;
1361      else if (kind === 'coupon' && data.reward.coupon?.code) {
1362        extra = ` · kod ${data.reward.coupon.code}`;
1363      } else if (kind === 'none') extra = ' — spróbuj za tydzień!';
1364      showMsg($('#wheel-msg'), `Wypadło: ${label}${extra}`);
1365      await refreshLoyalty();
1366    } catch (err) {
1367      showMsg($('#wheel-msg'), err.message, true);
1368    } finally {
1369      btn.disabled = false;
1370    }
1371  });
1372
1373  // referral-apply (wpisanie kodu po zalogowaniu) usunięte — kod tylko przy rejestracji.
1374
1375  logoutBtn?.addEventListener('click', async () => {
1376    await customerLogout();
1377    showPanel('auth');
1378    showAuthForm('login');
1379  });
1380
1381  const verifiedFromLink = await handleVerifyFromUrl();
1382  if (verifiedFromLink) return;
1383
1384  const resetToken = handleResetFromUrl();
1385  if (resetToken) return;
1386
1387  if (isCustomerLoggedIn()) {
1388    try {
1389      const user = await customerMe();
1390      await enterLoggedIn(user || getCustomerUser());
1391    } catch (err) {
1392      if (err?.status === 401) clearCustomerSession();
1393      showPanel('auth');
1394      showAuthForm('login');
1395      if (err?.status === 503) showMsg(loginMsg, err.message, true);
1396    }
1397  } else {
1398    showPanel('auth');
1399    let authTab = 'login';
1400    try {
1401      const tab = new URLSearchParams(window.location.search).get('tab');
1402      if (tab === 'register' || tab === 'login' || tab === 'forgot') authTab = tab;
1403    } catch {
1404      /* ignore */
1405    }
1406    showAuthForm(authTab);
1407  }
1408}
1409
1410boot();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.