fix: Восстановлена оригинальная структура формы мыла и исправлена обработка chat_id

- Восстановлена структура формы мыла с промежуточными стоимостями под каждым блоком
- Добавлен компонент CostBlock для отображения промежуточных стоимостей
- Улучшена обработка chat_id при отправке (проверка URL и sessionStorage)
- Улучшена навигация между калькуляторами с более понятным интерфейсом
- Добавлена подсказка для пользователя о переключении калькуляторов
This commit is contained in:
dosai 2025-11-01 20:27:36 +03:00
parent 186c85738d
commit fc7c42861c
3 changed files with 250 additions and 77 deletions

View File

@ -44,11 +44,11 @@ export default function CalculatorNav() {
}; };
return ( return (
<div className="mb-6 bg-gray-700 rounded-lg p-4"> <div className="mb-6 bg-gray-700 rounded-lg p-4 border-2 border-gray-600">
<h2 className="text-lg font-semibold text-gray-200 mb-3"> <h2 className="text-lg font-semibold text-gray-200 mb-3 text-center">
Выберите калькулятор: 🔄 Переключить калькулятор:
</h2> </h2>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-3">
{calculators.map((calc) => { {calculators.map((calc) => {
const isActive = pathname === `/${calc.id}`; const isActive = pathname === `/${calc.id}`;
@ -56,19 +56,24 @@ export default function CalculatorNav() {
<button <button
key={calc.id} key={calc.id}
onClick={() => handleCalculatorChange(calc.id)} onClick={() => handleCalculatorChange(calc.id)}
type="button"
className={` className={`
px-4 py-2 rounded-md text-center font-medium transition-colors px-4 py-3 rounded-md text-center font-semibold transition-all
${isActive ${isActive
? 'bg-sky-500 text-gray-100' ? 'bg-sky-500 text-gray-100 shadow-lg scale-105'
: 'bg-gray-600 text-gray-200 hover:bg-gray-500 active:bg-gray-400' : 'bg-gray-600 text-gray-200 hover:bg-gray-500 active:bg-gray-400 hover:scale-102'
} }
`} `}
> >
{isActive && '✓ '}
{calc.name} {calc.name}
</button> </button>
); );
})} })}
</div> </div>
<p className="text-xs text-gray-400 mt-2 text-center">
Нажмите на кнопку выше, чтобы переключиться между калькуляторами
</p>
</div> </div>
); );
} }

View File

@ -0,0 +1,24 @@
// components/CostBlock.tsx
// Блок отображения промежуточной стоимости
type CostBlockProps = {
title: string;
value: number;
highlight?: boolean;
};
export default function CostBlock({ title, value, highlight = false }: CostBlockProps) {
return (
<div
className={`
p-2
${highlight ? 'bg-gray-600 font-semibold' : 'bg-gray-700'}
text-gray-200
rounded
`}
>
{title}: {value.toFixed(1)} руб
</div>
);
}

View File

@ -9,6 +9,7 @@ import { getCalculator } from '@/lib/calculators';
import { submitCalculator } from '@/lib/api'; import { submitCalculator } from '@/lib/api';
import FormField from './FormField'; import FormField from './FormField';
import CalculatorNav from './CalculatorNav'; import CalculatorNav from './CalculatorNav';
import CostBlock from './CostBlock';
import type { Calculator } from '@/types/calculator'; import type { Calculator } from '@/types/calculator';
interface DynamicCalculatorProps { interface DynamicCalculatorProps {
@ -175,7 +176,21 @@ export default function DynamicCalculator({ calculatorType }: DynamicCalculatorP
// Обработка отправки формы // Обработка отправки формы
const handleSubmit = async (e: FormEvent) => { const handleSubmit = async (e: FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!chatId) {
// Получаем 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-бота.'); alert('❗ Не найден chat_id. Откройте калькулятор через Telegram-бота.');
return; return;
} }
@ -218,7 +233,7 @@ export default function DynamicCalculator({ calculatorType }: DynamicCalculatorP
// Отправка на backend // Отправка на backend
const response = await submitCalculator( const response = await submitCalculator(
calculatorType, calculatorType,
chatId, currentChatId!,
submitData, submitData,
photoFile photoFile
); );
@ -273,55 +288,141 @@ export default function DynamicCalculator({ calculatorType }: DynamicCalculatorP
Калькулятор: {calculator.name} Калькулятор: {calculator.name}
</h1> </h1>
{/* Основные поля (без группы) */} {/* Специфичная структура для мыла с промежуточными стоимостями */}
{groupedFields.general && ( {calculator.id === 'soap' && (
<div className="space-y-4"> <>
{groupedFields.general.map((field) => ( {/* Название мыла */}
<FormField <FormField
key={field.id} field={calculator.fieldSchema.find((f) => f.name === 'soapName')!}
field={field} value={formData.soapName || ''}
value={formData[field.name] || ''} onChange={(value) => updateField('soapName', value)}
onChange={(value) => updateField(field.name, value)} />
{/* Фото */}
<label className="flex flex-col gap-1">
<span className="text-gray-300">Фото мыла (необязательно)</span>
<input
type="file"
accept="image/*"
onChange={handlePhotoChange}
className="
bg-gray-700
text-gray-200
rounded-md
border border-gray-600
p-2
focus:outline-none focus:border-sky-500
"
/> />
))} </label>
</div> {photoFile && (
<img
src={URL.createObjectURL(photoFile)}
alt="Предпросмотр мыла"
className="w-32 h-32 object-cover rounded-lg border border-sky-400"
/>
)}
{/* Блок «Основа» */}
<div className="grid grid-cols-2 gap-4">
<FormField
field={calculator.fieldSchema.find((f) => f.name === 'weight')!}
value={formData.weight || ''}
onChange={(value) => updateField('weight', value)}
/>
<FormField
field={calculator.fieldSchema.find((f) => f.name === 'basePrice')!}
value={formData.basePrice || ''}
onChange={(value) => updateField('basePrice', value)}
/>
</div>
<CostBlock title="Себестоимость основы" value={result.base} />
{/* Блок «Отдушка» */}
<div className="grid grid-cols-2 gap-4">
<FormField
field={calculator.fieldSchema.find((f) => f.name === 'aromaPrice')!}
value={formData.aromaPrice || ''}
onChange={(value) => updateField('aromaPrice', value)}
/>
<FormField
field={calculator.fieldSchema.find((f) => f.name === 'aromaWeight')!}
value={formData.aromaWeight || ''}
onChange={(value) => updateField('aromaWeight', value)}
/>
</div>
<CostBlock title="Себестоимость отдушки (1 %)" value={result.aroma} />
{/* Блок «Пигмент» */}
<div className="grid grid-cols-2 gap-4">
<FormField
field={calculator.fieldSchema.find((f) => f.name === 'pigmentPrice')!}
value={formData.pigmentPrice || ''}
onChange={(value) => updateField('pigmentPrice', value)}
/>
<FormField
field={calculator.fieldSchema.find((f) => f.name === 'pigmentWeight')!}
value={formData.pigmentWeight || ''}
onChange={(value) => updateField('pigmentWeight', value)}
/>
</div>
<CostBlock title="Себестоимость пигмента (0.5 %)" value={result.pigment} />
{/* Блок «Форма» */}
<FormField
field={calculator.fieldSchema.find((f) => f.name === 'moldPrice')!}
value={formData.moldPrice || ''}
onChange={(value) => updateField('moldPrice', value)}
/>
<CostBlock title="Себестоимость формы" value={result.mold} />
{/* Блок «Упаковка» */}
<div className="grid grid-cols-2 gap-4">
<FormField
field={calculator.fieldSchema.find((f) => f.name === 'box')!}
value={formData.box || ''}
onChange={(value) => updateField('box', value)}
/>
<FormField
field={calculator.fieldSchema.find((f) => f.name === 'filler')!}
value={formData.filler || ''}
onChange={(value) => updateField('filler', value)}
/>
<FormField
field={calculator.fieldSchema.find((f) => f.name === 'ribbon')!}
value={formData.ribbon || ''}
onChange={(value) => updateField('ribbon', value)}
/>
<FormField
field={calculator.fieldSchema.find((f) => f.name === 'labelValue')!}
value={formData.labelValue || ''}
onChange={(value) => updateField('labelValue', value)}
/>
</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">
<FormField
field={calculator.fieldSchema.find((f) => f.name === 'markup')!}
value={formData.markup || ''}
onChange={(value) => updateField('markup', value)}
/>
</div>
<CostBlock title="Итоговая цена с наценкой" value={finalPrice} />
<CostBlock title="Цена за 100 г" value={pricePer100g} />
</>
)} )}
{/* Поля с фото */} {/* Универсальная структура для других калькуляторов */}
<label className="flex flex-col gap-1"> {calculator.id !== 'soap' && (
<span className="text-gray-300">Фото (необязательно)</span> <>
<input {/* Основные поля (без группы) */}
type="file" {groupedFields.general && (
accept="image/*" <div className="space-y-4">
onChange={handlePhotoChange} {groupedFields.general.map((field) => (
className="
bg-gray-700
text-gray-200
rounded-md
border border-gray-600
p-2
focus:outline-none focus:border-sky-500
"
/>
</label>
{photoFile && (
<img
src={URL.createObjectURL(photoFile)}
alt="Предпросмотр"
className="w-32 h-32 object-cover rounded-lg border border-sky-400"
/>
)}
{/* Группированные поля (например, упаковка) */}
{Object.entries(groupedFields)
.filter(([group]) => group !== 'general')
.map(([group, fields]) => (
<div key={group} className="space-y-4">
<h2 className="text-xl font-semibold text-gray-300 capitalize">
{group === 'packaging' ? 'Упаковка' : group}
</h2>
<div className="grid grid-cols-2 gap-4">
{fields.map((field) => (
<FormField <FormField
key={field.id} key={field.id}
field={field} field={field}
@ -330,30 +431,73 @@ export default function DynamicCalculator({ calculatorType }: DynamicCalculatorP
/> />
))} ))}
</div> </div>
</div> )}
))}
{/* Блоки с результатами расчёта */} {/* Поля с фото */}
<div className="space-y-2"> <label className="flex flex-col gap-1">
{Object.entries(result) <span className="text-gray-300">Фото (необязательно)</span>
.filter(([key]) => key !== 'total') <input
.map(([key, value]) => ( type="file"
<div key={key} className="p-2 bg-gray-700 text-gray-200 rounded"> accept="image/*"
{formatResultLabel(key)}: {value.toFixed(1)} onChange={handlePhotoChange}
</div> className="
))} bg-gray-700
<div className="p-2 bg-gray-600 font-semibold text-gray-200 rounded"> text-gray-200
Итого себестоимость: {result.total.toFixed(1)} rounded-md
</div> border border-gray-600
<div className="p-2 bg-gray-700 text-gray-200 rounded"> p-2
Итоговая цена с наценкой: {finalPrice.toFixed(1)} focus:outline-none focus:border-sky-500
</div> "
{weight > 0 && ( />
<div className="p-2 bg-gray-700 text-gray-200 rounded"> </label>
Цена за 100 г: {pricePer100g.toFixed(1)} {photoFile && (
<img
src={URL.createObjectURL(photoFile)}
alt="Предпросмотр"
className="w-32 h-32 object-cover rounded-lg border border-sky-400"
/>
)}
{/* Группированные поля */}
{Object.entries(groupedFields)
.filter(([group]) => group !== 'general')
.map(([group, fields]) => (
<div key={group} className="space-y-4">
<h2 className="text-xl font-semibold text-gray-300 capitalize">
{group === 'packaging' ? 'Упаковка' : group}
</h2>
<div className="grid grid-cols-2 gap-4">
{fields.map((field) => (
<FormField
key={field.id}
field={field}
value={formData[field.name] || ''}
onChange={(value) => updateField(field.name, value)}
/>
))}
</div>
</div>
))}
{/* Блоки с результатами расчёта */}
<div className="space-y-2">
{Object.entries(result)
.filter(([key]) => key !== 'total')
.map(([key, value]) => (
<CostBlock
key={key}
title={formatResultLabel(key)}
value={value}
/>
))}
<CostBlock title="Итого себестоимость" value={result.total} highlight />
<CostBlock title="Итоговая цена с наценкой" value={finalPrice} />
{weight > 0 && (
<CostBlock title="Цена за 100 г" value={pricePer100g} />
)}
</div> </div>
)} </>
</div> )}
{/* Кнопка отправки */} {/* Кнопка отправки */}
<button <button