/* 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)}>جديد}>
{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 ? (
لا عملاء بعد
أضف أول عميل لعرض حسابه وكشوفه هنا.
} onClick={() => setShowNew(true)}>إضافة عميل
) : (<>
{sel.name}
{sel.type}
{sel.phone}
{org
? <>{org.name}{sel.title ? " · " + sel.title : ""}>
: <>فرد مستقل (غير تابع لمنظمة)>}
{!isMobile && (
} onClick={() => setShowEdit(true)}>تعديل
} onClick={doPrint}>طباعة
} onClick={waStatement}>إرسال كشف
} onClick={() => setShowPay(true)}>تسديد دين
)}
{isMobile && (
} onClick={() => setShowEdit(true)}>تعديل
} onClick={doPrint}>طباعة
} onClick={waStatement}>إرسال كشف
} onClick={() => setShowPay(true)}>تسديد دين
)}
{/* نطاق الكشف: موظف أو الشركة كاملة (يظهر فقط للتابعين لمنظمة) */}
{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 && (
)}
)}
{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
?
} onClick={() => setPayInv(inv)}>تسديد
:
مسددة}
);
})}
{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" }}>
| 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} |
{orgScope && {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("address", e.target.value)} />
{f.name.trim() !== c.name &&
تغيير الاسم سينعكس على كل فواتيره تلقائيًا — الحسابات مربوطة برقمه لا باسمه.
}
} onClick={save} disabled={!f.name.trim() || busy}>{busy ? "جارٍ الحفظ…" : "حفظ التعديلات"}
);
}
function Mini({ label, node }) {
return (
);
}
window.CustomersScreen = CustomersScreen;