// Offers.jsx — Oferty: formularz ofertowy z podpowiedzią ceny wg cennika (function () { const { useState, useEffect } = React; const VAT_RATE = 0.23; function fmtPLN(v) { return Math.round(v).toLocaleString('pl-PL') + ' zł'; } function fmtVal(v) { if (v >= 1000000) return (v/1000000).toFixed(1).replace('.',',') + ' M zł'; if (v >= 1000) return Math.round(v/1000) + ' k zł'; return Math.round(v) + ' zł'; } function calcOffer(offer) { const netto = offer.items.reduce((s,it) => s + (Number(it.qty)||0)*(Number(it.price)||0), 0); const discountAmount = netto * (Number(offer.discount)||0) / 100; const afterDiscount = netto - discountAmount; const vat = afterDiscount * VAT_RATE; const brutto = afterDiscount + vat; return { netto, discountAmount, afterDiscount, vat, brutto }; } const inputStyle = { width:'100%', padding:'9px 12px', borderRadius:9, border:'1px solid var(--bdr)', background:'var(--bg-input)', color:'var(--t1)', fontSize:13, outline:'none' }; const labelStyle = { fontSize:11, fontWeight:600, color:'var(--t3)', marginBottom:5, display:'block', textTransform:'uppercase', letterSpacing:0.5 }; const iconBtnStyle = { width:28, height:28, borderRadius:7, border:'1px solid var(--bdr)', background:'var(--bg1)', color:'var(--t2)', cursor:'pointer', display:'flex', alignItems:'center', justifyContent:'center', outline:'none', flexShrink:0 }; function StatusPill({ statusId }) { const s = (window.SEED.offerStatuses||[]).find(x=>x.id===statusId) || { label:statusId, color:'#64748B' }; return ( {s.label} ); } // ── New offer modal ────────────────────────────────────────────── function NewOfferModal({ onClose, onSave, prefillClient, priceList }) { const clients = window.SEED.clients || []; const [clientId, setClientId] = useState(prefillClient?.id || clients[0]?.id || ''); const [discount, setDiscount] = useState(0); const mkRow = (product) => ({ rid: Math.random().toString(36).slice(2), priceId: product?.id || '', name: product?.name || '', unit: product?.unit || 'jednorazowo', qty:1, price: product?.price || 0 }); const [items, setItems] = useState(() => [mkRow(priceList[0])]); const client = clients.find(c => c.id === clientId); const setRow = (rid, patch) => setItems(prev => prev.map(r => r.rid===rid ? { ...r, ...patch } : r)); const pickProduct = (rid, priceId) => { const p = priceList.find(x => x.id === priceId); setRow(rid, { priceId, name:p?.name||'', unit:p?.unit||'jednorazowo', price:p?.price||0 }); }; const addRow = () => setItems(prev => [...prev, mkRow(priceList[0])]); const removeRow = (rid) => setItems(prev => prev.length>1 ? prev.filter(r=>r.rid!==rid) : prev); const netto = items.reduce((s,it)=> s + (Number(it.qty)||0)*(Number(it.price)||0), 0); const discountAmount = netto * (Number(discount)||0) / 100; const afterDiscount = netto - discountAmount; const vat = afterDiscount * VAT_RATE; const brutto = afterDiscount + vat; const canSave = !!client && items.every(it => it.name && Number(it.qty) > 0); const submit = () => { if (!canSave) return; const today = new Date(); const pad = n => String(n).padStart(2,'0'); const dateStr = `${pad(today.getDate())}.${pad(today.getMonth()+1)}.${today.getFullYear()}`; const valid = new Date(today.getTime() + 14*86400000); const validStr = `${pad(valid.getDate())}.${pad(valid.getMonth()+1)}.${valid.getFullYear()}`; onSave({ id: 'OF-' + today.getFullYear() + '-' + Math.floor(100 + Math.random()*899), clientId: client.id, company: client.company, person: client.person, date: dateStr, validUntil: validStr, status:'draft', discount:Number(discount)||0, items: items.map(it => ({ name:it.name, qty:Number(it.qty)||0, unit:it.unit, price:Number(it.price)||0 })), }); onClose(); }; return ( <>
Nowa oferta
Wybierz pozycje z cennika — cena podpowiada się automatycznie, można ją nadpisać
({value:c.id,label:c.company}))} />
setDiscount(e.target.value)} />
{/* Line items */}
{['Pozycja z cennika','Ilość','Cena jedn.','Wartość',''].map(h => (
{h}
))}
{items.map(row => { const rowTotal = (Number(row.qty)||0) * (Number(row.price)||0); return (
pickProduct(row.rid, v)} options={priceList.map(p=>({value:p.id,label:p.name}))} /> setRow(row.rid,{qty:e.target.value})} /> setRow(row.rid,{price:e.target.value})} />
{fmtPLN(rowTotal)}
); })}
{/* Totals */}
{[ ['Suma netto', fmtPLN(netto)], [`Rabat (${discount||0}%)`, '– ' + fmtPLN(discountAmount)], ['Po rabacie (netto)', fmtPLN(afterDiscount)], ['VAT (23%)', fmtPLN(vat)], ].map(([l,v],i) => (
{l}{v}
))}
Suma brutto {fmtPLN(brutto)}
); } // ── Offer drawer — preview / status / print ─────────────────────── function OfferDrawer({ offer, onClose, onStatusChange }) { useEffect(() => { const esc = (e) => { if (e.key === 'Escape') onClose(); }; document.addEventListener('keydown', esc); return () => document.removeEventListener('keydown', esc); }, []); const c = calcOffer(offer); const statuses = window.SEED.offerStatuses || []; return ( <>
{offer.id}
{offer.company}
DemoCRM Sp. z o.o.
ul. Przykładowa 12, 00-000 Warszawa
NIP 000-000-00-00
Oferta {offer.id}
Data: {offer.date}
Ważna do: {offer.validUntil}
Dla
{offer.company}
{offer.person}
{['Pozycja','Ilość','Cena jedn.','Wartość'].map(h => (
{h}
))}
{offer.items.map((it,i) => (
{it.name}
{it.unit}
{it.qty}
{fmtPLN(it.price)}
{fmtPLN(it.qty*it.price)}
))}
Suma netto{fmtPLN(c.netto)}
{offer.discount > 0 &&
Rabat ({offer.discount}%)– {fmtPLN(c.discountAmount)}
}
VAT (23%){fmtPLN(c.vat)}
Razem brutto {fmtPLN(c.brutto)}
Oferta ważna do {offer.validUntil} · Dokument wygenerowany w DemoCRM
{statuses.map(s => ( ))}
{offer.status === 'accepted' && ( )}
); } // ── Price list modal (cennik) — przeglądanie, dodawanie, edycja ──── function PriceListModal({ priceList, setPriceList, onClose }) { const [editingId, setEditingId] = useState(null); const [showAdd, setShowAdd] = useState(false); const [form, setForm] = useState({ name:'', category:'', unit:'jednorazowo', price:'' }); const setF = (k,v) => setForm(f => ({ ...f, [k]:v })); const categories = [...new Set(priceList.map(p => p.category))]; const canSaveForm = form.name.trim().length > 0 && form.category.trim().length > 0 && Number(form.price) > 0; const startAdd = () => { setEditingId(null); setForm({ name:'', category: categories[0] || 'Moduł', unit:'jednorazowo', price:'' }); setShowAdd(true); }; const startEdit = (p) => { setShowAdd(false); setEditingId(p.id); setForm({ name:p.name, category:p.category, unit:p.unit, price:String(p.price) }); }; const cancelForm = () => { setEditingId(null); setShowAdd(false); }; const saveForm = () => { if (!canSaveForm) return; if (editingId) { setPriceList(prev => prev.map(p => p.id === editingId ? { ...p, name:form.name.trim(), category:form.category.trim(), unit:form.unit.trim() || 'jednorazowo', price:Number(form.price) } : p)); } else { setPriceList(prev => [...prev, { id: 'P' + Math.random().toString(36).slice(2,7).toUpperCase(), name: form.name.trim(), category: form.category.trim(), unit: form.unit.trim() || 'jednorazowo', price: Number(form.price), }]); } cancelForm(); }; const removeItem = (id) => { setPriceList(prev => prev.filter(p => p.id !== id)); if (editingId === id) cancelForm(); }; const editForm = (
setF('name',e.target.value)} placeholder="np. Wdrożenie systemu CRM" autoFocus />
setF('category',e.target.value)} placeholder="np. Moduł" list="cennik-categories" /> {categories.map(c =>
setF('unit',e.target.value)} placeholder="jednorazowo" />
setF('price',e.target.value)} placeholder="0" />
); return ( <>
Cennik usług
Pozycje dostępne przy tworzeniu oferty — ceny netto
{showAdd &&
{editForm}
} {categories.map(cat => (
{cat}
{priceList.filter(p=>p.category===cat).map((p,i,arr) => editingId === p.id ? (
{editForm}
) : (
{p.name}
{p.unit}
{fmtPLN(p.price)}
))}
))} {!showAdd && ( )}
); } // ── Offers (main export) ────────────────────────────────────────── window.Offers = function Offers({ prefillClient, onPrefillConsumed }) { const [offers, setOffers] = useState(() => window.SEED.offers || []); const [priceList, setPriceList] = useState(() => window.SEED.priceList || []); const [search, setSearch] = useState(''); const [filterStatus, setFilterStatus] = useState('all'); const [selected, setSelected] = useState(null); const [showNew, setShowNew] = useState(false); const [showPriceList, setShowPriceList] = useState(false); const [pendingClient, setPendingClient] = useState(null); useEffect(() => { if (prefillClient) { setPendingClient(prefillClient); setShowNew(true); onPrefillConsumed && onPrefillConsumed(); } }, [prefillClient]); const statuses = window.SEED.offerStatuses || []; const filtered = offers.filter(o => { const matchSearch = o.company.toLowerCase().includes(search.toLowerCase()) || o.id.toLowerCase().includes(search.toLowerCase()); const matchStatus = filterStatus === 'all' || o.status === filterStatus; return matchSearch && matchStatus; }); const totalValue = offers.reduce((s,o) => s + calcOffer(o).brutto, 0); const acceptedValue = offers.filter(o=>o.status==='accepted').reduce((s,o) => s + calcOffer(o).brutto, 0); const handleStatusChange = (id, status) => { setOffers(prev => prev.map(o => o.id===id ? { ...o, status } : o)); setSelected(prev => prev && prev.id===id ? { ...prev, status } : prev); }; return (
{[ { label:'Wszystkie oferty', val: String(offers.length), color:'#7C5CFF' }, { label:'Łączna wartość', val: fmtVal(totalValue), color:'#4DA3FF' }, { label:'Zaakceptowane', val: fmtVal(acceptedValue),color:'#22C55E' }, ].map((s,i) => (
{s.label}
{s.val}
))}
setSearch(e.target.value)} placeholder="Szukaj ofert…" style={{ width:'100%', padding:'9px 12px 9px 34px', borderRadius:10, border:'1px solid var(--bdr)', background:'var(--bg-input)', color:'var(--t1)', fontSize:13, outline:'none' }} />
{statuses.map(s => ( ))}
{['Numer','Klient','Data','Ważna do','Wartość brutto','Status'].map(h => ( ))} {filtered.map((o,i) => ( setSelected(o)} style={{ borderBottom: ie.currentTarget.style.background='var(--bg-hover)'} onMouseLeave={e=>e.currentTarget.style.background='transparent'} > ))}
{h}
{o.id} {o.company} {o.date} {o.validUntil} {fmtVal(calcOffer(o).brutto)}
{filtered.length === 0 && (
Brak ofert dla podanych filtrów
)}
{selected && setSelected(null)} onStatusChange={handleStatusChange} />} {showPriceList && setShowPriceList(false)} />} {showNew && { setShowNew(false); setPendingClient(null); }} onSave={o => setOffers(prev => [o, ...prev])} prefillClient={pendingClient} priceList={priceList} />}
); }; })();