// components/DynamicCalculator.tsx // Универсальный компонент для генерации калькуляторов из схемы 'use client'; import { useState, useEffect, ChangeEvent, FormEvent } from 'react'; import Image from 'next/image'; import { getCalculator } from '@/lib/calculators'; import { submitCalculator } from '@/lib/api'; import FormField from './FormField'; import CalculatorNav from './CalculatorNav'; import type { Calculator } from '@/types/calculator'; interface DynamicCalculatorProps { calculatorType: string; } // Форматирование меток результатов function formatResultLabel(key: string): string { const labels: Record = { base: 'Себестоимость основы', aroma: 'Себестоимость отдушки', pigment: 'Себестоимость пигмента', mold: 'Себестоимость формы', packaging: 'Стоимость упаковки', operational: 'Операционные расходы (5%)', wax: 'Себестоимость воска', wick: 'Себестоимость фитиля', }; return labels[key] || key; } export default function DynamicCalculator({ calculatorType }: DynamicCalculatorProps) { const calculator = getCalculator(calculatorType); if (!calculator) { return (

Калькулятор "{calculatorType}" не найден

); } const [formData, setFormData] = useState>({}); const [photoFile, setPhotoFile] = useState(null); const [chatId, setChatId] = useState(null); // Инициализация формы с пустыми значениями useEffect(() => { const initialData: Record = {}; calculator.fieldSchema.forEach((field) => { initialData[field.name] = ''; }); setFormData(initialData); }, [calculator]); // Получаем chat_id из URL useEffect(() => { const params = new URLSearchParams(window.location.search); const id = params.get('chat_id'); if (id) { setChatId(id); } }, []); // Преобразование строки в число const toNum = (str: string): number => { const n = parseFloat(str.replace(',', '.')); return isNaN(n) ? 0 : n; }; // Обновление значения поля const updateField = (fieldName: string, value: string) => { setFormData((prev) => ({ ...prev, [fieldName]: value })); }; // Подготовка данных для расчёта const prepareCalculationData = (): Record => { const data: Record = {}; for (const field of calculator.fieldSchema) { if (field.type === 'number') { data[field.name] = toNum(formData[field.name] || '0'); } else { data[field.name] = formData[field.name] || ''; } } // Специфичная обработка для мыла (упаковка) if (calculator.id === 'soap') { data.packaging = { box: toNum(formData.box || '0'), filler: toNum(formData.filler || '0'), ribbon: toNum(formData.ribbon || '0'), label: toNum(formData.labelValue || '0'), }; } return data; }; // Выполнение расчёта const calculationData = prepareCalculationData(); const result = calculator.calculate(calculationData); // Вычисление финальной цены const markup = toNum(formData.markup || '0'); const finalPrice = result.total * (1 + markup / 100); const weight = toNum(formData.weight || '0'); const pricePer100g = weight > 0 ? (finalPrice / weight) * 100 : 0; // Группировка полей по группам const groupedFields = calculator.fieldSchema.reduce((acc, field) => { const group = field.group || 'general'; if (!acc[group]) { acc[group] = []; } acc[group].push(field); return acc; }, {} as Record); // Обработка изменения фото 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 requiredFields = calculator.getRequiredFields(); for (const fieldName of requiredFields) { if (!formData[fieldName] || formData[fieldName].trim() === '') { const field = calculator.fieldSchema.find((f) => f.name === fieldName); alert(`❗ Поле "${field?.label || fieldName}" обязательно для заполнения`); return; } } // Подготовка данных для отправки const submitData: Record = {}; for (const field of calculator.fieldSchema) { if (field.type === 'number') { const value = toNum(formData[field.name] || '0'); submitData[field.name] = value; } else { submitData[field.name] = formData[field.name] || ''; } } // Специфичная обработка для мыла (упаковка отправляется как отдельные поля) // Для других калькуляторов поля отправляются как есть if (calculator.id === 'soap') { submitData.box = toNum(formData.box || '0'); submitData.filler = toNum(formData.filler || '0'); submitData.ribbon = toNum(formData.ribbon || '0'); submitData.labelValue = toNum(formData.labelValue || '0'); } // Добавляем результаты расчёта submitData.totalCost = result.total; submitData.finalPrice = finalPrice; submitData.pricePer100g = pricePer100g; // Отправка на backend const response = await submitCalculator( calculatorType, chatId, submitData, photoFile ); if (response.success) { alert('✅ Расчёт успешно отправлен в Telegram!'); // Очистка формы const initialData: Record = {}; calculator.fieldSchema.forEach((field) => { initialData[field.name] = ''; }); setFormData(initialData); setPhotoFile(null); window.scrollTo({ top: 0, behavior: 'smooth' }); } else { alert(`❌ Ошибка: ${response.error || 'Неизвестная ошибка'}`); } }; return (
{/* Логотип */}
Logo
{/* Навигация между калькуляторами */} {/* Предупреждение о chat_id */} {chatId === null && (
❗ Не найден chat_id. Откройте калькулятор через Telegram-бота.
)} {/* Заголовок калькулятора */}

Калькулятор: {calculator.name}

{/* Основные поля (без группы) */} {groupedFields.general && (
{groupedFields.general.map((field) => ( updateField(field.name, value)} /> ))}
)} {/* Поля с фото */} {photoFile && ( Предпросмотр )} {/* Группированные поля (например, упаковка) */} {Object.entries(groupedFields) .filter(([group]) => group !== 'general') .map(([group, fields]) => (

{group === 'packaging' ? 'Упаковка' : group}

{fields.map((field) => ( updateField(field.name, value)} /> ))}
))} {/* Блоки с результатами расчёта */}
{Object.entries(result) .filter(([key]) => key !== 'total') .map(([key, value]) => (
{formatResultLabel(key)}: {value.toFixed(1)} ₽
))}
Итого себестоимость: {result.total.toFixed(1)} ₽
Итоговая цена с наценкой: {finalPrice.toFixed(1)} ₽
{weight > 0 && (
Цена за 100 г: {pricePer100g.toFixed(1)} ₽
)}
{/* Кнопка отправки */} ); }