// 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ć
{/* 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 (
);
})}
{/* 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.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 = (
);
return (
<>
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}
) : (
{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) => (
))}
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 => (
| {h} |
))}
{filtered.map((o,i) => (
setSelected(o)}
style={{ borderBottom: ie.currentTarget.style.background='var(--bg-hover)'}
onMouseLeave={e=>e.currentTarget.style.background='transparent'}
>
| {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}
/>}
);
};
})();