مجموعه‌ای از شیشه‌های عطر

رایحه‌های منتخب، با شخصیت ماندگار

رایحه‌ای که با شما می‌ماند

در Khademi Perfume، هر رایحه با جزئیات روشن، انتخاب آرام و تجربه‌ای شخصی ارائه می‌شود.

ارسال سریع به سراسر کشور اصالت تضمین‌شده ۷ روز ضمانت بازگشت

راهنمای انتخاب

رایحه مناسب خودتان را پیدا کنید

کلکسیون فروشگاه

محصولات منتخب

در حال همگام‌سازی…

بسته‌بندی با دقت

هر سفارش با جزئیات ثبت و برای آماده‌سازی پیگیری می‌شود.

اطلاعات شفاف رایحه

نت‌ها، حجم، موجودی و وضعیت هر محصول در دسترس است.

سفارش قابل پیگیری

بعد از ثبت، وضعیت سفارش از حساب کاربری قابل مشاهده است.

درباره مجموعه

انتخاب عطر، بدون شلوغی و حدس

Khademi Perfume برای ساخت تجربه‌ای منظم و خوش‌فکر در انتخاب رایحه طراحی شده است؛ از اطلاعات شفاف محصول تا پیگیری دقیق سفارش.

۰رایحه
۰سفارش
۰مشتری
چیدمانی از شیشه‌های عطر

ارتباط با ما

سؤال یا درخواستی دارید؟

پیام شما ثبت می‌شود و تیم پشتیبانی در اولین فرصت پاسخ می‌دهد.

support@khademi-perfume.example

۰۲۱-۰۰۰۰۰۰۰۰

تهران، خیابان ولیعصر

مدیریت فروشگاه

داشبورد

روند درآمد ثبت‌شده

بر پایه سفارش‌های ثبت‌شده

نیازمند اقدام

آخرین سفارش‌ها

آخرین رویدادها

`); w.document.close(); w.focus(); setTimeout(() => w.print(), 300); } /* ============================================================ ADMIN — INVENTORY ============================================================ */ function renderInventory(){ $("inventory-body").innerHTML = rows("product").map(p => { const low = Number(p.stock) <= Number(p.min_stock || 0); return ` ${esc(p.product_name)} ${fnum(p.stock)} ${fnum(p.min_stock)} ${low ? "نیازمند تأمین" : "مناسب"} `; }).join("") || `محصولی ثبت نشده است.`; } function renderInventoryLog(){ const logs = [...rows("inventory")].sort((a,b) => new Date(b.created_at) - new Date(a.created_at)).slice(0, 30); $("inventory-log-body").innerHTML = logs.length ? logs.map(l => { const p = productById(l.inventory_product_id); const ch = Number(l.inventory_change || 0); return ` ${esc(p?.product_name || "—")} ${ch >= 0 ? "+" : ""}${fnum(ch)} ${esc(l.inventory_reason || "—")} ${esc(l.activity_user || state.user?.email || "—")} ${formatDateTime(l.created_at)} `; }).join("") : `تغییری ثبت نشده است.`; } function openInventoryForm(){ if (!can("inventory")) { toast("فقط انباردار یا مدیر ارشد می‌تواند موجودی را تغییر دهد.", "error"); return; } $("simple-form-title").textContent = "ثبت تغییر موجودی"; $("simple-form-content").innerHTML = `
`; openModal("simple-form-modal"); } async function saveInventory(event){ event.preventDefault(); const product = productById($("inv-product").value); const change = Number($("inv-change").value); if (!product) return; if (Number(product.stock) + change < 0) { toast("موجودی نمی‌تواند منفی شود.", "error"); return; } const res = await window.dataSdk.update({ ...product, stock: Number(product.stock) + change, updated_at: now() }); if (res.isOk) { await window.dataSdk.create(blank("inventory", { inventory_product_id: product.__backendId, inventory_change: change, inventory_reason: $("inv-reason").value.trim(), activity_user: state.user?.email || "" })); closeModal("simple-form-modal"); toast("موجودی به‌روز شد.", "success"); } else { toast("ثبت تغییر موجودی ناموفق بود.", "error"); } } /* ============================================================ ADMIN — CUSTOMERS ============================================================ */ function renderCustomers(){ $("customers-body").innerHTML = rows("user").map(u => { const orders = rows("order").filter(o => o.customer_email === u.email); const total = orders.reduce((s, o) => s + Number(o.order_total || 0), 0); return ` ${esc(u.name)} ${esc(u.email)} ${esc(u.role)} ${fnum(orders.length)} ${money(total)} ${u.is_active !== false ? "فعال" : "غیرفعال"} `; }).join("") || `کاربری ثبت نشده است.`; } /* ============================================================ ADMIN — CATEGORIES ============================================================ */ function renderCategories(){ $("category-list").innerHTML = rows("category").map(c => `
${esc(c.category_name)}

ترتیب: ${fnum(c.category_order)} · ${c.category_active !== false ? "فعال" : "غیرفعال"}

`).join("") || `

دسته‌بندی اختصاصی ثبت نشده؛ دسته‌های پیش‌فرض در فرم محصول قابل استفاده‌اند.

`; } function openCategoryForm(){ $("simple-form-title").textContent = "دسته‌بندی جدید"; $("simple-form-content").innerHTML = `
`; openModal("simple-form-modal"); } async function saveCategory(event){ event.preventDefault(); const res = await window.dataSdk.create(blank("category", { category_name: $("new-category").value.trim(), category_order: rows("category").length + 1, category_active: true })); if (res.isOk) { closeModal("simple-form-modal"); toast("دسته‌بندی ذخیره شد.", "success"); } else toast("خطا در ذخیره دسته‌بندی.", "error"); } async function toggleCategory(id){ const c = byId(id); const res = await window.dataSdk.update({ ...c, category_active: c.category_active === false, updated_at: now() }); toast(res.isOk ? "وضعیت دسته تغییر کرد." : "خطا.", res.isOk ? "success" : "error"); } /* ============================================================ ADMIN — DISCOUNTS ============================================================ */ function renderDiscounts(){ $("discount-list").innerHTML = rows("discount").map(d => `
${esc(d.discount_code)} ${d.is_active !== false ? "فعال" : "غیرفعال"}

${d.discount_type === "درصدی" ? fnum(d.discount_value) + "٪" : money(d.discount_value)} · حداقل خرید ${money(d.discount_min_purchase || 0)} ${Number(d.discount_max_amount || 0) > 0 ? `· سقف ${money(d.discount_max_amount)}` : ""}

استفاده‌شده: ${fnum(d.discount_used)} ${Number(d.discount_usage_limit || 0) > 0 ? `/ ${fnum(d.discount_usage_limit)}` : ""}

`).join("") || `

کد تخفیفی تعریف نشده است.

`; } function openDiscountForm(){ $("simple-form-title").textContent = "کد تخفیف جدید"; $("simple-form-content").innerHTML = `
`; openModal("simple-form-modal"); } async function saveDiscount(event){ event.preventDefault(); const res = await window.dataSdk.create(blank("discount", { discount_code: $("disc-code").value.trim().toUpperCase(), discount_type: $("disc-type").value, discount_value: Number($("disc-value").value), discount_min_purchase: Number($("disc-min").value || 0), discount_max_amount: Number($("disc-max").value || 0), discount_usage_limit: Number($("disc-limit").value || 0), discount_start: $("disc-start").value, discount_end: $("disc-end").value, discount_used: 0, is_active: true })); if (res.isOk) { closeModal("simple-form-modal"); toast("کد تخفیف ذخیره شد.", "success"); } else toast("ذخیره کد تخفیف ناموفق بود.", "error"); } async function toggleDiscount(id){ const d = byId(id); const res = await window.dataSdk.update({ ...d, is_active: d.is_active === false, updated_at: now() }); toast(res.isOk ? "وضعیت کد تغییر کرد." : "خطا.", res.isOk ? "success" : "error"); } /* ============================================================ ADMIN — REVIEWS ============================================================ */ function renderReviewsAdmin(){ const status = $("review-status-filter")?.value || ""; const list = rows("review") .filter(r => !status || r.review_status === status) .sort((a,b) => new Date(b.created_at) - new Date(a.created_at)); $("reviews-admin-body").innerHTML = list.length ? list.map(r => { const p = productById(r.review_product_id); return ` ${esc(p?.product_name || "—")} ${esc(r.review_user_name || r.review_user_email || "—")} ${starHtml(r.review_rating)} ${esc(r.review_text)} ${esc(r.review_status)} ${r.review_status !== "تأییدشده" ? `` : ""} ${r.review_status !== "ردشده" ? `` : ""} `; }).join("") : `نظری یافت نشد.`; } async function setReviewStatus(id, status){ const r = byId(id); if (!r) return; const res = await window.dataSdk.update({ ...r, review_status: status, updated_at: now() }); if (res.isOk) { await recalcProductRating(r.review_product_id); toast(`نظر ${status}.`, "success"); } else toast("خطا در تغییر وضعیت نظر.", "error"); } async function recalcProductRating(productId){ const approved = rows("review").filter(r => r.review_product_id === productId && r.review_status === "تأییدشده"); const count = approved.length; const avg = count ? approved.reduce((s, r) => s + Number(r.review_rating || 0), 0) / count : 0; const product = productById(productId); if (!product) return; await window.dataSdk.update({ ...product, rating: Number(avg.toFixed(1)), review_count: count, updated_at: now() }); } function openReviewReply(id){ const r = byId(id); $("simple-form-title").textContent = "پاسخ به نظر"; $("simple-form-content").innerHTML = `
${esc(r.review_user_name || "کاربر")}

${esc(r.review_text)}

`; openModal("simple-form-modal"); } async function saveReviewReply(event, id){ event.preventDefault(); const r = byId(id); const res = await window.dataSdk.update({ ...r, admin_reply: $("admin-reply").value.trim(), updated_at: now() }); if (res.isOk) { closeModal("simple-form-modal"); toast("پاسخ ذخیره شد.", "success"); } else toast("ذخیره پاسخ ناموفق بود.", "error"); } /* ============================================================ ADMIN — REPORTS ============================================================ */ function renderReports(){ const products = [...rows("product")].sort((a,b) => Number(b.sales_count || 0) - Number(a.sales_count || 0)).slice(0, 5); const users = rows("user") .map(u => ({ user:u, total: rows("order").filter(o => o.customer_email === u.email).reduce((s, o) => s + Number(o.order_total || 0), 0) })) .sort((a,b) => b.total - a.total) .slice(0, 5); const payments = {}; rows("order").forEach(o => { payments[o.payment_method || "ثبت نشده"] = (payments[o.payment_method || "ثبت نشده"] || 0) + 1; }); const statuses = {}; rows("order").forEach(o => { statuses[o.order_status || "—"] = (statuses[o.order_status || "—"] || 0) + 1; }); // Category revenue const catRev = {}; rows("order").forEach(o => { let items = []; try { items = JSON.parse(o.order_items || "[]"); } catch {} items.forEach(it => { const p = productById(it.id); const cat = p?.category || "نامشخص"; catRev[cat] = (catRev[cat] || 0) + Number(it.price) * Number(it.qty); }); }); const totalRev = rows("order").filter(o => o.order_status !== "لغوشده").reduce((s, o) => s + Number(o.order_total || 0), 0); const avgOrder = rows("order").length ? totalRev / rows("order").length : 0; $("top-products").innerHTML = products.length ? products.map(p => `
  • ${esc(p.product_name)}${fnum(p.sales_count)} فروش
  • `).join("") : "
  • داده‌ای نیست.
  • "; $("top-customers").innerHTML = users.length ? users.map(u => `
  • ${esc(u.user.name)}${money(u.total)}
  • `).join("") : "
  • داده‌ای نیست.
  • "; $("payment-report").innerHTML = Object.keys(payments).length ? Object.entries(payments).map(([k, v]) => `

    ${esc(k)}${fnum(v)}

    `).join("") : "

    داده‌ای نیست.

    "; $("status-report").innerHTML = Object.keys(statuses).length ? Object.entries(statuses).map(([k, v]) => `

    ${esc(k)}${fnum(v)}

    `).join("") : "

    داده‌ای نیست.

    "; $("category-report").innerHTML = Object.keys(catRev).length ? Object.entries(catRev).sort((a,b) => b[1] - a[1]).map(([k, v]) => `

    ${esc(k)}${money(v)}

    `).join("") : "

    داده‌ای نیست.

    "; $("finance-report").innerHTML = `

    درآمد کل${money(totalRev)}

    تعداد سفارش${fnum(rows("order").length)}

    میانگین سفارش${money(Math.round(avgOrder))}

    محصولات فعال${fnum(rows("product").filter(p => p.published !== false).length)}

    `; } /* ============================================================ ADMIN — ACTIVITY ============================================================ */ function renderActivity(){ const list = [...rows("activity")].sort((a,b) => new Date(b.created_at) - new Date(a.created_at)).slice(0, 100); $("activity-body").innerHTML = list.length ? list.map(a => ` ${esc(a.activity_type)} ${esc(a.activity_detail)} ${esc(a.activity_user || "—")} ${formatDateTime(a.created_at)} `).join("") : `رویدادی ثبت نشده است.`; } /* ============================================================ ADMIN — USERS ============================================================ */ function renderUsers(){ $("users-body").innerHTML = rows("user").map(u => ` ${esc(u.name)} ${esc(u.email)} ${esc(u.role)} ${u.last_login ? formatDateTime(u.last_login) : "—"} ${u.is_active !== false ? "فعال" : "غیرفعال"} ${u.__backendId === state.user?.__backendId ? "—" : ` `} `).join("") || `کاربری ثبت نشده است.`; } function openUserForm(){ if (!superAdmin()) return; $("simple-form-title").textContent = "ایجاد کاربر مجاز"; $("simple-form-content").innerHTML = `
    `; openModal("simple-form-modal"); } async function saveUser(event){ event.preventDefault(); const email = $("new-user-email").value.trim().toLowerCase(); if (rows("user").some(u => u.email === email)) { toast("این ایمیل قبلاً ثبت شده است.", "error"); return; } const res = await window.dataSdk.create(blank("user", { name: $("new-user-name").value.trim(), email, role: $("new-user-role").value, access_code: $("new-user-code").value, is_active: true })); if (res.isOk) { closeModal("simple-form-modal"); toast("کاربر مجاز ایجاد شد.", "success"); } else toast("ایجاد کاربر ناموفق بود.", "error"); } async function toggleUser(id){ const u = byId(id); const res = await window.dataSdk.update({ ...u, is_active: u.is_active === false, updated_at: now() }); toast(res.isOk ? "وضعیت کاربر تغییر کرد." : "خطا.", res.isOk ? "success" : "error"); } function changeUserRole(id){ const u = byId(id); $("simple-form-title").textContent = "تغییر نقش " + u.name; $("simple-form-content").innerHTML = `
    `; openModal("simple-form-modal"); } async function saveUserRole(event, id){ event.preventDefault(); const u = byId(id); const res = await window.dataSdk.update({ ...u, role: $("role-select").value, updated_at: now() }); if (res.isOk) { closeModal("simple-form-modal"); toast("نقش تغییر کرد.", "success"); } else toast("تغییر نقش ناموفق بود.", "error"); } /* ============================================================ ADMIN — SETTINGS ============================================================ */ async function saveSettings(event){ event.preventDefault(); state.settings.storeName = $("setting-store-name").value.trim(); state.settings.shipping = Number($("setting-shipping").value); state.settings.freeShipping = Number($("setting-free-shipping").value); state.settings.phone = $("setting-phone").value.trim(); state.settings.email = $("setting-email").value.trim(); state.settings.notice = $("setting-notice").value; const values = { store_name: state.settings.storeName, shipping: state.settings.shipping, free_shipping: state.settings.freeShipping, phone: state.settings.phone, email: state.settings.email, notice: state.settings.notice }; for (const [key, value] of Object.entries(values)) { const old = rows("setting").find(s => s.setting_key === key); const record = old ? { ...old, setting_value: String(value), updated_at: now() } : blank("setting", { setting_key: key, setting_value: String(value) }); await window.dataSdk[old ? "update" : "create"](record); } toast("تنظیمات ذخیره شد.", "success"); } /* ============================================================ SHARED ADMIN HELPERS ============================================================ */ async function logActivity(type, detail){ if (!state.user) return; try { await window.dataSdk.create(blank("activity", { activity_type: type, activity_detail: detail, activity_user: state.user.email })); } catch {} } async function deleteRow(id, label){ const row = byId(id); if (!row) return; const res = await window.dataSdk.delete(row); toast(res.isOk ? label + " حذف شد." : "حذف ناموفق بود.", res.isOk ? "success" : "error"); } function askDeleteRow(id, label){ $("confirm-copy").textContent = `${label} حذف خواهد شد و قابل بازگشت نیست.`; $("confirm-action").onclick = async () => { await deleteRow(id, label); closeModal("confirm-modal"); }; openModal("confirm-modal"); } function exportCurrentCSV(){ let data = []; if (state.currentTab === "products") data = rows("product"); else if (state.currentTab === "orders") data = rows("order"); else if (state.currentTab === "customers" || state.currentTab === "users") data = rows("user"); else if (state.currentTab === "inventory") data = rows("inventory"); else if (state.currentTab === "discounts") data = rows("discount"); else if (state.currentTab === "reviews-admin") data = rows("review"); else if (state.currentTab === "activity") data = rows("activity"); else data = state.rows; if (!data.length) { toast("داده‌ای برای خروجی وجود ندارد.", "info"); return; } const keys = [...new Set(data.flatMap(Object.keys))].filter(k => k !== "__backendId"); const csv = [keys.join(","), ...data.map(row => keys.map(k => `"${String(row[k] ?? "").replace(/"/g,'""')}"`).join(","))].join("\n"); const blob = new Blob(["\uFEFF" + csv], { type:"text/csv;charset=utf-8" }); const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = `khademi-perfume-${state.currentTab}-${Date.now()}.csv`; link.click(); URL.revokeObjectURL(link.href); toast("خروجی CSV آماده شد.", "success"); } /* ============================================================ CONTACT / NEWSLETTER ============================================================ */ async function submitContact(event){ event.preventDefault(); const name = $("contact-name").value.trim(); const email = $("contact-email").value.trim(); const message = $("contact-message").value.trim(); await window.dataSdk.create(blank("activity", { activity_type: "پیام تماس", activity_detail: `${name} <${email}> — ${message}`, activity_user: email })); $("contact-name").value = ""; $("contact-email").value = ""; $("contact-message").value = ""; toast("پیام شما ثبت شد. به‌زودی پاسخ می‌دهیم.", "success"); } async function subscribeNewsletter(event){ event.preventDefault(); const input = event.target.querySelector("input"); const email = input.value.trim(); if (!email) return; await window.dataSdk.create(blank("activity", { activity_type: "عضویت خبرنامه", activity_detail: email, activity_user: email })); input.value = ""; toast("عضویت شما در خبرنامه ثبت شد.", "success"); } /* ============================================================ DATA SDK HANDLER ============================================================ */ const handler = { onDataChanged(data){ state.rows = Array.isArray(data) ? data : []; // settings rows("setting").forEach(s => { if (s.setting_key === "shipping") state.settings.shipping = Number(s.setting_value); if (s.setting_key === "free_shipping") state.settings.freeShipping = Number(s.setting_value); if (s.setting_key === "store_name") state.settings.storeName = s.setting_value; if (s.setting_key === "phone") state.settings.phone = s.setting_value; if (s.setting_key === "email") state.settings.email = s.setting_value; if (s.setting_key === "notice") state.settings.notice = s.setting_value; }); // rehydrate rehydrateUser(); rehydrateWishlist(); rehydrateCart(); // clear cart items whose products were deleted state.cart = state.cart.filter(i => productById(i.id)); const status = $("sync-status"); if (status) status.textContent = "همگام‌سازی انجام شد"; // re-render UI renderProducts(); renderFeatured(); updateCart(); renderWishlistCount(); refreshAccount(); renderStats(); if ($("admin-view").classList.contains("active")) renderAdmin(); } }; /* ============================================================ INIT ============================================================ */ async function init(){ $("footer-year").textContent = new Date().toLocaleDateString("fa-IR", { year:"numeric" }); // close suggestions on outside click document.addEventListener("click", e => { const box = $("search-suggest"); if (!box) return; if (!box.contains(e.target) && e.target.id !== "global-search") box.classList.add("hidden"); }); // ESC closes overlays document.addEventListener("keydown", e => { if (e.key === "Escape") closeAllOverlays(); }); $("mobile-menu").addEventListener("click", toggleMobileMenu); try { const result = await window.dataSdk.init(handler); $("sync-status").textContent = result.isOk ? "همگام‌سازی انجام شد" : "اتصال به داده‌ها برقرار نشد"; if (!result.isOk) toast("داده‌های فروشگاه بارگذاری نشدند.", "error"); state.ready = true; } catch (err) { console.error(err); $("sync-status").textContent = "خطا در اتصال"; toast("خطا در اتصال به داده‌ها.", "error"); } renderProducts(); renderFeatured(); updateCart(); renderWishlistCount(); refreshAccount(); renderStats(); if (window.lucide) lucide.createIcons(); } init();