DoSoapCalc/frontend/components/SoapCalculator.tsx
2025-06-04 02:46:45 +03:00

337 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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) => (
<label className="flex flex-col gap-1">
<span>{label}</span>
<input
type="number"
className="border px-2 py-1 rounded text-black"
value={value}
placeholder={placeholder}
onChange={(e) => onChange(e.target.value)}
/>
</label>
);
const CostBlock = ({
title,
value,
highlight = false,
}: {
title: string;
value: number;
highlight?: boolean;
}) => (
<div
className={`p-2 ${highlight ? 'bg-red-200 font-semibold' : 'bg-lime-100'}`}
>
{title}: {value.toFixed(1)} руб
</div>
);
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<File | null>(null);
const [chatId, setChatId] = useState<string | null>(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<HTMLInputElement>) => {
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('http://91.122.47.159:3001/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 (
<div className="max-w-xl mx-auto p-4 space-y-6 text-sm">
{chatId === null && (
<div className="bg-yellow-100 p-2 text-yellow-800 font-semibold">
Не найден chat_id. Откройте калькулятор через Telegram-бота.
</div>
)}
{/* Название мыла */}
<label className="flex flex-col gap-1">
<span>Название мыла</span>
<input
type="text"
className="border px-2 py-1 rounded text-black"
value={soapName}
onChange={(e) => setSoapName(e.target.value)}
placeholder="Например: Лавандовое"
/>
</label>
{/* Фото мыла */}
<label className="flex flex-col gap-1">
<span>Фото мыла (необязательно)</span>
<input type="file" accept="image/*" onChange={handlePhotoChange} />
</label>
{photoFile && (
<img
src={URL.createObjectURL(photoFile)}
alt="Предпросмотр мыла"
className="w-32 h-32 object-cover rounded"
/>
)}
{/* Блок «Основа» */}
<div className="grid grid-cols-2 gap-4">
<InputNumber
label="Вес мыла, г"
value={weight}
onChange={setWeight}
placeholder="Введите, например 60"
/>
<InputNumber
label="Цена за 1 кг основы, руб"
value={basePrice}
onChange={setBasePrice}
placeholder="Введите, например 1000"
/>
</div>
<CostBlock title="Себестоимость основы" value={result.base} />
{/* Блок «Отдушка» */}
<div className="grid grid-cols-2 gap-4">
<InputNumber
label="Цена отдушки, руб"
value={aromaPrice}
onChange={setAromaPrice}
placeholder="Введите, например 300"
/>
<InputNumber
label="Фасовка отдушки, г"
value={aromaWeight}
onChange={setAromaWeight}
placeholder="Введите, например 50"
/>
</div>
<CostBlock title="Себестоимость отдушки (1 %)" value={result.aroma} />
{/* Блок «Пигмент» */}
<div className="grid grid-cols-2 gap-4">
<InputNumber
label="Цена пигмента, руб"
value={pigmentPrice}
onChange={setPigmentPrice}
placeholder="Введите, например 20"
/>
<InputNumber
label="Фасовка пигмента, г"
value={pigmentWeight}
onChange={setPigmentWeight}
placeholder="Введите, например 10"
/>
</div>
<CostBlock title="Себестоимость пигмента (0.5 %)" value={result.pigment} />
{/* Блок «Форма» */}
<InputNumber
label="Цена формы, руб"
value={moldPrice}
onChange={setMoldPrice}
placeholder="Введите, например 500"
/>
<CostBlock title="Себестоимость формы" value={result.mold} />
{/* Блок «Упаковка» */}
<div className="grid grid-cols-2 gap-4">
<InputNumber
label="Пакет/коробка, руб"
value={box}
onChange={setBox}
placeholder="Введите, например 20"
/>
<InputNumber
label="Наполнитель, руб"
value={filler}
onChange={setFiller}
placeholder="Введите, например 5"
/>
<InputNumber
label="Лента, руб"
value={ribbon}
onChange={setRibbon}
placeholder="Введите, например 1"
/>
<InputNumber
label="Наклейка, руб"
value={labelValue}
onChange={setLabelValue}
placeholder="Введите, например 1"
/>
</div>
<CostBlock title="Стоимость упаковки" value={result.packaging} />
<CostBlock title="Операционные расходы (5 %)" value={result.operational} />
<CostBlock title="Итого себестоимость" value={result.total} highlight />
{/* Блок «Наценка и цена 100 г» */}
<div className="grid grid-cols-2 gap-4">
<InputNumber
label="Наценка, %"
value={markup}
onChange={setMarkup}
placeholder="Введите, например 30"
/>
</div>
<CostBlock title="Итоговая цена с наценкой" value={finalPrice} />
<CostBlock title="Цена за 100 г" value={pricePer100g} />
{/* Кнопка «Отправить расчёт» */}
<button
className="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded"
onClick={handleSubmit}
>
Отправить расчёт в Telegram
</button>
</div>
);
}