Created modular system for calculators, added soap and candles calculators, universal components, updated backend
12 KiB
Руководство по созданию новых калькуляторов
Это руководство поможет вам быстро создать новый калькулятор в модульной системе.
Структура модуля калькулятора
Каждый калькулятор состоит из двух файлов в папке frontend/calculators/[название]/:
config.ts— конфигурация калькулятора (поля, формулы, подитоги)calc.ts— функции расчета (опционально, если нужны сложные вычисления)
Шаг 1: Создание папки модуля
Создайте папку для вашего калькулятора:
frontend/calculators/candles/
Шаг 2: Создание файла calc.ts (если нужны функции расчета)
Если у вас простые формулы, можно обойтись без этого файла и указать формулы прямо в конфигурации.
Пример для калькулятора свечей:
// calculators/candles/calc.ts
export function calculateCandleStep(
stepId: string,
values: Record<string, number>
): number {
const { waxWeight = 0, waxPrice = 0, wickCount = 0, wickPrice = 0 } = values;
switch (stepId) {
case 'wax':
return (waxWeight / 1000) * waxPrice;
case 'wick':
return wickCount * wickPrice;
default:
return 0;
}
}
export function round(val: number): number {
return Math.round(val * 10) / 10;
}
Шаг 3: Создание файла config.ts
Это основной файл конфигурации калькулятора:
// calculators/candles/config.ts
import { CalculatorConfig } from '@/lib/calculator-types';
import { calculateCandleStep, round } from './calc';
export const candlesCalculatorConfig: CalculatorConfig = {
id: 'candles',
name: 'Калькулятор свечей',
description: 'Расчет себестоимости свечей',
icon: '🕯️',
// Поля ввода
fields: [
{
id: 'candleName',
type: 'text',
label: 'Название свечи',
placeholder: 'Введите название',
defaultValue: '',
gridCols: 1,
required: false,
},
{
id: 'waxWeight',
type: 'number',
label: 'Вес воска, г',
defaultValue: '',
gridCols: 2,
},
{
id: 'waxPrice',
type: 'number',
label: 'Цена воска за 1 кг, руб',
defaultValue: '',
gridCols: 2,
},
{
id: 'wickCount',
type: 'number',
label: 'Количество фитилей',
defaultValue: '',
gridCols: 2,
},
{
id: 'wickPrice',
type: 'number',
label: 'Цена одного фитиля, руб',
defaultValue: '',
gridCols: 2,
},
],
// Шаги расчета
calculationSteps: [
{
id: 'wax',
name: 'Себестоимость воска',
formula: (values) => round(calculateCandleStep('wax', values)),
formulaDescription: '(вес_воска / 1000) * цена_воска',
},
{
id: 'wick',
name: 'Себестоимость фитилей',
formula: (values) => round(calculateCandleStep('wick', values)),
formulaDescription: 'количество_фитилей * цена_фитиля',
},
],
// Подитоги
subtotals: [
{
id: 'total',
name: 'Итого себестоимость',
formula: (values, steps) => {
return round((steps.wax || 0) + (steps.wick || 0));
},
highlight: true,
formulaDescription: 'воск + фитили',
},
],
// Дополнительные расчеты (опционально)
additionalCalculations: [
{
id: 'pricePerCandle',
name: 'Цена за свечу',
formula: (values, steps, subtotals) => {
const total = subtotals.total || 0;
return round(total);
},
},
],
// Форматирование сообщения для Telegram (опционально)
formatTelegramMessage: (values, steps, subtotals, additional) => {
const candleName = values.candleName || 'Без названия';
const totalCost = subtotals.total || 0;
let text = `🕯️ <b>Расчёт свечи:</b> <i>${candleName}</i>\n\n`;
text += `📊 <b>Итоги расчёта:</b>\n`;
text += ` 💵 Себестоимость: ${totalCost.toFixed(1)} ₽\n`;
return text;
},
};
Шаг 4: Регистрация калькулятора
Откройте файл frontend/lib/calculator-registry.ts и добавьте регистрацию:
import { candlesCalculatorConfig } from '@/calculators/candles/config';
// В функции initializeCalculators():
registerCalculator(candlesCalculatorConfig);
Типы полей
text
Текстовое поле ввода:
{
id: 'productName',
type: 'text',
label: 'Название продукта',
defaultValue: '',
gridCols: 1, // 1 или 2 колонки
}
number
Числовое поле ввода:
{
id: 'weight',
type: 'number',
label: 'Вес, г',
defaultValue: '',
gridCols: 2,
validation: {
min: 0,
max: 10000,
},
// Группировка полей (опционально)
groupName: 'base', // Имя группы для группировки связанных полей
showStepAfter: 'base', // ID шага расчета, который показать после этой группы
}
file
Поле для загрузки файла (обычно фото):
{
id: 'photo',
type: 'file',
label: 'Фото продукта',
accept: 'image/*',
gridCols: 1,
}
Группировка полей
Для правильного расположения блоков расчета сразу после соответствующих полей используйте группировку:
fields: [
// Группа "base" - после этих полей покажется расчет "base"
{
id: 'weight',
type: 'number',
label: 'Вес, г',
groupName: 'base',
showStepAfter: 'base', // Первое поле группы должно иметь showStepAfter
},
{
id: 'price',
type: 'number',
label: 'Цена, руб',
groupName: 'base', // Та же группа
},
// Блок расчета "base" автоматически появится здесь
// Группа "packaging"
{
id: 'box',
type: 'number',
label: 'Коробка, руб',
groupName: 'packaging',
showStepAfter: 'packaging',
},
{
id: 'ribbon',
type: 'number',
label: 'Лента, руб',
groupName: 'packaging',
},
// Блок расчета "packaging" автоматически появится здесь
]
Формулы расчета
Формулы — это функции, которые принимают значения полей и возвращают число:
{
id: 'baseCost',
name: 'Базовая стоимость',
formula: (values) => {
const weight = values.weight || 0;
const price = values.price || 0;
return weight * price;
},
formulaDescription: 'вес * цена', // Опционально, для документации
}
В формуле доступны все значения полей через объект values, где ключ — это id поля.
Подитоги
Подитоги могут зависеть от шагов расчета:
{
id: 'total',
name: 'Итого',
formula: (values, steps) => {
// values — значения полей
// steps — результаты всех шагов расчета
return (steps.base || 0) + (steps.packaging || 0);
},
highlight: true, // Выделить итоговое значение
}
Дополнительные расчеты
Используются для расчетов, которые зависят от подитогов или других дополнительных расчетов:
{
id: 'finalPrice',
name: 'Итоговая цена',
formula: (values, steps, subtotals, additional) => {
const total = subtotals.total || 0;
const markup = values.markup || 0;
return total * (1 + markup / 100);
},
}
Важно: Дополнительные расчеты выполняются последовательно. Если один расчет зависит от другого, расположите зависимый расчет после того, от которого он зависит:
additionalCalculations: [
{
id: 'finalPrice',
name: 'Итоговая цена с наценкой',
formula: (values, steps, subtotals) => {
const total = subtotals.total || 0;
const markup = values.markup || 0;
return total * (1 + markup / 100);
},
},
{
id: 'pricePer100g',
name: 'Цена за 100 г',
formula: (values, steps, subtotals, additional) => {
// Используем уже рассчитанный finalPrice из additional
const finalPrice = additional?.finalPrice || 0;
const weight = values.weight || 0;
if (weight > 0) {
return (finalPrice / weight) * 100;
}
return 0;
},
},
]
Форматирование сообщения для Telegram
Если не указать formatTelegramMessage, будет использовано универсальное форматирование. Но лучше создать свою функцию:
formatTelegramMessage: (values, steps, subtotals, additional) => {
// values — все поля формы (строки или числа)
// steps — результаты шагов расчета
// subtotals — результаты подитогов
// additional — результаты дополнительных расчетов
let text = `📊 <b>Расчёт:</b>\n\n`;
text += `Итого: ${subtotals.total.toFixed(1)} ₽\n`;
return text;
}
Проверка работы
- Убедитесь, что калькулятор появился в меню на главной странице
- Заполните форму и проверьте расчеты
- Отправьте результат в Telegram и убедитесь, что сообщение корректно форматировано
Советы
- Используйте функцию
round()для округления результатов (обычно до 1 знака после запятой) - Для сложных расчетов выносите логику в
calc.ts - Группируйте связанные поля визуально, используя
gridCols - Всегда указывайте
formulaDescriptionдля документации - Тестируйте с нулевыми и пустыми значениями
Примеры
Полные примеры калькуляторов можно найти в:
Калькулятор мыла:
frontend/calculators/soap/config.tsfrontend/calculators/soap/calc.ts
Калькулятор свечей (пример простого калькулятора):
frontend/calculators/candles/config.tsfrontend/calculators/candles/calc.ts
Оба примера демонстрируют использование группировки полей, шагов расчета, подитогов и дополнительных расчетов.