/* Bonyan ERP — نافذة الفاتورة الموحّدة (تُستخدم في كل النظام) تعرض كل تفاصيل الفاتورة: المعلومات + الأصناف (المواد المباعة/المشتراة) مع إمكانية التعديل (الأصناف، الكميات، الأسعار، الجهة، الدفع) والحذف. أي شاشة تفتح فاتورة عبر: window.BnyOpenInvoice(invoiceNo) */ /* ---- مخزن مركزي بسيط لمزامنة التعديلات عبر كل الشاشات ---- */ (function () { var subs = new Set(); function emit() { subs.forEach(function (fn) { try { fn(); } catch (e) {} }); } function syncLegacy(no, inv) { var D = window.BnyData, st = window.BnyInvStatus(inv); [D.recentInvoices, D.purchaseInvoices].forEach(function (arr) { if (!arr) return; arr.forEach(function (r) { if (r.no === no) { r.no = inv.no; r.total = inv.total; r.cur = inv.cur; r.status = st.label; r.state = st.state; } }); }); } // في الوضع المباشر: احفظ التغيير في Supabase أيضًا (دون تعطيل تجاوب الواجهة). function persist(op, args) { if (!window.BnyConfigured || !window.BnyDB) return; var fn = window.BnyDB[op]; if (!fn) return; fn.apply(window.BnyDB, args).catch(function (e) { console.warn("بُنيان: فشل حفظ الفاتورة في الخادم", e); try { window.alert("تعذّر حفظ التغيير في الخادم. تحقّق من الاتصال ثم أعد المحاولة."); } catch (x) {} }); } window.BnyInvoices = { get: function (no) { return (window.BnyData.invoices || []).find(function (i) { return i.no === no; }); }, save: function (no, patch) { var old = this.get(no); // لقطة ما قبل التعديل (لحساب فروق المخزون/الأرصدة) window.BnyData.invoices = window.BnyData.invoices.map(function (i) { return i.no === no ? patch : i; }); syncLegacy(no, patch); emit(); persist("saveInvoice", [no, patch]); if (window.BnyOps) { window.BnyOps.invoiceEffects(old, patch); // إعادة ضبط المخزون والأرصدة بالفرق window.BnyOps.log("invoice_update", "تعديل الفاتورة #" + patch.no + " («" + (patch.party || "—") + "») — الإجمالي " + (patch.total || 0).toLocaleString("en-US") + " " + (patch.cur === "usd" ? "$" : "د.ع"), patch.no); } }, remove: function (no) { var D = window.BnyData; var old = this.get(no); // لعكس أثرها (إرجاع المخزون وتخفيض الدين) D.invoices = D.invoices.filter(function (i) { return i.no !== no; }); if (D.recentInvoices) D.recentInvoices = D.recentInvoices.filter(function (r) { return r.no !== no; }); if (D.purchaseInvoices) D.purchaseInvoices = D.purchaseInvoices.filter(function (r) { return r.no !== no; }); emit(); persist("removeInvoice", [no]); if (window.BnyOps) { if (old) window.BnyOps.invoiceEffects(old, null); // عكس أثر الفاتورة المحذوفة window.BnyOps.log("invoice_delete", "حذف الفاتورة #" + no, no); } }, add: function (inv, opts) { // إضافة فاتورة — applyEffects عند النسخ فقط (البيع من POS يطبّق أثره بنفسه) window.BnyData.invoices = [inv].concat(window.BnyData.invoices || []); emit(); persist("saveInvoice", [inv.no, inv]); // في الوضع المباشر: إدراج جديد if (opts && opts.applyEffects && window.BnyOps) window.BnyOps.invoiceEffects(null, inv); }, subscribe: function (fn) { subs.add(fn); return function () { subs.delete(fn); }; }, touch: function () { emit(); }, // إجبار إعادة الرسم بعد تغيير مسدَّد فاتورة (تسديد) }; })(); /* خطّاف يُعيد رسم الشاشة عند أي تعديل/حذف فاتورة */ window.useBnyInvoices = function () { var r = React.useReducer(function (x) { return x + 1; }, 0); React.useEffect(function () { return window.BnyInvoices.subscribe(r[1]); }, []); return window.BnyData.invoices; }; function bnyCloneInv(inv) { return Object.assign({}, inv, { items: (inv.items || []).map(function (it) { return Object.assign({}, it); }) }); } /* ---- النافذة: مستند فاتورة كامل (عرض + تعديل + طباعة + حذف) ---- */ function BnyInvoiceModal({ no, onClose }) { const I = window.BnyIcon, cur = window.BnyCur, status = window.BnyInvStatus, fmtDate = window.BnyDate; const { Button, Input, Select } = window.BonyanDesignSystem_ad3ca0; const isMobile = window.useBnyMobile(); const shop = window.BnyData.shop || {}; const source = window.BnyInvoices.get(no); const [draft, setDraft] = React.useState(() => (source ? bnyCloneInv(source) : null)); const [confirmDel, setConfirmDel] = React.useState(false); const [matQ, setMatQ] = React.useState(""); // بحث المواد (إضافة مادة) const [matOpen, setMatOpen] = React.useState(false); const [partyQ, setPartyQ] = React.useState(""); // بحث العميل/المورد const [partyOpen, setPartyOpen] = React.useState(false); const [pendingParty, setPendingParty] = React.useState(null); // جهة بانتظار تأكيد التغيير // تنظيف صنف الطباعة عند انتهاء الطباعة React.useEffect(() => { const after = () => document.body.classList.remove("bny-printing-inv"); window.addEventListener("afterprint", after); return () => { window.removeEventListener("afterprint", after); document.body.classList.remove("bny-printing-inv"); }; }, []); if (!source || !draft) return null; const sym = cur(draft.cur); const num = (v) => Number(v) || 0; const lineTotal = (it) => num(it.qty) * num(it.price); const subtotal = (draft.items || []).reduce((s, it) => s + lineTotal(it), 0); const paid = Math.min(subtotal, Math.max(0, num(draft.paid))); const remaining = Math.max(0, subtotal - paid); const st = status({ total: subtotal, paid }); const fmt = (n) => Number(n).toLocaleString("en-US"); const partyLabel = draft.type === "sale" ? "العميل" : "المورد"; const printInvoice = () => { document.body.classList.add("bny-printing-inv"); setTimeout(() => window.print(), 60); }; const setF = (k, v) => setDraft((d) => ({ ...d, [k]: v })); const setItem = (idx, k, v) => setDraft((d) => ({ ...d, items: d.items.map((it, i) => (i === idx ? { ...it, [k]: v } : it)) })); const addItem = () => setDraft((d) => ({ ...d, items: [...(d.items || []), { name: "", unit: "قطعة", qty: 1, price: 0 }] })); // إضافة مادة من المخزن (بحث لحظي) — تعبّئ الاسم والوحدة والسعر تلقائيًا const addMaterial = (m) => { const price = draft.cur === "usd" ? (m.priceUsd || (m.pricesUsd && m.pricesUsd.main) || 0) : (m.price || (m.prices && m.prices.main) || 0); setDraft((d) => ({ ...d, items: [...(d.items || []), { name: m.name, unit: m.unit, qty: 1, price: price, sku: m.sku }] })); setMatQ(""); setMatOpen(false); }; const removeItem = (idx) => setDraft((d) => ({ ...d, items: d.items.filter((_, i) => i !== idx) })); // التحقق من رقم الفاتورة: غير فارغ وغير مكرّر (يقارن مع باقي الفواتير) const noTrim = (draft.no || "").trim(); const dupNo = noTrim !== "" && (window.BnyData.invoices || []).some((i) => i.no === noTrim && i.no !== no); const noError = noTrim === "" ? "أدخل رقم الفاتورة" : dupNo ? "رقم الفاتورة مستخدم في فاتورة أخرى" : ""; const save = () => { if (noError) return; // لا يُحفظ مع رقم فارغ أو مكرّر const digits = noTrim.replace(/[^\d]/g, ""); const items = (draft.items || []) .map((it) => ({ name: (it.name || "").trim(), unit: (it.unit || "").trim() || "قطعة", qty: num(it.qty), price: num(it.price), sku: it.sku || undefined })) .filter((it) => it.name !== ""); const total = items.reduce((s, it) => s + it.qty * it.price, 0); const clean = { ...draft, no: noTrim, num: digits ? parseInt(digits, 10) : 0, items, total, paid: Math.min(total, Math.max(0, num(draft.paid))), party: (draft.party || "").trim() || "—", }; window.BnyInvoices.save(no, clean); onClose(); }; const del = () => { window.BnyInvoices.remove(no); onClose(); }; // ---- styles ---- const overlay = { position: "fixed", inset: 0, zIndex: 1000, background: "var(--surface-overlay)", display: "flex", alignItems: isMobile ? "flex-end" : "center", justifyContent: "center", padding: isMobile ? 0 : "var(--space-6)" }; const sheet = { width: "100%", maxWidth: 880, maxHeight: isMobile ? "96vh" : "94vh", overflowY: "auto", background: "var(--surface-card)", border: "1px solid var(--border-strong)", borderRadius: isMobile ? "var(--radius-lg) var(--radius-lg) 0 0" : "var(--radius-lg)", boxShadow: "var(--shadow-lg)" }; const head = { position: "sticky", top: 0, zIndex: 1, display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10, padding: "16px 18px", borderBottom: "1px solid var(--border-subtle)", background: "var(--surface-card)" }; const body = { padding: 18, display: "flex", flexDirection: "column", gap: 16 }; const lbl = { fontSize: 12, color: "var(--text-muted)", fontWeight: 600, marginBottom: 6, display: "block" }; const cellInput = { height: 34, width: "100%", fontSize: 13, padding: "0 8px" }; const typeBadge = draft.type === "sale" ? بيع : شراء; return (
e.stopPropagation()}>
فاتورة #{draft.no} {typeBadge}
{/* هوية المحل (كما تظهر في الطباعة) */}
{shop.logo ? : }
{shop.name || "بُنيان"}
{[shop.address, shop.phone].filter(Boolean).join(" · ")}
{draft.type === "sale" ? "فاتورة بيع" : "فاتورة شراء"}
{fmtDate(draft.date)}
{/* معلومات الفاتورة */}
setF("no", e.target.value)} placeholder="مثال: 10429" error={noError || undefined} style={{ fontFamily: "var(--font-mono)" }} />
{ setPartyOpen(true); setPartyQ(""); }} onChange={(e) => { setPartyQ(e.target.value); setPartyOpen(true); }} placeholder="ابحث بالاسم…" aria-label="اختيار الجهة" style={{ width: "100%" }} /> {partyOpen && (() => { const DB = window.BnyData; const src = draft.type === "sale" ? (DB.customers || []).filter((c) => !c.walkin) : (DB.suppliers || []); const ql = partyQ.trim(); const opts = src.filter((x) => !ql || (x.name || "").includes(ql)).slice(0, 8); return ( <>
setPartyOpen(false)} style={{ position: "fixed", inset: 0, zIndex: 9 }} />
{opts.map((x) => (
{ setPartyOpen(false); if ((x.name || "") !== draft.party) setPendingParty({ name: x.name, id: x.id }); }} onMouseEnter={(e) => (e.currentTarget.style.background = "var(--surface-hover)")} onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")} style={{ padding: "9px 12px", cursor: "pointer", borderBottom: "1px solid var(--border-subtle)", fontSize: 13, fontWeight: 600, color: "var(--text-strong)" }}>{x.name}
))} {opts.length === 0 &&
لا نتائج
}
); })()}
setF("date", e.target.value)} style={{ width: "100%", height: 40, padding: "0 10px", fontFamily: "var(--font-mono)" }} />
{/* الأصناف (المواد) */}
الأصناف ({(draft.items || []).length})
setMatOpen(true)} onChange={(e) => { setMatQ(e.target.value); setMatOpen(true); }} placeholder="إضافة مادة — بحث لحظي بالمخزن…" aria-label="إضافة مادة" style={{ flex: 1, height: 36, fontSize: 13 }} />
{matOpen && (() => { const DB = window.BnyData; const ql = matQ.trim(); const ms = (DB.materials || []).filter((m) => !ql || (m.name + m.sku + (m.cat || "")).includes(ql)).slice(0, 8); return ( <>
setMatOpen(false)} style={{ position: "fixed", inset: 0, zIndex: 9 }} />
{ms.map((m) => (
addMaterial(m)} onMouseEnter={(e) => (e.currentTarget.style.background = "var(--surface-hover)")} onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")} style={{ display: "flex", justifyContent: "space-between", gap: 8, padding: "9px 12px", cursor: "pointer", borderBottom: "1px solid var(--border-subtle)" }}>
{m.name}
{m.sku} · متوفر {m.stock} {m.unit}
{(draft.cur === "usd" ? (m.priceUsd || 0) : (m.price || 0)).toLocaleString("en-US")} {sym}
))} {ms.length === 0 &&
لا مواد مطابقة
}
); })()}
{isMobile ? (
{(draft.items || []).map((it, idx) => (
setItem(idx, "name", e.target.value)} placeholder="اسم المادة" style={{ ...cellInput, flex: 1 }} />
setItem(idx, "unit", e.target.value)} placeholder="الوحدة" style={cellInput} /> setItem(idx, "qty", e.target.value)} placeholder="الكمية" style={cellInput} /> setItem(idx, "price", e.target.value)} placeholder="السعر" style={cellInput} />
المجموع: {fmt(lineTotal(it))} {sym}
))} {(draft.items || []).length === 0 &&
لا أصناف — أضِف صنفًا.
}
) : (
{(draft.items || []).map((it, idx) => ( ))} {(draft.items || []).length === 0 && ( )}
المادة الوحدة الكمية سعر الوحدة المجموع
setItem(idx, "name", e.target.value)} placeholder="اسم المادة" style={cellInput} /> setItem(idx, "unit", e.target.value)} style={cellInput} /> setItem(idx, "qty", e.target.value)} style={cellInput} /> setItem(idx, "price", e.target.value)} style={cellInput} /> {fmt(lineTotal(it))} {sym}
لا أصناف — أضِف صنفًا.
)}
{/* الإجمالي والدفع */}
setF("paid", e.target.value)} />
الإجمالي {fmt(subtotal)} {sym}
المتبقي 0 ? "var(--danger)" : "var(--success)" }}>{fmt(remaining)} {sym}
الحالة {st.label}
{/* أزرار */} {confirmDel ? (
هل أنت متأكد من حذف الفاتورة #{draft.no}؟ لا يمكن التراجع.
) : (
)}
{/* تأكيد تغيير الجهة قبل الاعتماد */} {pendingParty && (
setPendingParty(null)} style={{ position: "fixed", inset: 0, zIndex: 1100, background: "rgba(0,0,0,.45)", display: "flex", alignItems: "center", justifyContent: "center", padding: "var(--space-5)" }}>
e.stopPropagation()} className="bny-card bny-card--pad" style={{ width: "100%", maxWidth: 410 }}>

تغيير {draft.type === "sale" ? "العميل" : "المورد"}

سيتم تغيير الجهة من «{draft.party || "—"}» إلى «{pendingParty.name}» في هذه الفاتورة. هل تريد التأكيد؟

)} {/* ورقة الطباعة A4 (مخفية على الشاشة) */}
{shop.logo ? : }
{shop.name || "بُنيان"}
{shop.address ?
{shop.address}
: null} {shop.phone ?
هاتف: {shop.phone}
: null}
{draft.type === "sale" ? "فاتورة بيع" : "فاتورة شراء"} #{draft.no}
التاريخ: {fmtDate(draft.date)}
العملة: {draft.cur === "usd" ? "دولار ($)" : "دينار (د.ع)"}
{partyLabel}: {draft.party}
الحالة: {st.label}
{(draft.items || []).map((it, idx) => ( ))}
المادة الوحدة الكمية سعر الوحدة المجموع
{it.name} {it.unit} {num(it.qty)} {fmt(num(it.price))} {sym} {fmt(lineTotal(it))} {sym}
الإجمالي{fmt(subtotal)} {sym}
المسدّد{fmt(paid)} {sym}
المتبقي{fmt(remaining)} {sym}
{shop.footer || ""}
); } const pTh = { textAlign: "start", fontWeight: 600, padding: "8px 10px", borderBottom: "1px solid #333" }; const pTd = { padding: "8px 10px" }; const mth = { fontWeight: 600, padding: "10px 8px", textAlign: "center" }; const mtd = { padding: "6px 8px" }; window.BnyInvoiceModal = BnyInvoiceModal; /* ---- المضيف: يُركَّب مرة واحدة في App، ويفتح أي فاتورة عند الطلب ---- */ function InvoiceModalHost() { const [no, setNo] = React.useState(null); React.useEffect(() => { window.__bnyOpenInvoice = (n) => setNo(n); return () => { window.__bnyOpenInvoice = null; }; }, []); if (no == null) return null; return setNo(null)} />; } window.InvoiceModalHost = InvoiceModalHost; /* الواجهة العامة: استدعِها من أي شاشة لفتح فاتورة */ window.BnyOpenInvoice = function (no) { if (window.__bnyOpenInvoice) window.__bnyOpenInvoice(no); }; /* استخراج الفاتورة من نص بيان كشف الحساب (مثل: "فاتورة 10428"، "فاتورة شراء PINV-3391") */ window.BnyInvoiceFromRef = function (ref) { return (window.BnyData.invoices || []).find(function (iv) { return ref && ref.indexOf(iv.no) !== -1; }) || null; };