/* Bonyan ERP — Customers (مستقل أو تابع لشركة) + كشف حساب موظف/شركة + عملات */ function CustomersScreen() { const I = window.BnyIcon; const D = window.BnyData; const cur = window.BnyCur; const { Card, Button, Money, Badge, Tabs, Avatar, Input } = window.BonyanDesignSystem_ad3ca0; const isMobile = window.useBnyMobile(); window.useBnyInvoices(); // إعادة الرسم عند تعديل/حذف فاتورة من أي مكان // أول عميل حقيقي (غير المستطرق) — أو العميل المطلوب من المساعد الصوتي const [sel, setSel] = React.useState(() => { const pid = window.BnyPendingCustomerId; if (pid != null) { window.BnyPendingCustomerId = null; const c = D.customers.find((x) => x.id === pid); if (c) return c; } return D.customers.find((c) => !c.walkin) || null; }); const [tab, setTab] = React.useState("stmt"); const [curFilter, setCurFilter] = React.useState("all"); // all | iqd | usd // نطاق الكشف — يبدأ بـ«الشركة» إن جاء الطلب من قسم الحسابات (مؤسسة) const [scope, setScope] = React.useState(() => { if (window.BnyPendingCustomerScope === "org") { window.BnyPendingCustomerScope = null; return "org"; } return "employee"; }); const [search, setSearch] = React.useState(""); const [sort, setSort] = React.useState("debt-desc"); // debt-desc|debt-asc|date-desc|date-asc const [filterVal, setFilterVal] = React.useState(""); // org:ID | region:X | "" const [printCust, setPrintCust] = React.useState(null); // العميل قيد سحب/طباعة الكشف const [showNew, setShowNew] = React.useState(false); // نافذة عميل جديد const [showPay, setShowPay] = React.useState(false); // نافذة تسديد دين const [showEdit, setShowEdit] = React.useState(false); // نافذة تعديل بيانات العميل const [payInv, setPayInv] = React.useState(null); // فاتورة قيد التسديد المنفرد const [delPay, setDelPay] = React.useState(null); // دفعة قيد الحذف (تأكيد المدير) const [printMode, setPrintMode] = React.useState("stmt"); // stmt | inv | pay const saveEdit = async (patch) => { await window.BnyOps.updateCustomerInfo(sel, patch); setShowEdit(false); forceRender(); }; const doPayInvoice = async (amount) => { await window.BnyOps.payInvoice(payInv, amount); setPayInv(null); forceRender(); }; const confirmDeletePay = async (creds) => { await window.BnyOps.deleteCustomerPayment(delPay, creds); // يرمي خطأً إن رفضت القاعدة التخويل setDelPay(null); forceRender(); }; const doPrint = () => { if (sel) { setPrintMode(tab); setPrintCust(sel); } }; // يطبع التبويب الحالي // إضافة عميل جديد (محلي + قاعدة) const addCustomer = async (data) => { const c = await window.BnyOps.addCustomer({ name: data.name.trim(), phone: (data.phone || "").trim(), phone2: (data.phone2 || "").trim(), address: (data.address || "").trim(), org: data.org || null, }); setShowNew(false); selectCust(c); forceRender(); }; const createOrg = (data) => { const o = { id: "org-" + Date.now(), name: data.name.trim(), phone: "", type: "شركة", notes: (data.notes || "").trim() }; window.BnyOps.addOrganization(o); return o; }; // تسديد دين العميل المحدَّد const payDebt = async (amount, curKey) => { await window.BnyOps.payCustomer(sel, amount, curKey); setShowPay(false); forceRender(); }; // إرسال كشف مختصر عبر واتساب const waStatement = () => { const msg = "كشف حساب — " + sel.name + "\nمن " + ((D.shop && D.shop.name) || "بُنيان") + "\n" + "المستحق بالدينار: " + (sel.balance || 0).toLocaleString("en-US") + " د.ع\n" + (sel.balanceUsd ? "المستحق بالدولار: " + sel.balanceUsd.toLocaleString("en-US") + " $\n" : "") + "شكرًا لتعاملكم معنا."; const url = sel.phone ? window.BnyWaLink(sel.phone, msg) : "https://wa.me/?text=" + encodeURIComponent(msg); try { window.open(url, "_blank"); } catch (e) {} }; React.useEffect(() => { if (!printCust) return; document.body.classList.add("bny-printing-stmt"); const after = () => { document.body.classList.remove("bny-printing-stmt"); setPrintCust(null); }; window.addEventListener("afterprint", after); const t = setTimeout(() => window.print(), 120); return () => { clearTimeout(t); window.removeEventListener("afterprint", after); document.body.classList.remove("bny-printing-stmt"); }; }, [printCust]); const [, forceRender] = React.useReducer((x) => x + 1, 0); const selectCust = (c) => { setSel(c); setScope("employee"); }; const toggleLimit = () => { sel.limitEnabled = !sel.limitEnabled; forceRender(); }; const setLimitVal = (key, raw) => { sel[key] = Number(raw) || 0; forceRender(); }; const s0 = sel || {}; // اشتقاقات آمنة حتى مع غياب عميل محدد (مشترك جديد) const org = s0.org ? D.organizations.find((o) => o.id === s0.org) : null; const orgScope = !!org && scope === "org"; const members = org ? D.customers.filter((c) => c.org === org.id) : (sel ? [sel] : []); const memberIds = members.map((m) => m.id); const custName = (id) => { const c = D.customers.find((x) => x.id === id); return c ? c.name : ""; }; const orgOf = (c) => (c.org ? D.organizations.find((o) => o.id === c.org) : null); const agg = (key) => members.reduce((s, m) => s + (m[key] || 0), 0); const bal = orgScope ? agg("balance") : (s0.balance || 0); const balUsd = orgScope ? agg("balanceUsd") : (s0.balanceUsd || 0); const lim = orgScope ? agg("limit") : (s0.limit || 0); const limUsd = orgScope ? agg("limitUsd") : (s0.limitUsd || 0); // الكشف مشتقّ من الفواتير + الدفعات (الأقدم أعلى، أحدث دفعة في الأسفل) const baseRows = !sel ? [] : orgScope ? members.reduce((all, m) => all.concat(window.BnyCustStatement(m).map((r) => ({ ...r, cust: m.id }))), []).sort((a, b) => String(a.iso || "").localeCompare(String(b.iso || ""))) : window.BnyCustStatement(sel); const stmtRows = baseRows.filter((s) => (curFilter === "all" || s.cur === curFilter) && (tab !== "pay" || s.credit > 0)); // فواتير بيع العميل (لتبويب «الفواتير» + التسديد المنفرد) — الأحدث أولًا const custInvoices = !sel ? [] : window.BnyCustSaleInvoices(sel) .filter((i) => curFilter === "all" || i.cur === curFilter) .slice().sort((a, b) => String(b.date || "").localeCompare(String(a.date || ""))); const CurFilter = () => (
); // منطقة العميل من العنوان (المقطع بعد "—"، أول جزء قبل "،") const regionOf = (c) => { const a = (c.address || "").split("—")[1] || (c.address || ""); return a.split("،")[0].trim(); }; const custSortOptions = [ { key: "debt-desc", label: "المديونية: الأعلى أولًا" }, { key: "debt-asc", label: "المديونية: الأقل أولًا" }, { key: "date-desc", label: "الأحدث مديونية" }, { key: "date-asc", label: "الأقدم مديونية" }, ]; const custFilterOptions = (() => { const opts = []; (D.organizations || []).forEach((o) => opts.push({ value: "org:" + o.id, label: "مؤسسة: " + o.name })); const regions = []; D.customers.filter((c) => !c.walkin).forEach((c) => { const r = regionOf(c); if (r && regions.indexOf(r) === -1) regions.push(r); }); regions.forEach((r) => opts.push({ value: "region:" + r, label: "منطقة: " + r })); return opts; })(); const toIqd = (c) => window.BnyToIqd(c.balance, c.balanceUsd); const custRows = D.customers.filter((c) => !c.walkin) .filter((c) => { const q = search.trim(); if (!q) return true; const o = orgOf(c); return c.name.includes(q) || (c.phone || "").includes(q) || (o && o.name.includes(q)); }) .filter((c) => { if (!filterVal) return true; if (filterVal.indexOf("org:") === 0) return c.org === filterVal.slice(4); if (filterVal.indexOf("region:") === 0) return regionOf(c) === filterVal.slice(7); return true; }) .slice() .sort((a, b) => { if (sort === "debt-desc") return toIqd(b) - toIqd(a); if (sort === "debt-asc") return toIqd(a) - toIqd(b); const da = window.BnyLastMovement(a.name, a.id, "cust") || "", db = window.BnyLastMovement(b.name, b.id, "cust") || ""; if (da === db) return 0; if (!da) return 1; if (!db) return -1; return sort === "date-desc" ? (db < da ? -1 : 1) : (da < db ? -1 : 1); }); // ---- كشف ديون العملاء الإجمالي ---- const iqdOf = (amt, c) => (c === "usd" ? (Number(amt) || 0) * (D.usdRate || 1500) : (Number(amt) || 0)); const lastPayOf = (c) => { const ps = (D.custPayments || []).filter((p) => p.cust === c.id); if (!ps.length) return ""; const last = ps.slice().sort((a, b) => String(b.iso || "").localeCompare(String(a.iso || "")))[0]; return last.disp || (last.iso ? window.BnyDate(last.iso) : ""); }; const custDebtRows = D.customers.filter((c) => !c.walkin).map((c) => { const invs = window.BnyCustSaleInvoices(c); const due = window.BnyCustDue(c); // المتبقّي = مجموع (إجمالي − مسدَّد) لكل فاتورة return { id: c.id, name: c.name, total: invs.reduce((s, i) => s + iqdOf(i.total, i.cur), 0), paid: invs.reduce((s, i) => s + iqdOf(i.paid, i.cur), 0), remaining: window.BnyToIqd(due.iqd, due.usd), extra: lastPayOf(c), _c: c, }; }).sort((a, b) => b.remaining - a.remaining); const showDetail = (r) => { selectCust(r._c); try { window.scrollTo({ top: 0, behavior: "smooth" }); } catch (e) {} }; // بيانات الطباعة حسب النمط: كشف كامل / الفواتير / الدفعات فقط const printData = (() => { if (!printCust) return { title: "", rows: [], cols: [] }; if (printMode === "inv") { const rows = window.BnyCustSaleInvoices(printCust).slice().sort((a, b) => String(a.date || "").localeCompare(String(b.date || ""))); return { title: "فواتير العميل", rows, cols: [ { label: "التاريخ", mono: true, render: (i) => window.BnyDate(i.date) }, { label: "الفاتورة", render: (i) => "#" + i.no }, { label: "العملة", align: "center", render: (i) => cur(i.cur) }, { label: "الإجمالي", align: "end", mono: true, render: (i) => Number(i.total || 0).toLocaleString("en-US") }, { label: "المسدَّد", align: "end", mono: true, render: (i) => Number(i.paid || 0).toLocaleString("en-US") }, { label: "المتبقّي", align: "end", mono: true, render: (i) => Math.max(0, (Number(i.total) || 0) - (Number(i.paid) || 0)).toLocaleString("en-US") }, ] }; } const all = window.BnyCustStatement(printCust); const rows = printMode === "pay" ? all.filter((r) => r.kind === "payment") : all; return { title: printMode === "pay" ? "دفعات العميل" : "كشف حساب عميل", rows, cols: [ { label: "التاريخ", render: (s) => s.date, mono: true }, { label: "البيان", render: (s) => s.ref }, { label: "العملة", align: "center", render: (s) => cur(s.cur) }, { label: "مدين", align: "end", mono: true, render: (s) => s.debit ? Number(s.debit).toLocaleString("en-US") : "—" }, { label: "دائن", align: "end", mono: true, render: (s) => s.credit ? Number(s.credit).toLocaleString("en-US") : "—" }, ] }; })(); return (
{/* list */} } onClick={() => setShowNew(true)}>جديد}>
setSearch(e.target.value)} placeholder="ابحث عن عميل" icon={} />
{custRows.length === 0 &&
لا عملاء مطابقون
} {custRows.map((c) => { const on = sel && c.id === sel.id; const settled = c.balance === 0 && c.balanceUsd === 0; const co = orgOf(c); return ( ); })}
{/* detail */}
{!sel ? (
لا عملاء بعد
أضف أول عميل لعرض حسابه وكشوفه هنا.
) : (<>

{sel.name}

{sel.type}
{sel.phone}
{org ? <>{org.name}{sel.title ? " · " + sel.title : ""} : <>فرد مستقل (غير تابع لمنظمة)}
{!isMobile && (
)}
{isMobile && (
)} {/* نطاق الكشف: موظف أو الشركة كاملة (يظهر فقط للتابعين لمنظمة) */} {org && (
نطاق كشف الحساب
{orgScope && ({members.length} موظفين · {org.name})}
)} {/* الرصيد المستحق — العملتان منفصلتان، حسب النطاق */}
0 ? "debt" : "credit"} style={{ fontSize: isMobile ? 19 : 22 }} />} /> 0 ? "debt" : "credit"} style={{ fontSize: isMobile ? 19 : 22 }} />} />
{/* السقف الائتماني: الحالة الافتراضية بدون سقف، يُفعَّل عند الحاجة */} {!orgScope && (
السقف الائتماني
{sel.limitEnabled ? "مفعّل — يمنع البيع الآجل عند تجاوزه" : "غير مفعّل — ائتمان غير محدود"}
{sel.limitEnabled ? "مفعّل" : "إيقاف"}
{sel.limitEnabled && (
setLimitVal("limit", e.target.value.replace(/[^\d]/g, ""))} />
setLimitVal("limitUsd", e.target.value)} />
)}
)}
{tab === "inv" ? (
{custInvoices.map((inv) => { const st = window.BnyInvStatus(inv); const rem = Math.max(0, (Number(inv.total) || 0) - (Number(inv.paid) || 0)); const u = inv.cur === "usd" ? "$" : "د.ع"; return (
window.BnyOpenInvoice(inv.no)}>
فاتورة #{inv.no} {st.label}
{inv.date} · الإجمالي {Number(inv.total || 0).toLocaleString("en-US")} {u} · مسدَّد {Number(inv.paid || 0).toLocaleString("en-US")} {u}
المتبقّي
0 ? "debt" : "credit"} />
{rem > 0 ? : مسددة}
); })} {custInvoices.length === 0 &&
لا فواتير لهذا العميل
}
) : isMobile ? (
{stmtRows.map((s, i) => { const canOpen = s.invNo && window.BnyInvoices && window.BnyInvoices.get(s.invNo); const isPay = s.kind === "payment"; return (
window.BnyOpenInvoice(s.invNo) : undefined}>
{s.ref}
{s.date}{orgScope ? " · " + custName(s.cust) : ""} · {s.debit ? "مدين" : "دائن"} · {cur(s.cur)}
{s.debit ? : } {isPay && s._pay && }
); })} {stmtRows.length === 0 &&
لا توجد حركات
}
) : (
{orgScope && } {stmtRows.map((s, i) => { const canOpen = s.invNo && window.BnyInvoices && window.BnyInvoices.get(s.invNo); const isPay = s.kind === "payment"; return ( (e.currentTarget.style.background = "var(--surface-hover)") : undefined} onMouseLeave={canOpen ? (e) => (e.currentTarget.style.background = "transparent") : undefined} style={{ borderTop: "1px solid var(--border-subtle)", transition: "background var(--dur-fast,160ms) ease" }}> {orgScope && } ); })}
التاريخالموظفالبيان العملة مدين دائن
window.BnyOpenInvoice(s.invNo) : undefined} style={{ padding: "13px 0", fontFamily: "var(--font-mono)", fontSize: 13, color: "var(--text-muted)", cursor: canOpen ? "pointer" : "default" }}>{s.date}{custName(s.cust)} window.BnyOpenInvoice(s.invNo) : undefined} style={{ padding: "13px 0", fontSize: 14, fontWeight: 500, color: canOpen ? "var(--text-link)" : "var(--text-strong)", cursor: canOpen ? "pointer" : "default" }}>{s.ref} {cur(s.cur)} {s.debit ? : } {s.credit ? : } {isPay && s._pay ? : null}
)}
)}
{/* نوافذ: عميل جديد + تسديد دين */} {showNew && setShowNew(false)} />} {showPay && sel && setShowPay(false)} />} {showEdit && sel && setShowEdit(false)} />} {payInv && ( doPayInvoice(amount)} onClose={() => setPayInv(null)} /> )} {/* كشف الديون الإجمالي انتقل إلى قسم «الحسابات» */} {/* ورقة الكشف للطباعة/PDF (مخفية على الشاشة) */} {printCust && (
t.label)} />
)} {delPay && ( setDelPay(null)} /> )}
); } /* نافذة تعديل بيانات عميل — الاسم/الهواتف/العنوان/النوع/المؤسسة */ function EditCustomerModal({ c, orgs, onSave, onClose }) { const I = window.BnyIcon; const { Button, Input, Select } = window.BonyanDesignSystem_ad3ca0; const [f, setF] = React.useState(() => ({ name: c.name || "", phone: c.phone || "", phone2: c.phone2 || "", address: c.address || "", type: c.type || "نقدي", org: c.org || "" })); const [busy, setBusy] = React.useState(false); const set = (k, v) => setF((s) => ({ ...s, [k]: v })); const lbl = { fontSize: 12.5, fontWeight: 600, color: "var(--text-strong)", display: "block", marginBottom: 5 }; const save = async () => { if (!f.name.trim() || busy) return; setBusy(true); try { await onSave({ name: f.name.trim(), phone: f.phone.trim(), phone2: f.phone2.trim(), address: f.address.trim(), type: f.type, org: f.org || null }); } catch (e) { console.error(e); } finally { setBusy(false); } }; return (
e.stopPropagation()} className="bny-card" style={{ width: "100%", maxWidth: 440, padding: 0, display: "flex", flexDirection: "column", maxHeight: "90vh" }}>

تعديل بيانات: {c.name}

set("name", e.target.value)} />
set("phone", e.target.value)} icon={} />
set("phone2", e.target.value)} />
set("address", e.target.value)} />
{f.name.trim() !== c.name &&

تغيير الاسم سينعكس على كل فواتيره تلقائيًا — الحسابات مربوطة برقمه لا باسمه.

}
); } function Mini({ label, node }) { return (
{label}
{node}
); } window.CustomersScreen = CustomersScreen;