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
? `

`
: `
`
}
`
)
.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);
}
});
}