/* Bonyan ERP — New invoice / POS Features: multi-unit selling, multi-currency, price lists + manual override, direct discount (amount/percent), and dual print copies (sales / picking). */ function PosScreen() { const I = window.BnyIcon; const D = window.BnyData; const shop = D.shop; // هوية المحل (تُخصَّص من شاشة الإعدادات) const { SearchBar, Card, Button, Money, Badge, Select, Avatar, Input, Switch } = window.BonyanDesignSystem_ad3ca0; const isMobile = window.useBnyMobile(); const [q, setQ] = React.useState(""); const [listening, setListening] = React.useState(false); // كل سطر يحمل معرّفًا فريدًا (rid) — يسمح بتكرار المادة بوحدات بيع مختلفة بلا تضارب const [lines, setLines] = React.useState([]); // فاتورة جديدة تبدأ فارغة — تُضاف الأصناف يدويًا // قوائم محليّة قابلة للإضافة (عميل/مؤسسة جديدة) — تُكتب في window.BnyData وتُربط لاحقًا بالـ backend const [custList, setCustList] = React.useState(() => D.customers.slice()); const [orgList, setOrgList] = React.useState(() => (D.organizations || []).slice()); const [cust, setCust] = React.useState(() => window.BnyWalkin() || D.customers[0]); // افتراضيًا: زبون مستطرق const [walkName, setWalkName] = React.useState(""); // اسم الزبون العابر — للطباعة فقط (لا يُسجَّل عميلًا) const [custQuery, setCustQuery] = React.useState(""); const [custOpen, setCustOpen] = React.useState(false); const [showNewCust, setShowNewCust] = React.useState(false); const [mgrAsked, setMgrAsked] = React.useState(false); const orgName = (id) => (orgList.find((o) => o.id === id) || {}).name || ""; // الاسم المعروض/المطبوع: للزبون المستطرق نستخدم الاسم العابر إن كُتب const custDisplayName = cust.walkin ? (walkName.trim() || cust.name) : cust.name; const [invCur, setInvCur] = React.useState("iqd"); // عملة الفاتورة: iqd | usd const [priceType, setPriceType] = React.useState("main"); // نوع التسعير const [discountType, setDiscountType] = React.useState("amount"); // amount | percent const [discountValue, setDiscountValue] = React.useState(""); const [paid, setPaid] = React.useState(""); // المبلغ المسدد const [invDate, setInvDate] = React.useState(() => window.BnyOps.todayIso()); // تاريخ الفاتورة (قابل للتغيير) const [printMode, setPrintMode] = React.useState("sales"); // sales | picking const isUsd = invCur === "usd"; const curSym = window.BnyCur(invCur); // ---- unit + pricing helpers ---- const unitLabel = (l) => (l.unitMode === "pack" && l.hasMultiUnits ? l.packUnit : l.unit); const factor = (l) => (l.unitMode === "pack" && l.hasMultiUnits ? l.subUnitCount : 1); const liveMat = (l) => D.materials.find((m) => m.sku === l.sku) || l; const basePrice = (l) => { // سعر الوحدة الصغرى للنوع المختار (يدوي لكل مادة) const m = liveMat(l); const map = isUsd ? m.pricesUsd : m.prices; if (map && map[priceType] != null) return map[priceType]; if (map && map.main != null) return map.main; return isUsd ? m.priceUsd : m.price; }; const computedUnit = (l) => basePrice(l) * factor(l); // سعر الوحدة المختارة const effUnit = (l) => (l.priceOverride != null ? l.priceOverride : computedUnit(l)); // مع التعديل اليدوي const baseQty = (l) => (Number(l.qty) || 0) * factor(l); // بالوحدة الصغرى (لتحديث المخزن) const lineTotal = (l) => effUnit(l) * (Number(l.qty) || 0); const clearOverrides = () => setLines((ls) => ls.map((l) => ({ ...l, priceOverride: undefined }))); const setCurrency = (c) => { setInvCur(c); clearOverrides(); }; const setType = (id) => { setPriceType(id); clearOverrides(); }; const results = q ? D.materials.filter((m) => (m.name + m.sku + m.cat).includes(q)) : []; // اقتراحات إرشادية: الأكثر مبيعًا هذا الشهر، وإلا المتوفر في المخزن. فارغة إذا لا مواد. const inStock = D.materials.filter((m) => Number(m.stock) > 0); const topSell = window.BnyTopProductsThisMonth(8).map((p) => D.materials.find((m) => m.name === p.name)).filter(Boolean); const suggestPool = (topSell.length ? topSell : inStock).slice(0, 6); const exampleNames = suggestPool.slice(0, 2).map((m) => m.name); const searchPlaceholder = exampleNames.length ? ("أضف مادة… مثل: " + exampleNames.join(" أو ")) : (D.materials.length ? "ابحث عن مادة" : ""); const add = (m) => { setLines((ls) => { // ادمج فقط مع سطر بنفس المادة ونفس وحدة البيع الأساسية؛ غير ذلك = سطر جديد مستقل const ex = ls.find((l) => l.sku === m.sku && l.unitMode === "base"); if (ex) return ls.map((l) => (l === ex ? { ...l, qty: (Number(l.qty) || 0) + 1 } : l)); return [...ls, { ...m, rid: bnyRid(), qty: 1, unitMode: "base" }]; }); setQ(""); }; const setQty = (rid, qty) => setLines((ls) => ls.map((l) => (l.rid === rid ? { ...l, qty } : l))); const stepQty = (l, d) => setQty(l.rid, Math.max(1, (Number(l.qty) || 0) + d)); const typeQty = (l, raw) => setQty(l.rid, String(raw).replace(/[^\d.]/g, "")); // إدخال مباشر const blurQty = (l) => { const n = parseFloat(l.qty); setQty(l.rid, (!n || n < 1) ? 1 : n); }; const setMode = (rid, unitMode) => setLines((ls) => ls.map((l) => (l.rid === rid ? { ...l, unitMode, priceOverride: undefined } : l))); const setOverride = (rid, raw) => setLines((ls) => ls.map((l) => { if (l.rid !== rid) return l; const v = raw === "" ? undefined : Number(raw); return { ...l, priceOverride: (v == null || Number.isNaN(v)) ? undefined : v }; })); const remove = (rid) => setLines((ls) => ls.filter((l) => l.rid !== rid)); // إنشاء عميل/مؤسسة جديدة — تُحفظ محليًا وفي القاعدة (BnyOps) const createCustomer = async (data) => { try { 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, }); setCustList(D.customers.slice()); setCust(c); setCustOpen(false); setShowNewCust(false); } catch (e) { console.error(e); try { window.alert("تعذّر حفظ العميل — تحقّق من الاتصال."); } catch (x) {} } }; const createOrg = (data) => { const id = "org-" + Date.now(); const o = { id, name: data.name.trim(), phone: "", type: "شركة", notes: (data.notes || "").trim() }; window.BnyOps.addOrganization(o); // يدفع محليًا فورًا ويثبّت في القاعدة setOrgList((D.organizations || []).slice()); return o; }; const subtotal = lines.reduce((s, l) => s + lineTotal(l), 0); const dv = Number(discountValue) || 0; let discountAmount = discountType === "percent" ? subtotal * dv / 100 : dv; discountAmount = Math.min(subtotal, Math.max(0, discountAmount)); if (!isUsd) discountAmount = Math.round(discountAmount); const total = Math.max(0, subtotal - discountAmount); const paidNum = Math.min(total, Math.max(0, Number(paid) || 0)); // المسدد لا يتجاوز الإجمالي const remaining = Math.max(0, total - paidNum); // المتبقي (لحظي) const custBalance = isUsd ? cust.balanceUsd : cust.balance; const hasLimit = !!cust.limitEnabled; // السقف غير مفعّل افتراضيًا (ائتمان غير محدود) const custLimit = isUsd ? cust.limitUsd : cust.limit; const newBalance = custBalance + remaining; // يُضاف للدين المتبقي فقط (لا الإجمالي) const over = hasLimit && newBalance > custLimit && cust.type === "آجل"; const [saved, setSaved] = React.useState(null); // الفاتورة المحفوظة (بعد التأكيد) const [saving, setSaving] = React.useState(false); const invoiceNo = saved ? saved.no : String(((D.numbering || {}).sale || {}).next || ""); const today = window.BnyDate(invDate); // يظهر في الطباعة بتاريخ الفاتورة المختار const printCopy = (mode) => { setPrintMode(mode); setTimeout(() => window.print(), 80); }; // ---- حفظ وتأكيد البيع: فاتورة + خصم مخزون + رصيد وكشف العميل ---- const canSave = lines.length > 0 && total > 0 && !over && !saving; const confirmSale = async () => { if (!canSave) return; setSaving(true); try { const inv = await window.BnyOps.saveSale({ lines: lines.map((l) => ({ sku: l.sku, name: l.name, unitLabel: unitLabel(l), baseQty: baseQty(l), qty: Number(l.qty) || 0, price: effUnit(l) })), cust, party: custDisplayName, cur: invCur, total, paid: paidNum, remaining, date: invDate, }); setSaved(inv); } catch (e) { console.error("Bonyan saveSale:", e); try { window.alert("تعذّر حفظ الفاتورة — تحقّق من الاتصال ثم أعد المحاولة."); } catch (x) {} } setSaving(false); }; const resetSale = () => { setLines([]); setPaid(""); setDiscountValue(""); setWalkName(""); setSaved(null); setMgrAsked(false); setInvDate(window.BnyOps.todayIso()); }; // ---- إرسال الفاتورة واتساب ---- const waInvoice = () => { const itemsTxt = lines.map((l) => "• " + l.name + " — " + l.qty + " " + unitLabel(l)).join("\n"); const msg = "فاتورة " + (saved ? "#" + saved.no + " " : "") + "من " + (shop.name || "بُنيان") + "\n" + "العميل: " + custDisplayName + "\n" + itemsTxt + "\n" + "الإجمالي: " + total.toLocaleString("en-US") + " " + curSym + (paidNum > 0 ? "\nالمسدد: " + paidNum.toLocaleString("en-US") + " " + curSym : "") + (remaining > 0 ? "\nالمتبقي: " + remaining.toLocaleString("en-US") + " " + curSym : "") + "\nشكرًا لتعاملكم معنا."; const url = cust.phone ? window.BnyWaLink(cust.phone, msg) : "https://wa.me/?text=" + encodeURIComponent(msg); try { window.open(url, "_blank"); } catch (e) {} }; // فاصلة لكل 3 مراتب لمنع الخطأ const fmtNum = (n) => (n === "" || n == null) ? "" : Number(n).toLocaleString("en-US"); const inputStyle = (overridden) => ({ height: 34, width: isMobile ? 120 : 140, fontSize: 13, padding: "0 8px", borderColor: overridden ? "var(--brand)" : "var(--border-strong)" }); // editable per-line unit price (manual override) — thousands-separated for IQD const PriceInput = (l) => ( {isUsd ? ( setOverride(l.rid, e.target.value)} aria-label="سعر الوحدة" style={inputStyle(l.priceOverride != null)} /> ) : ( setOverride(l.rid, e.target.value.replace(/[^\d]/g, ""))} aria-label="سعر الوحدة" style={inputStyle(l.priceOverride != null)} /> )} {curSym} / {unitLabel(l)} ); return (
{/* left: item search + lines */}
setListening((v) => !v)} placeholder={searchPlaceholder} /> {!q && suggestPool.length > 0 && (
{topSell.length ? "الأكثر طلبًا:" : "المتوفّرة:"} {suggestPool.map((m) => ( ))}
)} {results.length > 0 && (
{results.map((m) => (
add(m)} style={{ display: "flex", alignItems: "center", gap: 12, padding: "11px 14px", cursor: "pointer", borderBottom: "1px solid var(--border-subtle)" }} onMouseEnter={(e) => (e.currentTarget.style.background = "var(--surface-hover)")} onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")}>
{m.name}
{m.sku} · متوفر: {m.hasMultiUnits ? `${window.BnyStock(m)} (${m.stock.toLocaleString("en-US")} ${m.unit})` : `${m.stock} ${m.unit}`}
))}
)}
{isMobile ? (
{lines.map((l) => (
{l.name}
{l.unitMode === "pack" && l.hasMultiUnits ?
= {baseQty(l)} {l.unit}
:
{l.cat}
}
{l.hasMultiUnits ? (
) : ( {l.unit} )}
{PriceInput(l)}
))}
) : (
{lines.map((l) => ( ))}
المادة وحدة البيع الكمية سعر الوحدة المجموع
{l.name}
{l.unitMode === "pack" && l.hasMultiUnits ?
= {baseQty(l)} {l.unit} (خصم من المخزن)
:
{l.cat}
}
{l.hasMultiUnits ? (
) : ( {l.unit} )}
{PriceInput(l)}
)}
{/* right: customer + totals */}
{custDisplayName}
{cust.walkin ? "بيع سريع — زبون عابر" : cust.phone}
{cust.walkin ? "مستطرق" : cust.type}
{/* اختيار العميل: بحث ذكي بالاسم/الهاتف/المؤسسة + زر عميل جديد */}
{ setCustOpen(true); setCustQuery(""); }} onChange={(e) => { setCustQuery(e.target.value); setCustOpen(true); }} placeholder="ابحث باسم العميل أو المؤسسة…" style={{ flex: 1 }} aria-label="اختيار العميل" />
{custOpen &&
setCustOpen(false)} style={{ position: "fixed", inset: 0, zIndex: 9 }} />} {custOpen && (() => { const ql = custQuery.trim(); const matches = custList.filter((c) => !ql || c.name.includes(ql) || (c.phone || "").includes(ql) || orgName(c.org).includes(ql)); return (
{matches.map((c) => (
{ setCust(c); setCustOpen(false); setMgrAsked(false); }} onMouseEnter={(e) => (e.currentTarget.style.background = "var(--surface-hover)")} onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8, padding: "9px 12px", cursor: "pointer", borderBottom: "1px solid var(--border-subtle)" }}>
{c.name}
{orgName(c.org) || c.type}{c.phone ? " · " + c.phone : ""}
{cust.id === c.id && }
))} {matches.length === 0 &&
لا نتائج —
}
); })()}
{cust.walkin && (
setWalkName(e.target.value)} placeholder="يُطبع على الفاتورة فقط، لا يُسجَّل عميلًا" aria-label="اسم الزبون العابر" />
)}
الرصيد الحالي ({curSym}) 0 ? "debt" : "default"} />
السقف الائتماني {hasLimit ? : بدون سقف}
{/* invoice settings: date + currency + price list */}
تاريخ الفاتورة setInvDate(e.target.value || window.BnyOps.todayIso())} aria-label="تاريخ الفاتورة" style={{ height: 36, width: 175, padding: "0 10px", fontFamily: "var(--font-mono)", fontSize: 13 }} />
عملة الفاتورة
نوع التسعير
} /> {/* direct discount: amount (currency) or percent */}
الخصم
{discountType === "amount" && !isUsd ? ( setDiscountValue(e.target.value.replace(/[^\d]/g, ""))} aria-label="قيمة الخصم" style={{ height: 34, width: 110, fontSize: 13, padding: "0 8px" }} /> ) : ( setDiscountValue(e.target.value)} aria-label="قيمة الخصم" style={{ height: 34, width: 92, fontSize: 13, padding: "0 8px" }} /> )}
{discountAmount > 0 && (
قيمة الخصم المطبّق
)}
الإجمالي
{/* المبلغ المسدد → المتبقي (لحظي) */}
المبلغ المسدد setPaid(isUsd ? e.target.value.replace(/[^\d.]/g, "") : e.target.value.replace(/[^\d]/g, ""))} placeholder="0" aria-label="المبلغ المسدد" style={{ height: 36, width: 140, fontSize: 13, padding: "0 8px", textAlign: "start" }} />
المتبقي 0 ? "debt" : "default"} style={{ fontSize: 16, fontWeight: 700 }} />
{over && (
تجاوز السقف الائتماني. لا يمكن إتمام البيع الآجل. الرصيد بعد الفاتورة: {newBalance.toLocaleString("en-US")} {curSym} (السقف: {custLimit.toLocaleString("en-US")} {curSym}).
)}
{saved ? (
تم حفظ الفاتورة #{saved.no} بنجاح
خُصم المخزون وحُدّث حساب العميل تلقائيًا.
) : ( )} {over && !saved && ( mgrAsked ?
أُرسل طلب الإذن للمدير… بانتظار الموافقة
: )}
{showNewCust && setShowNewCust(false)} />} {/* A4 print view (hidden on screen, shown via @media print). Two copies: sales → prices + totals, no warehouse, no price-list name. picking→ warehouse per item, no prices/totals. */}
{shop.logo ? : }
{shop.name}
{printMode !== "picking" && shop.address ?
{shop.address}
: null} {shop.phone ?
هاتف: {shop.phone}
: null}
{printMode === "sales" ? (
فاتورة بيع #{invoiceNo}
التاريخ: {today}
العملة: {isUsd ? "دولار ($)" : "دينار (د.ع)"}
) : (
نسخة تجهيز
)}
{printMode === "sales" && (
العميل: {custDisplayName}
{cust.phone ?
الهاتف: {cust.phone}
: null}
)} {printMode === "picking" ? ( <> {lines.map((l) => ( ))}
المادة الوحدة الكمية (العدد)
{l.name} {unitLabel(l)} {l.qty}{l.unitMode === "pack" && l.hasMultiUnits ? ` (= ${baseQty(l)} ${l.unit})` : ""}
نسخة تجهيز البضاعة — بدون أسعار · تُسلّم لعمّال المستودع
) : ( <> {lines.map((l) => ( ))}
المادة الوحدة الكمية سعر الوحدة المجموع
{l.name} {unitLabel(l)} {l.qty} {effUnit(l).toLocaleString("en-US")} {curSym} {lineTotal(l).toLocaleString("en-US")} {curSym}
المجموع الفرعي{subtotal.toLocaleString("en-US")} {curSym}
{discountAmount > 0 &&
الخصم− {discountAmount.toLocaleString("en-US")} {curSym}
}
الإجمالي{total.toLocaleString("en-US")} {curSym}
{paidNum > 0 &&
المسدّد{paidNum.toLocaleString("en-US")} {curSym}
} {remaining > 0 &&
المتبقي{remaining.toLocaleString("en-US")} {curSym}
}
{shop.footer}
)}
); } const qtyBtn = { width: 30, height: 30, border: "none", background: "var(--surface-sunken)", cursor: "pointer", fontSize: 16, color: "var(--text-body)", fontWeight: 600 }; const printTh = { textAlign: "start", fontWeight: 600, padding: "8px 10px", borderBottom: "1px solid var(--border-strong)" }; const printTd = { padding: "8px 10px" }; function Row({ k, v }) { return (
{k}{v}
); } /* معرّف فريد لكل سطر فاتورة (يمنع تضارب تكرار نفس المادة بوحدات مختلفة) */ let bnyRowSeq = 0; function bnyRid() { bnyRowSeq += 1; return "r" + bnyRowSeq + "-" + Date.now().toString(36); } /* حقل الكمية: إدخال مباشر + زرّا زيادة/إنقاص */ function QtyInput({ l, onStep, onType, onBlur }) { return (
onType(l, e.target.value)} onBlur={() => onBlur(l)} inputMode="decimal" aria-label="الكمية" style={{ width: 54, textAlign: "center", fontFamily: "var(--font-mono)", fontWeight: 600, border: "none", outline: "none", background: "transparent", fontSize: 14, padding: "6px 0", color: "var(--text-strong)" }} />
); } const bnyModalOverlay = { position: "fixed", inset: 0, zIndex: 1000, background: "rgba(0,0,0,.45)", display: "flex", alignItems: "center", justifyContent: "center", padding: "var(--space-5)" }; const bnyModalHead = { display: "flex", alignItems: "center", gap: 10, padding: "16px 20px", borderBottom: "1px solid var(--border-subtle)" }; const bnyModalFoot = { padding: "12px 20px", borderTop: "1px solid var(--border-subtle)", display: "flex", justifyContent: "flex-end", gap: 8 }; const bnyXBtn = { border: 0, background: "none", cursor: "pointer", color: "var(--text-muted)", display: "inline-flex" }; const bnyLbl = { fontSize: 12.5, fontWeight: 600, color: "var(--text-strong)", display: "block", marginBottom: 5 }; /* نافذة عميل جديد سريع: اسم/هاتف/عنوان + تفعيل مؤسسة (مع إضافة مؤسسة) */ function NewCustomerModal({ orgs, onCreateOrg, onSave, onClose }) { const I = window.BnyIcon; const { Button, Input, Switch, Select } = window.BonyanDesignSystem_ad3ca0; const [name, setName] = React.useState(""); const [phone, setPhone] = React.useState(""); const [phone2, setPhone2] = React.useState(""); const [showPhone2, setShowPhone2] = React.useState(false); const [address, setAddress] = React.useState(""); const [hasOrg, setHasOrg] = React.useState(false); const [org, setOrg] = React.useState(""); const [orgList, setOrgList] = React.useState(orgs || []); const [showOrg, setShowOrg] = React.useState(false); const canSave = name.trim() !== ""; const save = () => { if (canSave) onSave({ name, phone, phone2, address, org: hasOrg ? (org || null) : null }); }; const onNewOrg = (data) => { const o = onCreateOrg(data); setOrgList((l) => [...l, o]); setOrg(o.id); setHasOrg(true); setShowOrg(false); }; return (
e.stopPropagation()} className="bny-card" style={{ width: "100%", maxWidth: 440, padding: 0, display: "flex", flexDirection: "column", maxHeight: "90vh" }}>

عميل جديد

setName(e.target.value)} placeholder="الاسم الكامل للعميل" autoFocus />
setPhone(e.target.value)} placeholder="07XXXXXXXXX" icon={} /> {!showPhone2 ? ( ) : (
setPhone2(e.target.value)} placeholder="رقم هاتف ثانٍ (اختياري)" icon={} />
)}
setAddress(e.target.value)} placeholder="المدينة — المنطقة / الشارع" />
تابع لمؤسسة setHasOrg((v) => !v)} />
{hasOrg && (
)}
{showOrg && setShowOrg(false)} />}
); } /* نافذة فرعية: مؤسسة جديدة (اسم + ملاحظات) */ function NewOrgModal({ onSave, onClose }) { const I = window.BnyIcon; const { Button, Input } = window.BonyanDesignSystem_ad3ca0; const [name, setName] = React.useState(""); const [notes, setNotes] = React.useState(""); const canSave = name.trim() !== ""; return (
e.stopPropagation()} className="bny-card" style={{ width: "100%", maxWidth: 400, padding: 0, display: "flex", flexDirection: "column" }}>

مؤسسة جديدة

setName(e.target.value)} placeholder="مثال: شركة الإعمار للمقاولات" autoFocus />