import React, { useState, useEffect } from 'react'; import { Routes, Route, Navigate } from 'react-router-dom'; import { Chat } from './components/Chat/Chat'; import { Constructor } from './components/Constructor/Constructor'; import { Archive } from './components/Archive/Archive'; import { AdminPanel } from './components/AdminPanel/AdminPanel'; import { RightSidebar } from './components/RightSidebar/RightSidebar'; import { AuthForm } from './components/Auth/AuthForm'; import { ShareModal } from './components/Archive/ShareModal'; import { Settings } from './components/Settings/Settings'; import { useAuth } from './contexts/AuthContext'; import { useTheme } from './contexts/ThemeContext'; import { api } from './api/client'; import { Moon, Sun, LogOut } from 'lucide-react'; export interface Estimate { id: string; number: string; direction: { code: string; name: string; shortName: string }; objectName: string; customer: string; executor: string; items: EstimateItem[]; totals: EstimateTotal[]; totalFieldWorks?: number; totalOfficeWorks?: number; totalLaboratory?: number; subtotal?: number; regionalCoef?: number; regionalCoefDocRef?: string; inflationIndex?: number; inflationDocRef?: string; companyCoef?: number; companyCoefDocRef?: string; executorCoef?: number; executorCoefDocRef?: string; totalWithoutVat?: number; vatRate?: number; vatAmount?: number; totalWithVat?: number; withVat?: boolean; status: string; } export interface EstimateItem { id: string; orderNumber: number; sectionType: string; workName: string; justification?: string; basePrice: number; quantity: number; unit?: string; coef1?: number; coef1Desc?: string; coef2?: number; coef2Desc?: string; coef3?: number; coef3Desc?: string; totalPrice: number; } export interface EstimateTotal { id: string; orderNumber: number; label: string; description?: string; resultValue: number; } export interface ExtractedData { direction?: string; customer?: string; objectName?: string; executor?: string; works?: Array<{ name: string; volume: number; unit: string; priceItemId?: string; justification?: string; }>; } export interface EstimateWithShared extends Estimate { sharedWithMe?: boolean; } function App() { const { user, loading, logout } = useAuth(); const { theme, toggleTheme } = useTheme(); const [showArchive, setShowArchive] = useState(false); const [manualMode, setManualMode] = useState(false); const [currentEstimate, setCurrentEstimate] = useState(null); const [extractedData, setExtractedData] = useState(null); const [chatSessionId, setChatSessionId] = useState(null); const [showShareModal, setShowShareModal] = useState(false); const [showSettings, setShowSettings] = useState(false); const [settings, setSettings] = useState(null); const [sidebarExpanded, setSidebarExpanded] = useState(false); const [externalMessageTrigger, setExternalMessageTrigger] = useState(0); useEffect(() => { if (showSettings) { api.getSettings().then(setSettings).catch(() => setSettings(null)); } }, [showSettings]); useEffect(() => { if (!user || currentEstimate) return; let cancelled = false; const loadOrCreateEstimate = async () => { try { const list = await api.getEstimates(); const lastId = typeof localStorage !== 'undefined' ? localStorage.getItem('smeta-last-estimate-id') : null; if (list?.length) { const toOpen = list.find((e: { id: string }) => e.id === lastId) ?? list[0]; const full = await api.getEstimate(toOpen.id); if (!cancelled) setCurrentEstimate(full); } else { const directions = await api.getSurveyDirections(); const firstDir = directions?.[0]; const estimate = await api.createEstimate({ directionCode: firstDir?.code ?? 'geodesy', objectName: 'Новая смета', customer: '—', }); if (!cancelled) setCurrentEstimate(estimate); } } catch (err) { console.error('Failed to load initial estimate:', err); } }; loadOrCreateEstimate(); return () => { cancelled = true; }; }, [user, currentEstimate]); useEffect(() => { setExtractedData(null); }, [currentEstimate?.id]); useEffect(() => { if (currentEstimate?.id && typeof localStorage !== 'undefined') { localStorage.setItem('smeta-last-estimate-id', currentEstimate.id); } }, [currentEstimate?.id]); if (loading) { return (
); } if (!user) { return ; } /** Заполняет текущую смету (справа) данными из загруженного в чат файла. Обновляет только смету, чат не трогает. */ const fillCurrentEstimateFromData = async (estimateId: string, data: ExtractedData) => { try { const updates: { objectName?: string; customer?: string; executor?: string } = {}; if (data.objectName?.trim()) updates.objectName = data.objectName.trim(); if (data.customer?.trim()) updates.customer = data.customer.trim(); if (data.executor?.trim()) updates.executor = data.executor.trim(); if (Object.keys(updates).length > 0) { await api.updateEstimate(estimateId, updates); } if (data.works?.length) { for (const work of data.works) { await api.addEstimateItem(estimateId, { sectionType: 'field', workName: work.name, justification: work.justification, basePrice: 0, quantity: work.volume, unit: work.unit, }); } } if (Object.keys(updates).length > 0 || (data.works?.length ?? 0) > 0) { try { await api.recalculateEstimate(estimateId); } catch (_) { // Пересчёт мог упасть (например vatRate null) — всё равно подтягиваем сохранённые данные } const fullEstimate = await api.getEstimate(estimateId); setCurrentEstimate(fullEstimate); } } catch (err) { console.error('Failed to fill estimate from file:', err); } }; const handleExtractedData = (data: ExtractedData) => { setExtractedData(prev => { const next: ExtractedData = prev ? { ...prev } : {}; if (data.direction != null && data.direction !== '') next.direction = data.direction; if (data.customer != null && data.customer !== '') next.customer = data.customer; if (data.objectName != null && data.objectName !== '') next.objectName = data.objectName; if (data.executor != null && data.executor !== '') next.executor = data.executor; if (data.works != null && data.works.length > 0) next.works = data.works; return next; }); }; const handleCreateEstimate = async () => { if (!extractedData?.direction || !extractedData?.customer || !extractedData?.objectName) return; try { const estimate = await api.createEstimate({ directionCode: extractedData.direction, customer: extractedData.customer, objectName: extractedData.objectName, executor: extractedData.executor, }); if (extractedData.works?.length) { for (const work of extractedData.works) { await api.addEstimateItem(estimate.id, { sectionType: 'field', workName: work.name, justification: work.justification, basePrice: 0, quantity: work.volume, unit: work.unit, }); } await api.recalculateEstimate(estimate.id); } const fullEstimate = await api.getEstimate(estimate.id); setCurrentEstimate(fullEstimate); if (chatSessionId) { await api.updateChatSession(chatSessionId, { estimateId: fullEstimate.id }); } } catch (error) { console.error('Failed to create estimate:', error); } }; const handleEstimateUpdate = async (estimateId: string) => { try { const estimate = await api.getEstimate(estimateId); setCurrentEstimate(estimate); } catch (error) { console.error('Failed to update estimate:', error); } }; const handleCreateManualEstimate = async (data: { directionCode: string; objectName: string; customer: string; executor?: string; }) => { try { const estimate = await api.createEstimate(data); const fullEstimate = await api.getEstimate(estimate.id); setCurrentEstimate(fullEstimate); } catch (error) { console.error('Failed to create estimate:', error); } }; return ( } />
Портал смет
{user.name || user.email}
setSidebarExpanded(v => !v)} currentEstimate={currentEstimate} onOpenEstimate={estimate => { setCurrentEstimate(estimate); setShowArchive(false); }} onShowArchive={() => setShowArchive(true)} onShowShare={() => setShowShareModal(true)} onShowSettings={() => setShowSettings(true)} />
{showArchive ? (
{ setCurrentEstimate(estimate); setShowArchive(false); }} onClose={() => setShowArchive(false)} />
) : manualMode ? (
{ if (!chatSessionId) return; try { await api.sendMessage(chatSessionId, `[Вопрос по выделенному]\n\nКонтекст: ${contextText}\n\nВопрос: ${question}`, 'discuss'); setExternalMessageTrigger(t => t + 1); } catch (e) { console.error('Failed to ask about selection:', e); } }} />
) : ( <>
{ if (!chatSessionId) return; try { await api.sendMessage(chatSessionId, `[Вопрос по выделенному]\n\nКонтекст: ${contextText}\n\nВопрос: ${question}`, 'discuss'); setExternalMessageTrigger(t => t + 1); } catch (e) { console.error('Failed to ask about selection:', e); } }} />
)}
{showShareModal && currentEstimate && !currentEstimate.sharedWithMe && ( setShowShareModal(false)} onShared={() => {}} /> )} {showSettings && (
setShowSettings(false)}>
e.stopPropagation()}> setShowSettings(false)} onSave={() => { api.getSettings().then(setSettings); }} />
)}
} /> ); } export default App;