fix: Восстановлена оригинальная структура формы мыла и исправлена обработка chat_id
- Восстановлена структура формы мыла с промежуточными стоимостями под каждым блоком - Добавлен компонент CostBlock для отображения промежуточных стоимостей - Улучшена обработка chat_id при отправке (проверка URL и sessionStorage) - Улучшена навигация между калькуляторами с более понятным интерфейсом - Добавлена подсказка для пользователя о переключении калькуляторов
This commit is contained in:
parent
186c85738d
commit
fc7c42861c
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
24
frontend/components/CostBlock.tsx
Normal file
24
frontend/components/CostBlock.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@ -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,6 +288,137 @@ export default function DynamicCalculator({ calculatorType }: DynamicCalculatorP
|
|||||||
Калькулятор: {calculator.name}
|
Калькулятор: {calculator.name}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
{/* Специфичная структура для мыла с промежуточными стоимостями */}
|
||||||
|
{calculator.id === 'soap' && (
|
||||||
|
<>
|
||||||
|
{/* Название мыла */}
|
||||||
|
<FormField
|
||||||
|
field={calculator.fieldSchema.find((f) => f.name === 'soapName')!}
|
||||||
|
value={formData.soapName || ''}
|
||||||
|
onChange={(value) => updateField('soapName', 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>
|
||||||
|
{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} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Универсальная структура для других калькуляторов */}
|
||||||
|
{calculator.id !== 'soap' && (
|
||||||
|
<>
|
||||||
{/* Основные поля (без группы) */}
|
{/* Основные поля (без группы) */}
|
||||||
{groupedFields.general && (
|
{groupedFields.general && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@ -312,7 +458,7 @@ export default function DynamicCalculator({ calculatorType }: DynamicCalculatorP
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Группированные поля (например, упаковка) */}
|
{/* Группированные поля */}
|
||||||
{Object.entries(groupedFields)
|
{Object.entries(groupedFields)
|
||||||
.filter(([group]) => group !== 'general')
|
.filter(([group]) => group !== 'general')
|
||||||
.map(([group, fields]) => (
|
.map(([group, fields]) => (
|
||||||
@ -338,22 +484,20 @@ export default function DynamicCalculator({ calculatorType }: DynamicCalculatorP
|
|||||||
{Object.entries(result)
|
{Object.entries(result)
|
||||||
.filter(([key]) => key !== 'total')
|
.filter(([key]) => key !== 'total')
|
||||||
.map(([key, value]) => (
|
.map(([key, value]) => (
|
||||||
<div key={key} className="p-2 bg-gray-700 text-gray-200 rounded">
|
<CostBlock
|
||||||
{formatResultLabel(key)}: {value.toFixed(1)} ₽
|
key={key}
|
||||||
</div>
|
title={formatResultLabel(key)}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
<div className="p-2 bg-gray-600 font-semibold text-gray-200 rounded">
|
<CostBlock title="Итого себестоимость" value={result.total} highlight />
|
||||||
Итого себестоимость: {result.total.toFixed(1)} ₽
|
<CostBlock title="Итоговая цена с наценкой" value={finalPrice} />
|
||||||
</div>
|
|
||||||
<div className="p-2 bg-gray-700 text-gray-200 rounded">
|
|
||||||
Итоговая цена с наценкой: {finalPrice.toFixed(1)} ₽
|
|
||||||
</div>
|
|
||||||
{weight > 0 && (
|
{weight > 0 && (
|
||||||
<div className="p-2 bg-gray-700 text-gray-200 rounded">
|
<CostBlock title="Цена за 100 г" value={pricePer100g} />
|
||||||
Цена за 100 г: {pricePer100g.toFixed(1)} ₽
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Кнопка отправки */}
|
{/* Кнопка отправки */}
|
||||||
<button
|
<button
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user