// Version: 2.02.00
// --- GLOBAL EDITOR COMPONENT ---
const { useState, useEffect, useRef, useMemo } = React;
// Import helpers from window scope
const { urlToBase64, cleanNum, formatPrice, calcFinal, cleanNumberInput } = window;
// Import Icons from window scope
const {
Layout, Plus, Search, ExternalLink, ChevronUp, Copy, ArrowDown, ChevronDown,
Trash2, ImageIcon, Clipboard, Ruler, Disk, UploadIcon, Cloud, Loader, FileText,
Download
} = window;
window.HeaderRow = ({ hasGlobalDiscount, debugClass, gridContainerClass }) => (
Фото
Описание
Ссылка
Кол
Цена
Сумма
{hasGlobalDiscount &&
Итого
}
);
window.Editor = ({ user, initialProject, useAquaPlaza, onBack }) => {
// ... [Logic remains same as v5.147, just Version bump]
const getAutoNumber = () => {
const today = new Date();
const dd = String(today.getDate()).padStart(2, '0');
const mm = String(today.getMonth() + 1).padStart(2, '0');
return `КП-${dd}/${mm}-`;
};
// ... [Rest of the code]
// NOTE: Copy the FULL logic from previous version 5.147 here,
// I am ensuring the structure is modular.
// Since I cannot output 1000 lines every time, I assume you have the content.
// If not, I can output the full file again.
// For this response, I will output the FULL file to be safe.
const defaults = { docInfo: { number: getAutoNumber(), date: new Date().toISOString().split('T')[0], recipient: '' }, items: [{ id: Date.now(), type: 'item', title: '', price: 0, quantity: 1, finalPrice: 0 }] };
const initData = initialProject?.data || defaults;
const [docInfo, setDocInfo] = useState(initData.docInfo);
const [items, setItems] = useState(initData.items);
const [currentProjectId, setCurrentProjectId] = useState(initialProject?.id || null);
const [isPdf, setIsPdf] = useState(false);
const [saving, setSaving] = useState(false);
const [loadingItems, setLoadingItems] = useState({});
const [fileColumns, setFileColumns] = useState([]);
const [showMappingModal, setShowMappingModal] = useState(false);
const [mapping, setMapping] = useState({ article: '', title: '', price: '', link: '' });
const [globalDiscount, setGlobalDiscount] = useState(initData.globalDiscount || 0);
const [showGlobalDiscountInput, setShowGlobalDiscountInput] = useState(initData.globalDiscount > 0);
const [showDebugGrid, setShowDebugGrid] = useState(false);
const [showPasteModal, setShowPasteModal] = useState(false);
const [pasteText, setPasteText] = useState("");
const [previewItems, setPreviewItems] = useState([]);
const [statusMessage, setStatusMessage] = useState(null);
const debugClass = showDebugGrid ? "border-r border-dashed border-red-400 bg-red-50/30" : "";
const textareaRefs = useRef({});
useEffect(() => { Object.values(textareaRefs.current).forEach(t => t && adjustHeight(t)); }, [items]);
const adjustHeight = (el) => { if (!el) return; el.style.height = 'auto'; el.style.height = el.scrollHeight + 'px'; };
const handleDateChange = (e) => { setDocInfo({ ...docInfo, date: e.target.value }); };
const getFormattedDateForPrint = () => { if (!docInfo.date) return ''; const parts = docInfo.date.split('-'); if (parts.length === 3) return `${parts[2]}.${parts[1]}.${parts[0]}`; return docInfo.date; };
const handleGlobalDiscountChange = (val) => { setGlobalDiscount(val); const numVal = parseFloat(val); setItems(prev => prev.map(item => { if (item.type === 'item') { const isDiscounted = numVal > 0; const final = calcFinal(item.price, item.quantity, isDiscounted, 'percent', val); return { ...item, isDiscounted, discountType: 'percent', discountValue: val, finalPrice: final }; } return item; })); };
const toggleGlobalDiscount = () => { if (showGlobalDiscountInput) { setShowGlobalDiscountInput(false); handleGlobalDiscountChange(0); } else { setShowGlobalDiscountInput(true); } };
const fetchProductByArticle = async (id, article) => {
if (!article) return;
setLoadingItems(prev => ({ ...prev, [id]: true }));
try {
let data = { found: false };
if (useAquaPlaza) {
const res = await fetch(`/api/proxy/search?article=${encodeURIComponent(article)}`);
const apiData = await res.json();
if (apiData.found) {
data = apiData;
if (data.link) { const sep = data.link.includes('?') ? '&' : '?'; data.link = `${data.link}${sep}utm_source=kp-gen`; }
}
}
if (!data.found) {
const res = await fetch(`/api/search?article=${encodeURIComponent(article)}`);
const localData = await res.json();
if (localData.found) data = localData;
}
if (data.found) {
const imgUrl = data.image ? (data.image.startsWith('http') ? data.image : 'https://' + data.image) : null;
const base64Img = imgUrl ? await urlToBase64(imgUrl) : null;
setItems(prev => prev.map(item => { if (item.id === id) { const updated = { ...item, title: data.title, price: data.price, link: data.link || item.link, image: base64Img || imgUrl }; updated.finalPrice = calcFinal(parseFloat(updated.price) || 0, parseFloat(updated.quantity) || 0, updated.isDiscounted, updated.discountType, updated.discountValue); return updated; } return item; }));
} else { alert('Товар не найден (ни в прайсе, ни на сайте)'); }
} catch (error) { alert("Ошибка поиска"); } finally { setLoadingItems(prev => ({ ...prev, [id]: false })); }
};
const enrichItemWithImage = async (id, article) => {
if (!article) return;
try {
const res = await fetch(`/api/proxy/search?article=${encodeURIComponent(article)}`);
const data = await res.json();
if (data.found) {
let link = data.link;
if (link) { const sep = link.includes('?') ? '&' : '?'; link = `${link}${sep}utm_source=kp-gen`; }
const imgUrl = data.image ? (data.image.startsWith('http') ? data.image : 'https://' + data.image) : null;
const base64Img = imgUrl ? await urlToBase64(imgUrl) : null;
setItems(prev => prev.map(it => { if (it.id === id) { return { ...it, image: base64Img || imgUrl, link: link || it.link }; } return it; }));
}
} catch (e) { }
};
const smartParse = (rawHTML, rawText) => {
const newItems = [];
if (rawHTML) {
const parser = new DOMParser();
const doc = parser.parseFromString(rawHTML, 'text/html');
const rows = doc.querySelectorAll('tr');
let colMap = { art: -1, title: -1, qty: -1, price: -1, sum: -1, unit: -1 };
let headerFound = false;
rows.forEach((row, rIndex) => {
const cells = Array.from(row.cells).map(c => c.textContent.trim());
if (cells.length < 3) return;
if (!headerFound) {
const txt = cells.join(' ').toLowerCase();
if (txt.includes('артикул') || txt.includes('код') || txt.includes('товар') || txt.includes('наименование')) {
let priceIndices = [], sumIndices = [];
cells.forEach((c, idx) => {
const t = c.toLowerCase();
if (t.includes('артикул') || t.includes('код')) colMap.art = idx;
else if (t.includes('товар') || t.includes('наименование') || t.includes('работы')) colMap.title = idx;
else if (t.includes('кол') && !t.includes('цена')) colMap.qty = idx;
else if (t.includes('ед')) colMap.unit = idx;
else if (t.includes('цена')) priceIndices.push(idx);
else if (t.includes('сумма') && !t.includes('без')) sumIndices.push(idx);
});
if (priceIndices.length > 0) colMap.price = priceIndices[0];
if (sumIndices.length > 0) colMap.sum = sumIndices[sumIndices.length - 1];
if (colMap.title !== -1) { headerFound = true; return; }
}
}
if (!headerFound && rIndex > 5) { colMap = { art: 2, title: 3, qty: 4, unit: 5, price: 6, sum: 7 }; headerFound = true; }
if (!headerFound) return;
let shift = 0;
if (colMap.unit === -1 && colMap.qty !== -1) {
const possibleUnit = (cells[colMap.qty + 1] || '').toLowerCase();
if (['шт', 'компл', 'упак', 'м2', 'пог.м', 'л'].some(u => possibleUnit.includes(u))) shift = 1;
}
const getVal = (colType) => {
let idx = colMap[colType];
if (idx === -1) return '';
if (shift > 0 && colMap.qty !== -1 && idx > colMap.qty) return cells[idx + shift] || '';
return cells[idx] || '';
};
const title = getVal('title');
if (!title || title.toLowerCase().includes('итого')) return;
let qty = cleanNum(getVal('qty'));
let basePrice = cleanNum(getVal('price'));
let finalSum = cleanNum(getVal('sum'));
if (qty === 0) qty = 1;
if (basePrice === 0 && finalSum > 0) basePrice = finalSum / qty;
if (finalSum === 0 && basePrice > 0) finalSum = basePrice * qty;
let isDiscounted = false, discountValue = '';
let finalPrice = finalSum > 0 ? finalSum : (basePrice * qty);
const calculatedBaseSum = basePrice * qty;
if (calculatedBaseSum > (finalSum + 2)) { isDiscounted = true; const diff = calculatedBaseSum - finalSum; const percent = (diff / calculatedBaseSum) * 100; discountValue = Math.round(percent); }
if (basePrice > 0 || finalSum > 0) {
newItems.push({ id: Date.now() + Math.random(), type: 'item', article: getVal('art'), title: title, price: basePrice, quantity: qty, unit: (colMap.unit !== -1 ? getVal('unit') : (shift ? cells[colMap.qty + 1] : 'шт')), finalPrice: finalPrice, isDiscounted: isDiscounted, discountType: 'percent', discountValue: isDiscounted ? discountValue : '', image: null, link: '' });
}
});
}
if (newItems.length === 0 && rawText) {
const lines = rawText.trim().split('\n');
lines.forEach(line => {
const row = line.split('\t');
if (row.length < 5) return;
const cleanNum = (s) => parseFloat(s?.replace(/[\s\u00A0]/g, '').replace(',', '.') || '0');
const title = row[3]?.trim();
if (!title) return;
const qty = cleanNum(row[4]);
const basePrice = cleanNum(row[6]);
const finalSum = cleanNum(row[row.length - 1]);
const q = qty || 1;
if (basePrice > 0 || finalSum > 0) {
const fSum = finalSum > 0 ? finalSum : basePrice * q;
const bPrice = basePrice > 0 ? basePrice : fSum / q;
let isDisc = (bPrice * q) > (fSum + 1);
let discVal = '';
if (isDisc) discVal = Math.round(((bPrice * q - fSum) / (bPrice * q)) * 100);
newItems.push({ id: Date.now() + Math.random(), type: 'item', article: row[2]?.trim(), title: title, price: bPrice, quantity: q, unit: row[5]?.trim() || 'шт', finalPrice: fSum, isDiscounted: isDisc, discountType: 'percent', discountValue: discVal, image: null, link: '' });
}
});
}
return newItems;
};
const handlePasteBox = (e) => {
const html = e.clipboardData.getData('text/html');
const text = e.clipboardData.getData('text/plain');
const parsed = smartParse(html, text);
if (parsed.length > 0) { setPreviewItems(parsed); e.preventDefault(); }
};
const confirmPaste = async () => {
if (previewItems.length === 0) return;
setItems(prev => [...prev.filter(x => x.title || x.price), ...previewItems]);
setShowPasteModal(false);
const itemsToProcess = [...previewItems];
setPreviewItems([]);
setStatusMessage(`Загрузка фото: 0 из ${itemsToProcess.length}...`);
for (let i = 0; i < itemsToProcess.length; i++) {
const it = itemsToProcess[i];
if (it.article) {
setStatusMessage(`Загрузка фото: ${i + 1} из ${itemsToProcess.length}...`);
await enrichItemWithImage(it.id, it.article);
await new Promise(r => setTimeout(r, 1200));
}
}
setStatusMessage(null);
alert(`Готово! Добавлено ${itemsToProcess.length} товаров.`);
};
const addSection = () => setItems([...items, { id: Date.now(), type: 'section', title: 'НОВЫЙ РАЗДЕЛ' }]);
const addItem = () => setItems([...items, { id: Date.now(), type: 'item', title: '', price: 0, quantity: 1, finalPrice: 0, article: '', isDiscounted: false, discountType: 'fixed', discountValue: '' }]);
const handleLocalSave = () => { const data = JSON.stringify({ docInfo, items, globalDiscount }, null, 2); const blob = new Blob([data], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `KP-${docInfo.number}.json`; a.click(); URL.revokeObjectURL(url); };
const handleLocalLoad = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (ev) => { try { const data = JSON.parse(ev.target.result); if (data.docInfo) setDocInfo(data.docInfo); if (data.items) setItems(data.items); if (data.globalDiscount !== undefined) { setGlobalDiscount(data.globalDiscount); setShowGlobalDiscountInput(data.globalDiscount > 0); } alert("Проект загружен!"); } catch (err) { alert("Ошибка"); } }; reader.readAsText(file); e.target.value = null; };
const handleCloudSave = async () => {
if (!user || !user.id) { alert("Войдите в систему"); return; }
setSaving(true);
const pName = docInfo.recipient ? `${docInfo.number} Для ${docInfo.recipient}` : docInfo.number;
let projectIdToSave = currentProjectId;
try {
const checkRes = await fetch(`/api/projects/${user.id}?t=${Date.now()}`);
const checkData = await checkRes.json();
if (Array.isArray(checkData)) {
const existingProject = checkData.find(p => p.name === pName);
if (existingProject) {
if (!projectIdToSave || projectIdToSave !== existingProject.id) {
const confirmOverwrite = confirm(`Файл "${pName}" уже существует. Перезаписать его?`);
if (confirmOverwrite) { projectIdToSave = existingProject.id; } else { setSaving(false); return; }
}
}
}
} catch (e) { }
const projectData = { user_id: user.id, id: projectIdToSave || 0, name: pName, data: JSON.stringify({ docInfo, items, globalDiscount }) };
try {
const res = await fetch('/api/projects', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(projectData) });
const data = await res.json();
if (data.status === 'created' || data.status === 'updated') {
setCurrentProjectId(data.id);
alert("☁️ Сохранено!");
} else if (res.status === 402) {
alert("⛔ " + data.message);
} else {
alert("Ошибка сохранения: " + data.detail);
}
} catch (e) { alert("Ошибка"); } finally { setSaving(false); }
};
const handleUploadPrice = async (e) => { const file = e.target.files[0]; if (!file) return; const formData = new FormData(); formData.append('file', file); e.target.value = null; try { const res = await fetch('/api/upload', { method: 'POST', body: formData }); const data = await res.json(); if (data.status === 'success') { setFileColumns(data.columns); const guess = { article: '', title: '', price: '', link: '' }; data.columns.forEach(col => { const c = col.toLowerCase(); if (c.includes('арт') || c.includes('code')) guess.article = col; else if (c.includes('назв') || c.includes('name')) guess.title = col; else if (c.includes('цен') || c.includes('price')) guess.price = col; else if (c.includes('ссыл') || c.includes('link')) guess.link = col; }); setMapping(guess); setShowMappingModal(true); } else { alert("Ошибка: " + data.detail); } } catch (err) { alert('Ошибка загрузки.'); } };
const saveColumnMapping = async () => { if (!mapping.article || !mapping.price) { alert("Выберите Артикул и Цену."); return; } try { const res = await fetch('/api/mapping', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(mapping) }); const data = await res.json(); if (data.status === 'ok') { setShowMappingModal(false); alert("✅ Настройки сохранены!"); } } catch (err) { alert("Ошибка сохранения"); } };
const handleItemChange = (id, f, v) => setItems(prev => prev.map(i => { if (i.id === id) { const u = { ...i, [f]: v }; if (i.type === 'item') { const p = parseFloat(u.price) || 0; const q = parseFloat(u.quantity) || 0; u.finalPrice = calcFinal(p, q, u.isDiscounted, u.discountType, u.discountValue); } return u; } return i; }));
const handleArticleBlur = (id, artValue, currentTitle) => { if (artValue && !currentTitle) { handleItemChange(id, 'title', artValue); } };
const deleteItem = (id) => setItems(items.filter(i => i.id !== id));
const duplicateItem = (id) => { const idx = items.findIndex(i => i.id === id); if (idx === -1) return; const cp = { ...items[idx], id: Date.now() }; const ni = [...items]; ni.splice(idx + 1, 0, cp); setItems(ni); };
const duplicateItemToBottom = (id) => { const idx = items.findIndex(i => i.id === id); if (idx === -1) return; const cp = { ...items[idx], id: Date.now() }; setItems([...items, cp]); };
const moveItem = (idx, dir) => { const ni = [...items]; if (dir === -1 && idx > 0) { [ni[idx], ni[idx - 1]] = [ni[idx - 1], ni[idx]]; } else if (dir === 1 && idx < ni.length - 1) { [ni[idx], ni[idx + 1]] = [ni[idx + 1], ni[idx]]; } setItems(ni); };
const formatPrice = (p) => new Intl.NumberFormat('ru-RU').format(Math.round(p || 0));
const totals = useMemo(() => { const baseTotal = items.reduce((acc, i) => i.type === 'item' ? acc + (i.price * i.quantity) : acc, 0); const finalTotal = items.reduce((acc, i) => i.type === 'item' ? acc + i.finalPrice : acc, 0); const disc = baseTotal - finalTotal; return { t: baseTotal, disc: disc, f: finalTotal }; }, [items]);
const hasGlobalDiscount = items.some(i => i.isDiscounted);
const handlePdf = async () => {
setIsPdf(true); await new Promise(r => setTimeout(r, 100)); const el = document.getElementById('print-area'); const imgs = el.querySelectorAll('.pdf-no-border'); imgs.forEach(i => i.style.border = 'none');
for (const item of items) { if (item.image && item.image.startsWith('http')) { const b64 = await urlToBase64(item.image); if (b64) { const imgEls = el.querySelectorAll(`img[src="${item.image}"]`); imgEls.forEach(img => img.src = b64); } } }
window.html2pdf().set({ margin: 0, filename: `${docInfo.number}.pdf`, image: { type: 'jpeg', quality: 0.98 }, html2canvas: { scale: 2, useCORS: true, scrollY: 0, backgroundColor: '#ffffff' }, jsPDF: { format: 'a4', orientation: 'portrait', unit: 'mm' } }).from(el).save().then(() => { setIsPdf(false); imgs.forEach(i => i.style.border = ''); });
};
const handleImageUpload = (id, e) => { const f = e.target.files[0]; if (f) { const r = new FileReader(); r.onload = (ev) => handleItemChange(id, 'image', ev.target.result); r.readAsDataURL(f); } };
const handleImageDrop = (e, id) => { e.preventDefault(); e.stopPropagation(); const file = e.dataTransfer.files[0]; if (file && file.type.startsWith('image/')) { const reader = new FileReader(); reader.onload = (ev) => handleItemChange(id, 'image', ev.target.result); reader.readAsDataURL(file); } };
const handleImagePaste = (e, id) => { const items = e.clipboardData.items; for (let i = 0; i < items.length; i++) { if (items[i].type.indexOf('image') !== -1) { e.preventDefault(); const file = items[i].getAsFile(); const reader = new FileReader(); reader.onload = (ev) => handleItemChange(id, 'image', ev.target.result); reader.readAsDataURL(file); if (e.target.tagName === 'INPUT') e.target.value = ''; break; } } };
const handleDescDrop = (e, id) => { e.preventDefault(); e.stopPropagation(); const text = e.dataTransfer.getData('text'); if (text) handleItemChange(id, 'title', text); };
const handlePriceDrop = (e, id) => { e.preventDefault(); e.stopPropagation(); const text = e.dataTransfer.getData('text'); if (!text) return; let clean = text.replace(/[^\d.,]/g, '').replace(',', '.'); const val = parseFloat(clean); if (!isNaN(val)) { handleItemChange(id, 'price', val); } };
const handleCopyTo1C = () => {
const canExport = user && (user.is_vip || user.premium || user.role === 'admin');
if (!canExport) { alert("Экспорт в 1С доступен в PRO версии"); return; }
const lines = items.filter(i => i.type === 'item').map(i => {
let p = parseFloat(i.price); if (isNaN(p)) p = 0;
let q = parseFloat(i.quantity); if (isNaN(q)) q = 0;
if (i.isDiscounted && q > 0) { let fp = parseFloat(i.finalPrice); if (isNaN(fp)) fp = 0; p = fp / q; }
const fmt = (n) => n.toFixed(2).replace('.', ',');
const cleanArt = (i.article || '').replace(/\t/g, ' ');
const cleanTitle = (i.title || '').replace(/\t/g, ' ');
return `\t\t${cleanArt}\t${cleanTitle}\t${fmt(q)}\t${fmt(p)}`;
});
if (lines.length === 0) { alert("Нет товаров для экспорта"); return; }
navigator.clipboard.writeText(lines.join('\n')).then(() => alert("📋 Скопировано! Вставьте в 1С (Ctrl+V)")).catch(() => alert("Ошибка доступа к буферу обмена"));
};
const isPro = user?.premium || user?.is_vip || user?.role === 'admin';
const isFree = user?.tier === 'free' && !user?.is_vip;
const can1C = user?.tier === 'speed' || user?.is_vip || user?.tier === 'vip' || user?.role === 'admin';
const hasWatermark = user?.tier === 'free' && !user?.is_vip;
const gridContainerClass = `kp-grid-container ${!hasGlobalDiscount ? 'no-discount' : ''}`;
return (
{statusMessage &&
{statusMessage}
}
{showMappingModal && (
Настройка колонок Excel
Артикул setMapping({ ...mapping, article: e.target.value })} className="w-full p-2 border rounded">-- {fileColumns.map(c => {c} )}
Название setMapping({ ...mapping, title: e.target.value })} className="w-full p-2 border rounded">-- {fileColumns.map(c => {c} )}
Цена setMapping({ ...mapping, price: e.target.value })} className="w-full p-2 border rounded">-- {fileColumns.map(c => {c} )}
Ссылка setMapping({ ...mapping, link: e.target.value })} className="w-full p-2 border rounded">-- {fileColumns.map(c => {c} )}
Сохранить
)}
{showPasteModal && (
Импорт из 1С Нажмите на область ниже и нажмите Ctrl+V .
Добавить {previewItems.length > 0 ? previewItems.length : ''}
Вставьте таблицу сюда (Ctrl+V)
{previewItems.length > 0 ? (
Артикул Название Кол-во Базовая Цена Скидка Сумма
{previewItems.map((it, idx) => ({it.article} {it.title} {it.quantity} {formatPrice(it.price)} {it.isDiscounted ? `-${it.discountValue}%` : '-'} {formatPrice(it.finalPrice)} ))}
) : (
Нет данных для просмотра
)}
{ setShowPasteModal(false); setPreviewItems([]); }} className="px-6 py-2 text-gray-500 hover:bg-gray-100 rounded-lg font-bold">Отмена Добавить {previewItems.length} товаров
)}
{!isPdf && (
)}
{/* --- FLOATING BUTTONS --- */}
{!isPdf && (
)}
{hasWatermark &&
}
{!isPdf && (
Добавить Товар
Добавить Раздел
)}
{items.map((item, index) => {
const isFirstItem = index === 0;
const isAfterSection = index > 0 && items[index - 1].type === 'section';
const showHeader = item.type === 'item' && (isFirstItem || isAfterSection);
if (item.type === 'section') {
return (
{!isPdf ? (
handleItemChange(item.id, 'title', e.target.value)} className="text-2xl md:text-4xl font-black italic text-[#2d3440] uppercase bg-transparent outline-none w-full placeholder:text-gray-300" placeholder="НАЗВАНИЕ РАЗДЕЛА" />
) : (
{item.title}
)}
{!isPdf && (
moveItem(index, -1)} disabled={index === 0} title="Вверх" className="hover:text-blue-600 text-gray-400 p-1">
moveItem(index, 1)} disabled={index === items.length - 1} title="Вниз" className="hover:text-blue-600 text-gray-400 p-1">
deleteItem(item.id)} className="text-red-400 hover:text-red-600 p-1" title="Удалить">
)}
);
}
return (
{showHeader && }
{/* IMAGE */}
!isPdf && e.preventDefault()} onDrop={e => !isPdf && handleImageDrop(e, item.id)}>
{item.image ?
: !isPdf &&
}
{!isPdf &&
handleImageUpload(item.id, e)} />}
{!isPdf && (
handleImagePaste(e, item.id)} className="w-full text-[9px] text-center bg-gray-50 border border-gray-200 rounded outline-none focus:bg-white focus:border-blue-400 px-1" />
)}
{item.image &&
}
{/* TITLE / DESC */}
{/* LINK */}
{!isPdf ? (
<>
handleItemChange(item.id, 'link', e.target.value)} placeholder="URL" className="bg-[#f0f0f0] px-2 py-1 rounded-md text-[10px] font-bold text-gray-600 w-full text-center outline-none" />
{item.link &&
}
>
) : (
)}
{/* QTY */}
{!isPdf ? handleItemChange(item.id, 'quantity', e.target.value)} className="w-full text-right bg-transparent outline-none" /> : item.quantity}
{/* PRICE */}
{!isPdf ? handleItemChange(item.id, 'price', e.target.value)} onFocus={e => e.target.select()} onDragOver={e => { e.preventDefault(); e.stopPropagation(); e.currentTarget.classList.add('bg-blue-100') }} onDragLeave={e => e.currentTarget.classList.remove('bg-blue-100')} onDrop={e => handlePriceDrop(e, item.id)} className="w-full text-right bg-transparent outline-none rounded transition-colors" /> : formatPrice(item.price)}
{/* SUM */}
{hasGlobalDiscount && item.isDiscounted && parseFloat(item.discountValue) > 0 ? (
{formatPrice(item.price * item.quantity)}
) : (
{formatPrice(item.price * item.quantity)}
)}
{/* FINAL TOTAL */}
{hasGlobalDiscount &&
{item.isDiscounted ? formatPrice(item.finalPrice) : ''}
}
{/* ITEM ACTIONS */}
{!isPdf && (
moveItem(index, -1)} disabled={index === 0} title="Вверх" className="hover:text-blue-600 text-gray-400 p-1">
duplicateItem(item.id)} className="text-blue-500 hover:text-blue-700 p-1" title="Копировать рядом">
duplicateItemToBottom(item.id)} className="text-green-600 hover:text-green-800 p-1" title="Копировать в конец списка">
moveItem(index, 1)} disabled={index === items.length - 1} title="Вниз" className="hover:text-blue-600 text-gray-400 p-1">
deleteItem(item.id)} className="text-red-400 hover:text-red-600 p-1" title="Удалить">
)}
);
})}
Сумма: {formatPrice(totals.t)} ₽
{totals.disc > 0 && (
Скидка на заказ: -{formatPrice(totals.disc)} ₽
)}
Итого: {formatPrice(totals.f)} ₽
{!isPdf && (
{!showGlobalDiscountInput ? (
% Скидка на проект
) : (
Скидка (%):
handleGlobalDiscountChange(e.target.value)} className="w-16 border border-gray-300 rounded px-2 py-1 text-right font-bold text-red-500 outline-none focus:border-red-500" placeholder="0" autoFocus />
×
)}
)}
{!isPdf && (
)}
);
};