// components/SoapCalculator.tsx
'use client';
import { useState, useEffect, ChangeEvent } from 'react';
import { calculateTotal } from '@/lib/calc';
type InputNumberProps = {
label: string;
value: string;
onChange: (v: string) => void;
placeholder?: string;
};
const InputNumber = ({ label, value, onChange, placeholder }: InputNumberProps) => (
);
const CostBlock = ({
title,
value,
highlight = false,
}: {
title: string;
value: number;
highlight?: boolean;
}) => (
{title}: {value.toFixed(1)} руб
);
export default function SoapCalculator() {
// 1) Поля ввода как строки (по умолчанию пустые)
const [soapName, setSoapName] = useState('');
const [weight, setWeight] = useState(''); // г
const [basePrice, setBasePrice] = useState(''); // руб/кг
const [aromaPrice, setAromaPrice] = useState(''); // руб/фасовка
const [aromaWeight, setAromaWeight] = useState(''); // г фасовка
const [pigmentPrice, setPigmentPrice] = useState(''); // руб/фасовка
const [pigmentWeight, setPigmentWeight] = useState(''); // г фасовка
const [moldPrice, setMoldPrice] = useState(''); // руб за форму
const [box, setBox] = useState(''); // руб
const [filler, setFiller] = useState(''); // руб
const [ribbon, setRibbon] = useState(''); // руб
const [labelValue, setLabelValue] = useState(''); // руб наклейка
const [markup, setMarkup] = useState(''); // %
// Файл фотографии и chat_id
const [photoFile, setPhotoFile] = useState(null);
const [chatId, setChatId] = useState(null);
// 2) Извлекаем chat_id из URL при монтировании
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const id = params.get('chat_id');
if (id) {
setChatId(id);
}
}, []);
// 3) Преобразуем строковые поля в числа (или 0, если не число)
const toNum = (str: string) => {
const n = parseFloat(str.replace(',', '.'));
return isNaN(n) ? 0 : n;
};
const weightNum = toNum(weight);
const basePriceNum = toNum(basePrice);
const aromaPriceNum = toNum(aromaPrice);
const aromaWeightNum = toNum(aromaWeight);
const pigmentPriceNum = toNum(pigmentPrice);
const pigmentWeightNum = toNum(pigmentWeight);
const moldPriceNum = toNum(moldPrice);
const boxNum = toNum(box);
const fillerNum = toNum(filler);
const ribbonNum = toNum(ribbon);
const labelNum = toNum(labelValue);
const markupNum = toNum(markup);
// 4) Считаем все базовые значения через calculateTotal
const result = calculateTotal({
weight: weightNum,
basePrice: basePriceNum,
aromaPrice: aromaPriceNum,
aromaWeight: aromaWeightNum,
pigmentPrice: pigmentPriceNum,
pigmentWeight: pigmentWeightNum,
moldPrice: moldPriceNum,
packaging: {
box: boxNum,
filler: fillerNum,
ribbon: ribbonNum,
label: labelNum,
},
});
// 5) Итоговые значения
const finalPrice = result.total * (1 + markupNum / 100);
const pricePer100g = weightNum > 0 ? (finalPrice / weightNum) * 100 : 0;
// 6) Обработчик изменения фото
const handlePhotoChange = (e: ChangeEvent) => {
if (e.target.files && e.target.files[0]) {
setPhotoFile(e.target.files[0]);
} else {
setPhotoFile(null);
}
};
// 7) Обработчик отправки формы на backend
const handleSubmit = async () => {
if (!chatId) {
alert('❗ Не найден chat_id. Откройте калькулятор через Telegram-бота.');
return;
}
// Собираем FormData
const formData = new FormData();
formData.append('chat_id', chatId);
formData.append('soapName', soapName || '');
formData.append('weight', weightNum.toString());
formData.append('basePrice', basePriceNum.toString());
formData.append('aromaPrice', aromaPriceNum.toString());
formData.append('aromaWeight', aromaWeightNum.toString());
formData.append('pigmentPrice', pigmentPriceNum.toString());
formData.append('pigmentWeight', pigmentWeightNum.toString());
formData.append('moldPrice', moldPriceNum.toString());
formData.append('box', boxNum.toString());
formData.append('filler', fillerNum.toString());
formData.append('ribbon', ribbonNum.toString());
formData.append('labelValue', labelNum.toString());
formData.append('markup', markupNum.toString());
formData.append('totalCost', result.total.toString());
formData.append('finalPrice', finalPrice.toString());
formData.append('pricePer100g', pricePer100g.toString());
if (photoFile) {
formData.append('photo', photoFile);
}
try {
// Замените URL на публичный адрес вашего backend (Express/ngrok/другой хостинг)
// Например: 'https://abcd1234.ngrok.io/api/submit'
const res = await fetch('https://api-dosoap.duckdns.org/api/submit', {
method: 'POST',
body: formData,
});
if (res.ok) {
alert('✅ Расчёт успешно отправлен в Telegram!');
// Сбрасываем все поля на пустые строки / null
setSoapName('');
setWeight('');
setBasePrice('');
setAromaPrice('');
setAromaWeight('');
setPigmentPrice('');
setPigmentWeight('');
setMoldPrice('');
setBox('');
setFiller('');
setRibbon('');
setLabelValue('');
setMarkup('');
setPhotoFile(null);
// Прокручиваем страницу вверх
window.scrollTo({ top: 0, behavior: 'smooth' });
} else {
const text = await res.text();
alert(`Ошибка при отправке: ${text}`);
}
} catch (err) {
console.error(err);
alert('Ошибка сети при отправке расчёта');
}
};
return (
{chatId === null && (
❗ Не найден chat_id. Откройте калькулятор через Telegram-бота.
)}
{/* Название мыла */}
{/* Фото мыла */}
{photoFile && (
})
)}
{/* Блок «Основа» */}
{/* Блок «Отдушка» */}
{/* Блок «Пигмент» */}
{/* Блок «Форма» */}
{/* Блок «Упаковка» */}
{/* Блок «Наценка и цена 100 г» */}
{/* Кнопка «Отправить расчёт» */}
);
}