'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 (
);
};
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[key] = values[key] as any; // Для текстовых полей сохраняем строку
}
});
// Выполняем расчеты
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 (
);
}