'use client'; import { useState, useEffect, ChangeEvent, FormEvent } from 'react'; import Image from 'next/image'; import { CalculatorConfig, CalculatorValues, CalculationResults } from '@/lib/calculator-types'; import { getCalculator } from '@/lib/calculator-registry'; type CalculatorEngineProps = { calculatorId: string; chatId?: string | null; onBack?: () => void; }; const toNum = (str: string): number => { const n = parseFloat(str.replace(',', '.')); return isNaN(n) ? 0 : n; }; const InputNumber = ({ id, label, value, onChange, gridCols = 2, }: { id: string; label: string; value: string; onChange: (v: string) => void; gridCols?: 1 | 2; }) => { return (
onChange(e.target.value)} placeholder=" " className={` peer h-10 w-full bg-gray-700 border-2 border-gray-600 rounded-md text-gray-200 placeholder-transparent pl-3 focus:outline-none focus:border-sky-500 appearance-none `} />
); }; const InputText = ({ id, label, value, onChange, }: { id: string; label: string; value: string; onChange: (v: string) => void; }) => { return (
onChange(e.target.value)} placeholder=" " className={` peer h-10 w-full bg-gray-700 border-2 border-gray-600 rounded-md text-gray-200 placeholder-transparent pl-3 focus:outline-none focus:border-sky-500 appearance-none `} />
); }; const CostBlock = ({ title, value, highlight = false, }: { title: string; value: number; highlight?: boolean; }) => { const displayValue = isNaN(value) || !isFinite(value) ? 0 : value; return (
{title}: {displayValue.toFixed(1)} руб
); }; export default function CalculatorEngine({ calculatorId, chatId: externalChatId, onBack, }: CalculatorEngineProps) { const [config, setConfig] = useState(null); const [values, setValues] = useState({}); const [photoFile, setPhotoFile] = useState(null); const [chatId, setChatId] = useState(null); // Загружаем конфигурацию калькулятора useEffect(() => { const calculatorConfig = getCalculator(calculatorId); if (calculatorConfig) { setConfig(calculatorConfig); // Инициализируем значения по умолчанию const initialValues: CalculatorValues = {}; calculatorConfig.fields.forEach((field) => { initialValues[field.id] = field.defaultValue || ''; }); setValues(initialValues); } }, [calculatorId]); // Получаем chat_id из URL или пропсов useEffect(() => { if (externalChatId) { setChatId(externalChatId); } else { const params = new URLSearchParams(window.location.search); const id = params.get('chat_id'); if (id) { setChatId(id); } } }, [externalChatId]); if (!config) { return (

Калькулятор не найден

{onBack && ( )}
); } // Вычисляем все значения как числа const numValues: Record = {}; Object.keys(values).forEach((key) => { if (config.fields.find((f) => f.id === key && f.type !== 'file' && f.type !== 'text')) { numValues[key] = toNum(values[key]); } else if (config.fields.find((f) => f.id === key && f.type === 'text')) { // Для текстовых полей сохраняем строку (но в numValues это не используется, только для consistency) numValues[key] = 0; // Текстовые поля не участвуют в числовых расчетах } }); // Выполняем расчеты const results: CalculationResults = { steps: {}, subtotals: {}, additional: {}, }; // Шаги расчета config.calculationSteps.forEach((step) => { results.steps[step.id] = step.formula(numValues); }); // Подитоги config.subtotals.forEach((subtotal) => { results.subtotals[subtotal.id] = subtotal.formula(numValues, results.steps); }); // Дополнительные расчеты (нужно делать последовательно, так как некоторые зависят от других) if (config.additionalCalculations) { config.additionalCalculations.forEach((calc) => { results.additional![calc.id] = calc.formula(numValues, results.steps, results.subtotals, results.additional); }); } const handleFieldChange = (fieldId: string, newValue: string) => { setValues((prev) => ({ ...prev, [fieldId]: newValue })); }; const handlePhotoChange = (e: ChangeEvent) => { if (e.target.files && e.target.files[0]) { setPhotoFile(e.target.files[0]); } else { setPhotoFile(null); } }; const handleSubmit = async (e: FormEvent) => { e.preventDefault(); if (!chatId) { alert('❗ Не найден chat_id. Откройте калькулятор через Telegram-бота.'); return; } const formData = new FormData(); formData.append('chat_id', chatId); formData.append('calculator_id', config.id); formData.append('calculator_name', config.name); // Добавляем все поля config.fields.forEach((field) => { if (field.type !== 'file') { const value = values[field.id] || ''; if (field.type === 'number') { formData.append(field.id, toNum(value).toString()); } else { formData.append(field.id, value); } } }); // Добавляем результаты расчетов Object.keys(results.steps).forEach((key) => { formData.append(`step_${key}`, results.steps[key].toString()); }); Object.keys(results.subtotals).forEach((key) => { formData.append(`subtotal_${key}`, results.subtotals[key].toString()); }); if (results.additional) { Object.keys(results.additional).forEach((key) => { formData.append(`additional_${key}`, results.additional![key].toString()); }); } // Добавляем фото если есть if (photoFile) { formData.append('photo', photoFile); } // Формируем данные для форматирования сообщения const allValues: Record = {}; Object.keys(values).forEach((key) => { const field = config.fields.find((f) => f.id === key); if (field && field.type === 'number') { allValues[key] = toNum(values[key]); } else { allValues[key] = values[key]; } }); // Форматируем сообщение для Telegram const telegramMessage = config.formatTelegramMessage ? config.formatTelegramMessage(allValues, results.steps, results.subtotals, results.additional || {}) : JSON.stringify({ values, results }, null, 2); formData.append('telegram_message', telegramMessage); try { // Для локальной разработки используем localhost, для продакшена - api-dosoap.duckdns.org const isLocalhost = typeof window !== 'undefined' && window.location.hostname === 'localhost'; const apiUrl = isLocalhost ? 'http://localhost:3001/api/submit' : 'https://api-dosoap.duckdns.org/api/submit'; const res = await fetch(apiUrl, { method: 'POST', body: formData, }); if (res.ok) { alert('✅ Расчёт успешно отправлен в Telegram!'); // Сброс формы const resetValues: CalculatorValues = {}; config.fields.forEach((field) => { resetValues[field.id] = field.defaultValue || ''; }); setValues(resetValues); setPhotoFile(null); window.scrollTo({ top: 0, behavior: 'smooth' }); } else { const text = await res.text(); alert(`Ошибка при отправке: ${text}`); } } catch (err) { console.error(err); alert('Ошибка сети при отправке расчёта'); } }; // Находим поле для фото const photoField = config.fields.find((f) => f.type === 'file'); const textFields = config.fields.filter((f) => f.type === 'text'); // Группируем числовые поля по группам const numberFields = config.fields.filter((f) => f.type === 'number'); const fieldsByGroup: Record = {}; const ungroupedFields: typeof numberFields = []; numberFields.forEach((field) => { if (field.groupName) { if (!fieldsByGroup[field.groupName]) { fieldsByGroup[field.groupName] = []; } fieldsByGroup[field.groupName].push(field); } else { ungroupedFields.push(field); } }); // Определяем порядок групп на основе порядка полей в конфигурации const groupOrder: string[] = []; const seenGroups = new Set(); numberFields.forEach((field) => { if (field.groupName && !seenGroups.has(field.groupName)) { groupOrder.push(field.groupName); seenGroups.add(field.groupName); } }); // Добавляем группы, которые не были найдены (на всякий случай) Object.keys(fieldsByGroup).forEach((groupName) => { if (!seenGroups.has(groupName)) { groupOrder.push(groupName); } }); return (
{/* Кнопка назад */} {onBack && ( )} {/* Логотип */}
Logo
{chatId === null && (
❗ Не найден chat_id. Откройте калькулятор через Telegram-бота.
)} {/* Текстовые поля */} {textFields.map((field) => ( handleFieldChange(field.id, value)} /> ))} {/* Поле для фото */} {photoField && ( <> {photoFile && ( // eslint-disable-next-line @next/next/no-img-element Предпросмотр )} )} {/* Группированные поля с расчетами между ними */} {groupOrder.map((groupName) => { const groupFields = fieldsByGroup[groupName] || []; const firstField = groupFields[0]; const showStepId = firstField?.showStepAfter; return (
{/* Поля группы */}
{groupFields.map((field) => ( handleFieldChange(field.id, value)} gridCols={field.gridCols} /> ))}
{/* Блок расчета после группы */} {showStepId && ( s.id === showStepId)?.name || ''} value={results.steps[showStepId] || 0} /> )}
); })} {/* Несгруппированные поля */} {ungroupedFields.length > 0 && (
{ungroupedFields.map((field) => ( handleFieldChange(field.id, value)} gridCols={field.gridCols} /> ))}
)} {/* Подитоги */} {config.subtotals.map((subtotal) => ( ))} {/* Дополнительные расчеты */} {config.additionalCalculations?.map((calc) => ( ))} {/* Кнопка отправки */} ); }