import { hydrateCart, subscribe, getSnapshot, updateQuantity, removeItem, clearCart, formatPrice, } from "/js/cart.js"; import { IRAN_PROVINCES } from "/js/iran-locations.js?v=loc1"; import { generateInvoicePdf, downloadFile, openWhatsAppWithInvoice, whatsappChatUrl, } from "/js/invoice-pdf.js?v=eurInv1"; function escapeHtml(s) { return String(s) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """); } function generateOrderId() { const d = new Date(); const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); const n = String(Math.floor(1000 + Math.random() * 9000)); return `KIKO-${y}${m}${day}-${n}`; } function ordersEndpoint() { return ( document.body.dataset.ordersEndpoint || window.__KIKOMIKO_ORDERS_URL__ || "" ).trim(); } function whatsappNumber() { return ( document.body.dataset.whatsapp || window.__KIKOMIKO_WHATSAPP__ || "393520566607" ); } function eurToTomanRate() { const raw = Number(document.body.dataset.eurRate || ""); return Number.isFinite(raw) && raw > 0 ? raw : 0; } function googlePlacesKey() { return ( document.body.dataset.googlePlacesKey || window.__GOOGLE_PLACES_API_KEY__ || "" ).trim(); } function normalizeFa(s) { return String(s || "") .trim() .replace(/\u200c/g, "") .replace(/ي/g, "ی") .replace(/ك/g, "ک") .replace(/\s+/g, " "); } function findProvince(name) { const n = normalizeFa(name); if (!n) return null; return ( IRAN_PROVINCES.find((p) => normalizeFa(p.name) === n) || IRAN_PROVINCES.find((p) => normalizeFa(p.name).includes(n) || n.includes(normalizeFa(p.name))) || null ); } function findCityInProvince(province, cityName) { if (!province) return null; const n = normalizeFa(cityName); if (!n) return null; return ( province.cities.find((c) => normalizeFa(c) === n) || province.cities.find((c) => normalizeFa(c).includes(n) || n.includes(normalizeFa(c))) || null ); } function receiptItemsHtml(items) { const rows = items .map((i) => { const lineTotal = formatPrice(i.price * i.quantity); const productType = i.category || "—"; const img = i.image ? `` : ``; return ` ${escapeHtml(productType)}
${escapeHtml(i.name)}
${escapeHtml(String(i.quantity))} ${escapeHtml(lineTotal)} ${img} `; }) .join(""); return `
${rows}
نوع محصول عدد قیمت عکس
`; } function fillReceipt(el, data) { if (!el) return; const set = (sel, text) => { const node = el.querySelector(sel); if (node) node.textContent = text; }; set("[data-receipt-id]", data.orderId); set("[data-receipt-date]", data.dateLabel); set("[data-receipt-name]", data.customerName); set("[data-receipt-total]", data.totalLabel); const itemsHost = el.querySelector("[data-receipt-items]"); if (itemsHost) { if (data.itemsHtml) itemsHost.innerHTML = data.itemsHtml; else itemsHost.textContent = data.itemsText || ""; } el.hidden = false; } /** * Province → city cascading selects (Iran). Customers choose; no free text. */ function initLocationSelects(form) { const provinceSelect = form.querySelector("[data-province-select]"); const citySelect = form.querySelector("[data-city-select]"); if (!(provinceSelect instanceof HTMLSelectElement) || !(citySelect instanceof HTMLSelectElement)) { return { setProvinceCity: () => {} }; } for (const p of IRAN_PROVINCES) { const opt = document.createElement("option"); opt.value = p.name; opt.textContent = p.name; provinceSelect.appendChild(opt); } function fillCities(provinceName, preferredCity) { const province = findProvince(provinceName); citySelect.innerHTML = ""; if (!province) { citySelect.disabled = true; const opt = document.createElement("option"); opt.value = ""; opt.textContent = "ابتدا استان را انتخاب کنید"; citySelect.appendChild(opt); return; } citySelect.disabled = false; const placeholder = document.createElement("option"); placeholder.value = ""; placeholder.textContent = "انتخاب شهر"; citySelect.appendChild(placeholder); for (const city of province.cities) { const opt = document.createElement("option"); opt.value = city; opt.textContent = city; citySelect.appendChild(opt); } if (preferredCity) { const matched = findCityInProvince(province, preferredCity); if (matched) { citySelect.value = matched; } else { const extra = document.createElement("option"); extra.value = preferredCity; extra.textContent = preferredCity; citySelect.appendChild(extra); citySelect.value = preferredCity; } } } provinceSelect.addEventListener("change", () => { fillCities(provinceSelect.value, ""); }); function setProvinceCity(provinceName, cityName) { const province = findProvince(provinceName); if (!province) { provinceSelect.value = ""; fillCities("", ""); return; } provinceSelect.value = province.name; fillCities(province.name, cityName || ""); } return { setProvinceCity, provinceSelect, citySelect }; } function loadGoogleMapsPlaces(apiKey) { if (window.google?.maps?.places) return Promise.resolve(); if (window.__kikoGoogleMapsPromise) return window.__kikoGoogleMapsPromise; window.__kikoGoogleMapsPromise = new Promise((resolve, reject) => { const cb = `__kikoPlacesReady_${Date.now()}`; window[cb] = () => { delete window[cb]; resolve(); }; const script = document.createElement("script"); script.src = `https://maps.googleapis.com/maps/api/js?key=${encodeURIComponent(apiKey)}&libraries=places&language=fa®ion=IR&callback=${cb}`; script.async = true; script.onerror = () => reject(new Error("Google Maps failed to load")); document.head.appendChild(script); }); return window.__kikoGoogleMapsPromise; } /** * Google Places Autocomplete on address — fills استان/شهر when a place is chosen. */ async function initGoogleAddressAutocomplete(form, setProvinceCity) { const key = googlePlacesKey(); if (!key) return; const addressEl = form.querySelector('textarea[name="address"], input[name="address"]'); if (!(addressEl instanceof HTMLTextAreaElement) && !(addressEl instanceof HTMLInputElement)) { return; } try { await loadGoogleMapsPlaces(key); } catch { return; } // Places Autocomplete needs an ; mirror textarea value via a search input above it. let input = addressEl; if (addressEl instanceof HTMLTextAreaElement) { const wrap = document.createElement("label"); wrap.className = "field"; wrap.innerHTML = `جستجوی آدرس (گوگل)`; input = document.createElement("input"); input.type = "text"; input.setAttribute("autocomplete", "off"); input.setAttribute("placeholder", "شهر، محله یا خیابان را جستجو کنید…"); input.setAttribute("aria-label", "جستجوی آدرس با گوگل"); wrap.appendChild(input); addressEl.parentElement?.insertBefore(wrap, addressEl); } const autocomplete = new google.maps.places.Autocomplete(input, { componentRestrictions: { country: "ir" }, fields: ["address_components", "formatted_address", "name"], types: ["geocode"], }); autocomplete.addListener("place_changed", () => { const place = autocomplete.getPlace(); if (!place) return; let province = ""; let city = ""; for (const c of place.address_components || []) { const types = c.types || []; if (types.includes("administrative_area_level_1")) { province = c.long_name; } if ( types.includes("locality") || types.includes("administrative_area_level_2") ) { if (!city) city = c.long_name; } } if (province || city) setProvinceCity(province, city); const formatted = place.formatted_address || place.name || ""; if (formatted && addressEl instanceof HTMLTextAreaElement) { if (!addressEl.value.trim()) addressEl.value = formatted; } else if (formatted && addressEl instanceof HTMLInputElement && addressEl === input) { addressEl.value = formatted; } }); } function validateCheckoutFields(data) { const firstName = String(data.get("firstName") || "").trim(); const lastName = String(data.get("lastName") || "").trim(); const phone = String(data.get("phone") || "").trim(); const address = String(data.get("address") || "").trim(); const city = String(data.get("city") || "").trim(); const province = String(data.get("province") || "").trim(); if (!firstName) return "نام الزامی است."; if (!lastName) return "نام خانوادگی الزامی است."; if (!phone) return "شماره تلفن الزامی است."; if (phone.replace(/\D/g, "").length < 10) return "شماره تلفن معتبر وارد کنید."; if (!address) return "آدرس الزامی است."; if (!province) return "استان را از لیست انتخاب کنید."; if (!city) return "شهر را از لیست انتخاب کنید."; return ""; } /** Static checkout: Sheets storage + WhatsApp (no site backend). */ export function initCheckout() { const root = document.querySelector("[data-checkout]"); if (!root) return; hydrateCart(); const list = root.querySelector("[data-cart-lines]"); const totalEl = root.querySelector("[data-cart-total]"); const form = root.querySelector("form"); const errorEl = root.querySelector("[data-checkout-error]"); const successEl = root.querySelector("[data-checkout-success]"); const submitBtn = root.querySelector("[type=submit]"); const receiptEl = root.querySelector("[data-order-receipt]"); const downloadPdfBtn = root.querySelector("[data-receipt-pdf]"); const openWaBtn = root.querySelector("[data-receipt-whatsapp]"); const phone = whatsappNumber(); /** @type {{ file: File|null; waText: string; waUrl: string; pdfUrl?: string } | null} */ let lastInvoice = null; downloadPdfBtn?.addEventListener("click", () => { if (lastInvoice?.file) downloadFile(lastInvoice.file); else if (lastInvoice?.pdfUrl) window.open(lastInvoice.pdfUrl, "_blank", "noopener"); }); openWaBtn?.addEventListener("click", async () => { if (lastInvoice?.file) { try { const result = await openWhatsAppWithInvoice( lastInvoice.file, lastInvoice.waText, phone ); lastInvoice.pdfUrl = result.pdfUrl; lastInvoice.waUrl = result.waUrl; return; } catch { /* fall through */ } } if (lastInvoice?.waUrl) { window.location.href = lastInvoice.waUrl; return; } window.location.href = whatsappChatUrl(phone, ""); }); let setProvinceCity = () => {}; if (form instanceof HTMLFormElement) { const loc = initLocationSelects(form); setProvinceCity = loc.setProvinceCity; initGoogleAddressAutocomplete(form, setProvinceCity); } function render(snap) { if (!list || !totalEl || !submitBtn) return; if (!snap.items.length) { list.innerHTML = `

سبد خرید خالی است.

`; totalEl.textContent = formatPrice(0); submitBtn.setAttribute("disabled", ""); return; } submitBtn.removeAttribute("disabled"); list.innerHTML = snap.items .map( (item) => `
${ item.image ? `` : `
` }
${escapeHtml(item.name)}
${formatPrice(item.price)}
${item.quantity}
` ) .join(""); totalEl.textContent = formatPrice(snap.subtotal); list.querySelectorAll(".cart-line").forEach((row) => { const id = row.getAttribute("data-id"); const item = snap.items.find((i) => i.productId === id); if (!item || !id) return; row.querySelector("[data-qty-dec]")?.addEventListener("click", () => updateQuantity(id, item.quantity - 1) ); row.querySelector("[data-qty-inc]")?.addEventListener("click", () => updateQuantity(id, item.quantity + 1) ); row.querySelector("[data-qty-remove]")?.addEventListener("click", () => removeItem(id) ); }); } subscribe(render); render(getSnapshot()); form?.addEventListener("submit", async (e) => { e.preventDefault(); if (!(form instanceof HTMLFormElement)) return; const snap = getSnapshot(); if (!snap.items.length) return; errorEl && (errorEl.hidden = true); successEl && (successEl.hidden = true); const data = new FormData(form); const validationError = validateCheckoutFields(data); if (validationError) { if (errorEl) { errorEl.hidden = false; errorEl.textContent = validationError; } return; } submitBtn?.setAttribute("disabled", ""); if (submitBtn) submitBtn.textContent = "در حال ثبت…"; const customerName = `${String(data.get("firstName") || "").trim()} ${String(data.get("lastName") || "").trim()}`.trim(); const customerPhone = String(data.get("phone") || "").trim(); const email = String(data.get("email") || "").trim(); const address = String(data.get("address") || "").trim(); const city = String(data.get("city") || "").trim(); const province = String(data.get("province") || "").trim(); const notes = String(data.get("notes") || "").trim(); const orderId = generateOrderId(); const dateIso = new Date().toISOString(); const dateLabel = new Date().toLocaleString("fa-IR"); const productsLines = snap.items.map( (i) => `- ${i.name} | ${i.category || "—"} | ×${i.quantity} = ${formatPrice(i.price * i.quantity)}` ); const itemsText = productsLines.join("\n"); const itemsHtml = receiptItemsHtml(snap.items); const totalLabel = formatPrice(snap.subtotal); const endpoint = ordersEndpoint(); if (endpoint) { try { const res = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "text/plain;charset=utf-8" }, body: JSON.stringify({ orderId, date: dateIso, customerName, phone: customerPhone, items: itemsText, total: totalLabel, status: "Pending", }), redirect: "follow", }); const json = await res.json().catch(() => ({})); if (!res.ok || json.ok === false) { throw new Error(json.error || "ثبت سفارش در سیستم ناموفق بود"); } } catch (err) { if (errorEl) { errorEl.hidden = false; errorEl.textContent = err instanceof Error ? err.message : "خطا در ثبت سفارش"; } if (submitBtn) { submitBtn.textContent = "ثبت سفارش از طریق واتساپ"; submitBtn.removeAttribute("disabled"); } return; } } fillReceipt(receiptEl, { orderId, dateLabel, customerName, itemsHtml, totalLabel, }); if (successEl) { successEl.hidden = false; successEl.textContent = `سفارش ${orderId} ثبت شد. در حال ساخت فاکتور و باز کردن واتساپ…`; } const waLines = [ "Order Number:", orderId, "", "Customer Name:", customerName, "", "Products:", ...productsLines, "", "Total:", totalLabel, "", `Email: ${email}`, `Phone: ${customerPhone}`, `Address: ${address}`, `City: ${city}`, `Province: ${province}`, `Notes: ${notes}`, ]; const waText = waLines.join("\n"); const itemsForPdf = snap.items.map((i) => ({ ...i })); try { const pdfFile = await generateInvoicePdf({ orderId, dateLabel, customerName, customerPhone, address, city, province, items: itemsForPdf, totalLabel, formatPrice, eurToTomanRate: eurToTomanRate(), }); clearCart(); form.reset(); setProvinceCity("", ""); if (submitBtn) { submitBtn.textContent = "ثبت سفارش از طریق واتساپ"; submitBtn.setAttribute("disabled", ""); } if (successEl) { successEl.textContent = "فاکتور آماده شد — واتساپ باز می‌شود. فقط دکمه ارسال را بزنید."; } const result = await openWhatsAppWithInvoice(pdfFile, waText, phone); lastInvoice = { file: pdfFile, waText, waUrl: result.waUrl, pdfUrl: result.pdfUrl, }; } catch (err) { clearCart(); form.reset(); setProvinceCity("", ""); if (submitBtn) { submitBtn.textContent = "ثبت سفارش از طریق واتساپ"; submitBtn.setAttribute("disabled", ""); } const url = whatsappChatUrl(phone, waText); lastInvoice = { file: null, waText, waUrl: url }; if (successEl) { successEl.textContent = err instanceof Error ? `ثبت شد، اما فاکتور آماده نشد: ${err.message}. واتساپ با متن سفارش باز می‌شود.` : "ثبت شد. واتساپ با متن سفارش باز می‌شود."; } window.setTimeout(() => { window.location.href = url; }, 500); } }); }