// Version: 2.11.57 (Local Template Requisites) const { useState, useEffect, useRef, useMemo } = React; const { urlToBase64, cleanNum, formatPrice, calcFinal, cleanNumberInput } = window; const getSafeIcon = (name) => window[name] || (({size, className}) => ); const Layout = getSafeIcon('Layout'); const Plus = getSafeIcon('Plus'); const Search = getSafeIcon('Search'); const ExternalLink = getSafeIcon('ExternalLink'); const ChevronUp = getSafeIcon('ChevronUp'); const Copy = getSafeIcon('Copy'); const ArrowDown = getSafeIcon('ArrowDown'); const ChevronDown = getSafeIcon('ChevronDown'); const Trash2 = getSafeIcon('Trash2'); const ImageIcon = getSafeIcon('ImageIcon'); const Clipboard = getSafeIcon('Clipboard'); const Ruler = getSafeIcon('Ruler'); const Save = getSafeIcon('Save'); const UploadCloud = getSafeIcon('UploadCloud'); const Cloud = getSafeIcon('Cloud'); const Loader = getSafeIcon('Loader'); const FileText = getSafeIcon('FileText'); const Download = getSafeIcon('Download'); const Send = getSafeIcon('Send'); const CheckCircle = getSafeIcon('CheckCircle'); const AlertTriangle = getSafeIcon('AlertTriangle'); const SendIcon = window.Send || window.Download || getSafeIcon('Send'); const HeaderRow = ({ hasGlobalDiscount, debugClass, gridContainerClass }) => ( ); window.Editor = ({ user, initialProject, useAquaPlaza, onBack }) => { 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}-`; }; const getSavedSeller = () => { try { const saved = localStorage.getItem('savedSellerRequisites'); if (saved) return JSON.parse(saved); } catch(e) {} return { name: '', inn: '', kpp: '', ogrn: '', account: '', bank: '', bik: '', corr: '', phone: '', site: '', email: '', directorTitle: '', directorName: '', managerName: '', managerPhone: '' }; }; 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, isDiscounted: false, discountType: 'percent', discountValue: '' }] }; const initData = initialProject?.data || defaults; const [docInfo, setDocInfo] = useState(initData.docInfo); const [items, setItems] = useState(initData.items); // Обратная совместимость: если старое КП, проверяем, были ли заполнены полные реквизиты const [showBaseRequisites, setShowBaseRequisites] = useState(() => { if (initData.showBaseRequisites !== undefined) return initData.showBaseRequisites; const hasData = initData.requisites && (initData.requisites.seller.inn || initData.requisites.buyer.company); return initData.showRequisites && !hasData ? true : false; }); const [showFullRequisites, setShowFullRequisites] = useState(() => { if (initData.showFullRequisites !== undefined) return initData.showFullRequisites; const hasData = initData.requisites && (initData.requisites.seller.inn || initData.requisites.buyer.company); return initData.showRequisites && hasData ? true : false; }); const [requisites, setRequisites] = useState(() => { if (initData.requisites) return initData.requisites; return { seller: getSavedSeller(), buyer: { targetTitle: '', company: '', targetName: '' } }; }); const [currentProjectId, setCurrentProjectId] = useState(initialProject?.id || null); const [isPdf, setIsPdf] = useState(false); const [isMobilePdf, setIsMobilePdf] = useState(false); const [saving, setSaving] = useState(false); const [sendingTg, setSendingTg] = useState(false); const [showTgMenu, setShowTgMenu] = useState(false); const [showPdfMenu, setShowPdfMenu] = 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 [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const [toast, setToast] = useState({ show: false, message: '', type: 'success' }); const isInitialMount = useRef(true); const textareaRefs = useRef({}); const debugClass = showDebugGrid ? "border-r border-dashed border-red-400 bg-red-50/30" : ""; const tgMenuRef = useRef(null); const pdfMenuRef = useRef(null); const showToast = (message, type = 'success') => { setToast({ show: true, message, type }); setTimeout(() => setToast({ show: false, message: '', type: 'success' }), 4000); }; useEffect(() => { if (window.loadPdfLibrary) { window.loadPdfLibrary().catch(e => console.error("Ошибка загрузки PDF", e)); } }, []); useEffect(() => { const handleClickOutside = (event) => { if (tgMenuRef.current && !tgMenuRef.current.contains(event.target)) setShowTgMenu(false); if (pdfMenuRef.current && !pdfMenuRef.current.contains(event.target)) setShowPdfMenu(false); }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); useEffect(() => { if (isInitialMount.current) isInitialMount.current = false; else setHasUnsavedChanges(true); }, [items, docInfo, globalDiscount, showBaseRequisites, showFullRequisites, requisites]); useEffect(() => { Object.values(textareaRefs.current).forEach(t => t && adjustHeight(t)); }, [items, isPdf, isMobilePdf]); 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 handleBackClick = () => { if (hasUnsavedChanges) { if (!confirm("У вас есть несохраненные изменения. Выйти без сохранения?")) return; } onBack(); }; const setReqField = (section, field, value) => { setRequisites(prev => ({ ...prev, [section]: { ...prev[section], [field]: value } })); }; const saveSellerToLocal = () => { try { localStorage.setItem('savedSellerRequisites', JSON.stringify(requisites.seller)); showToast('Реквизиты продавца успешно сохранены как шаблон!'); } catch (e) { showToast('Ошибка сохранения шаблона', 'error'); } }; 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) { let imgUrl = data.image; if (imgUrl) { if (imgUrl.startsWith('/')) imgUrl = 'https://aquaplaza-online.ru' + imgUrl; else if (!imgUrl.startsWith('http')) imgUrl = 'https://' + imgUrl; if (window.urlToCompressedBase64) { const compressed = await window.urlToCompressedBase64(imgUrl, 300, 0.7); if (compressed) imgUrl = compressed; } } setItems(prev => prev.map(item => { if (item.id === id) { const updated = { ...item, title: data.title, price: data.price, link: data.link || item.link, image: imgUrl }; updated.finalPrice = calcFinal(parseFloat(updated.price) || 0, parseFloat(updated.quantity) || 0, updated.isDiscounted, updated.discountType, updated.discountValue); return updated; } return item; })); } else { showToast('Товар не найден', 'error'); } } catch (error) { showToast("Ошибка поиска", 'error'); } 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`; } let imgUrl = data.image; if (imgUrl) { if (imgUrl.startsWith('/')) imgUrl = 'https://aquaplaza-online.ru' + imgUrl; else if (!imgUrl.startsWith('http')) imgUrl = 'https://' + imgUrl; if (window.urlToCompressedBase64) { const compressed = await window.urlToCompressedBase64(imgUrl, 300, 0.7); if (compressed) imgUrl = compressed; } } setItems(prev => prev.map(it => { if (it.id === id) { return { ...it, image: 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); showToast(`Готово! Добавлено ${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: 'percent', discountValue: '' }]); const handleLocalSave = () => { const data = JSON.stringify({ docInfo, items, globalDiscount, showBaseRequisites, showFullRequisites, requisites }, 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); setHasUnsavedChanges(false); }; 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); } if (data.showBaseRequisites !== undefined) setShowBaseRequisites(data.showBaseRequisites); if (data.showFullRequisites !== undefined) setShowFullRequisites(data.showFullRequisites); if (data.showBaseRequisites === undefined && data.showFullRequisites === undefined && data.showRequisites !== undefined) { const hasData = data.requisites && (data.requisites.seller.inn || data.requisites.buyer.company); if (hasData) setShowFullRequisites(data.showRequisites); else setShowBaseRequisites(data.showRequisites); } if (data.requisites) setRequisites(data.requisites); showToast("Проект загружен!"); setHasUnsavedChanges(false); } catch (err) { showToast("Ошибка загрузки", 'error'); } }; reader.readAsText(file); e.target.value = null; }; const handleCloudSave = async () => { if (!user || !user.id) { showToast("Войдите в систему", 'error'); 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()}`); if (checkRes.ok) { 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) { console.error("Error checking existing projects:", e); } let itemsToSave = items; let hasCompressed = false; if (window.compressImage) { itemsToSave = await Promise.all(items.map(async (it) => { if (it.image && it.image.startsWith('data:image/') && it.image.length > 50000) { try { const compressed = await window.compressImage(it.image, 300, 0.7); if (compressed !== it.image) hasCompressed = true; return { ...it, image: compressed }; } catch (e) { return it; } } return it; })); } let projectData = { user_id: user.id, id: projectIdToSave || 0, name: pName, data: JSON.stringify({ docInfo, items: itemsToSave, globalDiscount, showBaseRequisites, showFullRequisites, requisites }) }; let payloadSize = new Blob([JSON.stringify(projectData)]).size; if (payloadSize > 900 * 1024) { itemsToSave = await Promise.all(itemsToSave.map(async (it) => { if (it.image && it.image.startsWith('data:image/')) { try { const compressed = await window.compressImage(it.image, 150, 0.5); if (compressed !== it.image) hasCompressed = true; return { ...it, image: compressed }; } catch (e) { return it; } } return it; })); projectData.data = JSON.stringify({ docInfo, items: itemsToSave, globalDiscount, showBaseRequisites, showFullRequisites, requisites }); payloadSize = new Blob([JSON.stringify(projectData)]).size; } if (hasCompressed) setItems(itemsToSave); if (payloadSize > 1024 * 1024) { const mbSize = (payloadSize / (1024 * 1024)).toFixed(2); showToast(`Файл слишком большой (${mbSize} МБ). Сервер принимает до 1 МБ. Пожалуйста, удалите несколько фото.`, 'error'); setSaving(false); return; } try { const res = await fetch('/api/projects', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(projectData) }); if (!res.ok) { if (res.status == 413) throw new Error("Файл отклонен сервером (Error 413: Слишком большой)."); throw new Error(`HTTP Error: ${res.status}`); } const data = await res.json(); if (data.status === 'created' || data.status === 'updated') { setCurrentProjectId(data.id); setHasUnsavedChanges(false); showToast("Проект сохранен в облако!"); } else if (res.status === 402) { showToast("⛔ " + data.message, 'error'); } else { showToast("Ошибка сохранения: " + data.detail, 'error'); } } catch (e) { let msg = e.message || "Неизвестная ошибка"; if (msg.includes("Failed to fetch")) msg = "Сервер сбросил соединение. Возможно, файл всё ещё слишком велик."; showToast(msg, 'error'); } finally { setSaving(false); } }; 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 toggleDiscount = (id, currentIsDiscounted) => { setItems(prev => prev.map(i => { if (i.id === id) { const isNowDiscounted = !currentIsDiscounted; const u = { ...i, isDiscounted: isNowDiscounted }; if (isNowDiscounted) { u.discountType = 'percent'; if (i.discountType === 'fixed') u.discountValue = ''; } 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 formatPriceSafe = (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 processImagesForPdf = async (el) => { const imgEls = Array.from(el.querySelectorAll('img')).filter(img => { const src = img.getAttribute('src'); return src && src.startsWith('http'); }); if (imgEls.length > 0) { showToast("Подготовка старых фото для PDF...", "success"); } const promises = imgEls.map(async (img) => { try { const originalSrc = img.getAttribute('src'); img.setAttribute('data-original-src', originalSrc); if (window.urlToCompressedBase64) { const b64 = await window.urlToCompressedBase64(originalSrc, 300, 0.8); if (b64) { await new Promise(resolve => { const timeout = setTimeout(resolve, 3000); img.onload = () => { clearTimeout(timeout); resolve(); }; img.onerror = () => { clearTimeout(timeout); resolve(); }; img.src = b64; }); return; } } const proxyUrl = `/api/proxy/image?url=${encodeURIComponent(originalSrc)}`; const res = await fetch(proxyUrl); if (!res.ok) throw new Error('Proxy fetch failed'); const blob = await res.blob(); const b64_fallback = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(blob); }); await new Promise(resolve => { const timeout = setTimeout(resolve, 3000); img.onload = () => { clearTimeout(timeout); resolve(); }; img.onerror = () => { clearTimeout(timeout); resolve(); }; img.src = b64_fallback; }); } catch(e) { console.error("Image processing error:", e); } }); await Promise.all(promises); await new Promise(r => setTimeout(r, 300)); }; const revertImagesAfterPdf = (el) => { const imgEls = el.querySelectorAll('img[data-original-src]'); imgEls.forEach(img => { img.src = img.getAttribute('data-original-src'); img.removeAttribute('data-original-src'); }); }; 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'); try { await processImagesForPdf(el); await 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(); } catch (e) { console.error(e); showToast("Ошибка при сохранении PDF", "error"); } finally { revertImagesAfterPdf(el); setIsPdf(false); imgs.forEach(i => i.style.border = ''); } }; const handleMobilePdf = async () => { const currentScrollY = window.scrollY; window.scrollTo(0, 0); setIsMobilePdf(true); await new Promise(r => setTimeout(r, 400)); const el = document.getElementById('print-area'); try { await processImagesForPdf(el); await window.html2pdf().set({ margin: [5, 0, 5, 0], filename: `${docInfo.number}_mobile.pdf`, image: { type: 'jpeg', quality: 0.98 }, html2canvas: { scale: 2, useCORS: true, scrollY: 0, backgroundColor: '#ffffff' }, jsPDF: { format: [110, 297], orientation: 'portrait', unit: 'mm' } }).from(el).save(); } catch (e) { console.error("Mobile PDF Error: ", e); showToast("Ошибка при генерации мобильного PDF", 'error'); } finally { revertImagesAfterPdf(el); setIsMobilePdf(false); setTimeout(() => window.scrollTo(0, currentScrollY), 100); } }; const handleSendToTelegram = (type = 'standard') => { if (!user) return; showToast("Генерация и отправка в Telegram (~30 сек). Можно продолжать работу.", "success"); setSendingTg(true); setShowTgMenu(false); const currentScrollY = window.scrollY; if (type === 'mobile') { window.scrollTo(0, 0); setIsMobilePdf(true); } else { setIsPdf(true); } setTimeout(async () => { const el = document.getElementById('print-area'); const imgs = el.querySelectorAll('.pdf-no-border'); imgs.forEach(i => i.style.border = 'none'); try { await processImagesForPdf(el); const pdfOptions = type === 'mobile' ? { margin: [5, 0, 5, 0], image: { type: 'jpeg', quality: 0.98 }, html2canvas: { scale: 2, useCORS: true, scrollY: 0, backgroundColor: '#ffffff' }, jsPDF: { format: [110, 297], orientation: 'portrait', unit: 'mm' } } : { margin: 0, image: { type: 'jpeg', quality: 0.98 }, html2canvas: { scale: 2, useCORS: true, scrollY: 0, backgroundColor: '#ffffff' }, jsPDF: { format: 'a4', orientation: 'portrait', unit: 'mm' } }; const blob = await window.html2pdf().set(pdfOptions).from(el).output('blob'); if (blob.size === 0) throw new Error("Generated PDF is empty"); revertImagesAfterPdf(el); imgs.forEach(i => i.style.border = ''); if (type === 'mobile') { setIsMobilePdf(false); setTimeout(() => window.scrollTo(0, currentScrollY), 100); } else { setIsPdf(false); } const formData = new FormData(); formData.append('file', blob, `${docInfo.number}${type === 'mobile' ? '_mobile' : ''}.pdf`); formData.append('user_id', user.id); const res = await fetch('/api/send-pdf', { method: 'POST', body: formData }); let errData = null; if (!res.ok) { try { errData = await res.json(); } catch (parseError) { throw new Error("Ошибка сервера: сервис временно недоступен или размер файла превышает лимит хостинга."); } } if (res.ok) { showToast("Файл успешно доставлен в Telegram!", "success"); } else { if (res.status === 400 && errData?.detail && errData.detail.includes("Telegram ID missing")) { showToast("Бот не знает ваш ID. Нажмите /start в боте.", 'error'); } else { showToast("Ошибка отправки: " + (errData?.detail || `Код ${res.status}`), 'error'); } } } catch (e) { console.error("CRITICAL SEND ERROR:", e); showToast(e.message || "Произошла неизвестная ошибка при отправке", 'error'); revertImagesAfterPdf(el); imgs.forEach(i => i.style.border = ''); if (type === 'mobile') { setIsMobilePdf(false); setTimeout(() => window.scrollTo(0, currentScrollY), 100); } else { setIsPdf(false); } } finally { setSendingTg(false); } }, type === 'mobile' ? 400 : 200); }; const processImageFile = (id, file) => { if (!file || !file.type.startsWith('image/')) return; const reader = new FileReader(); reader.onload = async (ev) => { let res = ev.target.result; if (window.compressImage) { res = await window.compressImage(res, 300, 0.7); } handleItemChange(id, 'image', res); }; reader.readAsDataURL(file); }; const handleImageUpload = (id, e) => processImageFile(id, e.target.files[0]); const handleImageDrop = (e, id) => { e.preventDefault(); e.stopPropagation(); processImageFile(id, e.dataTransfer.files[0]); }; 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(); processImageFile(id, items[i].getAsFile()); 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 isManagerOrHigher = user?.is_vip || user?.tier === 'speed' || user?.tier === 'manager'; const isSpeedOrVip = user?.is_vip || user?.tier === 'speed'; const handleCopyTo1C = () => { if (!isSpeedOrVip) { showToast("Экспорт в 1С доступен на тарифах Speed и VIP", 'error'); 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) { showToast("Нет товаров для экспорта", 'error'); return; } navigator.clipboard.writeText(lines.join('\n')).then(() => showToast("Скопировано! Вставьте в 1С (Ctrl+V)")).catch(() => showToast("Ошибка доступа к буферу", 'error')); }; const hasWatermark = user?.tier === 'free' && !user?.is_vip; const gridContainerClass = `kp-grid-container ${!hasGlobalDiscount ? 'no-discount' : ''}`; return (
{toast.show && (
{toast.type === 'success' && } {toast.type === 'error' && } {toast.message}
)} {statusMessage &&
{statusMessage}
} {showMappingModal && (

Настройка колонок Excel

)} {showPasteModal && (

Импорт из 1С

Нажмите на область ниже и нажмите Ctrl+V.

Вставьте таблицу сюда (Ctrl+V)
{previewItems.length > 0 ? ( {previewItems.map((it, idx) => ())}
АртикулНазваниеКол-воБазовая ЦенаСкидкаСумма
{it.article}{it.title}{it.quantity}{formatPriceSafe(it.price)}{it.isDiscounted ? `-${it.discountValue}%` : '-'}{formatPriceSafe(it.finalPrice)}
) : (
Нет данных для просмотра
)}
)} {!isPdf && !isMobilePdf && (
{isManagerOrHigher && ( <> )} {isSpeedOrVip && ( <> )} {isSpeedOrVip && (
{showTgMenu && (
)}
)}
{showPdfMenu && (
)}
)} {!isPdf && !isMobilePdf && (
)}
{/* --- ВЫНЕСЕННЫЕ КНОПКИ ПОКАЗА РЕКВИЗИТОВ --- */} {!isPdf && !isMobilePdf && (
)} {/* --- ФОРМА ВВОДА ПОЛНЫХ РЕКВИЗИТОВ --- */} {showFullRequisites && !isPdf && !isMobilePdf && (

Информация о продавце

setReqField('seller', 'name', e.target.value)} placeholder="Название компании или ИП" className="w-full border border-gray-300 focus:border-blue-500 p-2.5 rounded-md outline-none transition-colors" />
setReqField('seller', 'inn', e.target.value)} placeholder="ИНН" className="w-full border border-gray-300 focus:border-blue-500 p-2.5 rounded-md outline-none transition-colors" /> setReqField('seller', 'kpp', e.target.value)} placeholder="КПП" className="w-full border border-gray-300 focus:border-blue-500 p-2.5 rounded-md outline-none transition-colors" /> setReqField('seller', 'ogrn', e.target.value)} placeholder="ОГРН" className="w-full border border-gray-300 focus:border-blue-500 p-2.5 rounded-md outline-none transition-colors" /> setReqField('seller', 'account', e.target.value)} placeholder="№ расчетного счета" className="w-full border border-gray-300 focus:border-blue-500 p-2.5 rounded-md outline-none transition-colors md:col-span-1" /> setReqField('seller', 'bank', e.target.value)} placeholder="Наименование банка" className="w-full border border-gray-300 focus:border-blue-500 p-2.5 rounded-md outline-none transition-colors md:col-span-2" /> setReqField('seller', 'bik', e.target.value)} placeholder="БИК Банка" className="w-full border border-gray-300 focus:border-blue-500 p-2.5 rounded-md outline-none transition-colors md:col-span-1" /> setReqField('seller', 'corr', e.target.value)} placeholder="№ корреспондентского счета" className="w-full border border-gray-300 focus:border-blue-500 p-2.5 rounded-md outline-none transition-colors md:col-span-2" /> setReqField('seller', 'phone', e.target.value)} placeholder="Телефон" className="w-full border border-gray-300 focus:border-blue-500 p-2.5 rounded-md outline-none transition-colors" /> setReqField('seller', 'site', e.target.value)} placeholder="Сайт" className="w-full border border-gray-300 focus:border-blue-500 p-2.5 rounded-md outline-none transition-colors" /> setReqField('seller', 'email', e.target.value)} placeholder="Email" className="w-full border border-gray-300 focus:border-blue-500 p-2.5 rounded-md outline-none transition-colors" /> setReqField('seller', 'directorTitle', e.target.value)} placeholder="Должность руководителя" className="w-full border border-gray-300 focus:border-blue-500 p-2.5 rounded-md outline-none transition-colors md:col-span-1" /> setReqField('seller', 'directorName', e.target.value)} placeholder="ФИО руководителя" className="w-full border border-gray-300 focus:border-blue-500 p-2.5 rounded-md outline-none transition-colors md:col-span-2" /> setReqField('seller', 'managerName', e.target.value)} placeholder="ФИО исполнителя" className="w-full border border-gray-300 focus:border-blue-500 p-2.5 rounded-md outline-none transition-colors md:col-span-1" /> setReqField('seller', 'managerPhone', e.target.value)} placeholder="Телефон исполнителя" className="w-full border border-gray-300 focus:border-blue-500 p-2.5 rounded-md outline-none transition-colors md:col-span-2" />

Информация о покупателе

setReqField('buyer', 'targetTitle', e.target.value)} placeholder="Должность сотрудника для кого адресовано КП" className="w-full border border-gray-300 focus:border-blue-500 p-2.5 rounded-md outline-none transition-colors" />
setReqField('buyer', 'company', e.target.value)} placeholder="Название компании" className="w-full border border-gray-300 focus:border-blue-500 p-2.5 rounded-md outline-none transition-colors" /> setReqField('buyer', 'targetName', e.target.value)} placeholder="ФИО сотрудника" className="w-full border border-gray-300 focus:border-blue-500 p-2.5 rounded-md outline-none transition-colors" />
)}