// 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 CostBlock from './CostBlock'; 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 (для статического экспорта используем window.location напрямую) useEffect(() => { if (typeof window === 'undefined') return; const updateChatId = () => { // Получаем chat_id из query параметров URL const params = new URLSearchParams(window.location.search); let id = params.get('chat_id'); // Если не найден в search params, проверяем hash if (!id && window.location.hash) { const hashParams = new URLSearchParams(window.location.hash.replace('#', '')); id = hashParams.get('chat_id'); } // Если всё ещё не найден, проверяем весь URL строку if (!id) { const fullUrl = window.location.href; const chatIdMatch = fullUrl.match(/[?&#]chat_id=([^&#]+)/); if (chatIdMatch) { id = decodeURIComponent(chatIdMatch[1]); } } if (id) { setChatId(id); // Сохраняем в sessionStorage для сохранения при переходах if (typeof Storage !== 'undefined') { sessionStorage.setItem('chat_id', id); } } else { // Пробуем восстановить из sessionStorage if (typeof Storage !== 'undefined') { const savedChatId = sessionStorage.getItem('chat_id'); if (savedChatId) { setChatId(savedChatId); } } } }; updateChatId(); // Слушаем изменения URL (popstate для навигации назад/вперёд) window.addEventListener('popstate', updateChatId); return () => { window.removeEventListener('popstate', updateChatId); }; }, [calculatorType]); // Преобразование строки в число 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(); // Получаем chat_id из разных источников let currentChatId = chatId; if (!currentChatId && typeof window !== 'undefined') { const params = new URLSearchParams(window.location.search); currentChatId = params.get('chat_id'); if (!currentChatId && typeof Storage !== 'undefined') { currentChatId = sessionStorage.getItem('chat_id'); } if (currentChatId) { setChatId(currentChatId); } } if (!currentChatId) { 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, currentChatId!, 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}

{/* Специфичная структура для мыла с промежуточными стоимостями */} {calculator.id === 'soap' && ( <> {/* Название мыла */} f.name === 'soapName')!} value={formData.soapName || ''} onChange={(value) => updateField('soapName', value)} /> {/* Фото */} {photoFile && ( Предпросмотр мыла )} {/* Блок «Основа» */}
f.name === 'weight')!} value={formData.weight || ''} onChange={(value) => updateField('weight', value)} /> f.name === 'basePrice')!} value={formData.basePrice || ''} onChange={(value) => updateField('basePrice', value)} />
{/* Блок «Отдушка» */}
f.name === 'aromaPrice')!} value={formData.aromaPrice || ''} onChange={(value) => updateField('aromaPrice', value)} /> f.name === 'aromaWeight')!} value={formData.aromaWeight || ''} onChange={(value) => updateField('aromaWeight', value)} />
{/* Блок «Пигмент» */}
f.name === 'pigmentPrice')!} value={formData.pigmentPrice || ''} onChange={(value) => updateField('pigmentPrice', value)} /> f.name === 'pigmentWeight')!} value={formData.pigmentWeight || ''} onChange={(value) => updateField('pigmentWeight', value)} />
{/* Блок «Форма» */} f.name === 'moldPrice')!} value={formData.moldPrice || ''} onChange={(value) => updateField('moldPrice', value)} /> {/* Блок «Упаковка» */}
f.name === 'box')!} value={formData.box || ''} onChange={(value) => updateField('box', value)} /> f.name === 'filler')!} value={formData.filler || ''} onChange={(value) => updateField('filler', value)} /> f.name === 'ribbon')!} value={formData.ribbon || ''} onChange={(value) => updateField('ribbon', value)} /> f.name === 'labelValue')!} value={formData.labelValue || ''} onChange={(value) => updateField('labelValue', value)} />
{/* Блок «Наценка и цена 100 г» */}
f.name === 'markup')!} value={formData.markup || ''} onChange={(value) => updateField('markup', value)} />
)} {/* Универсальная структура для других калькуляторов */} {calculator.id !== 'soap' && ( <> {/* Основные поля (без группы) */} {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]) => ( ))} {weight > 0 && ( )}
)} {/* Кнопка отправки */} ); }