Created modular system for calculators, added soap and candles calculators, universal components, updated backend
387 lines
12 KiB
Markdown
387 lines
12 KiB
Markdown
# Руководство по созданию новых калькуляторов
|
||
|
||
Это руководство поможет вам быстро создать новый калькулятор в модульной системе.
|
||
|
||
## Структура модуля калькулятора
|
||
|
||
Каждый калькулятор состоит из двух файлов в папке `frontend/calculators/[название]/`:
|
||
|
||
- `config.ts` — конфигурация калькулятора (поля, формулы, подитоги)
|
||
- `calc.ts` — функции расчета (опционально, если нужны сложные вычисления)
|
||
|
||
## Шаг 1: Создание папки модуля
|
||
|
||
Создайте папку для вашего калькулятора:
|
||
|
||
```
|
||
frontend/calculators/candles/
|
||
```
|
||
|
||
## Шаг 2: Создание файла calc.ts (если нужны функции расчета)
|
||
|
||
Если у вас простые формулы, можно обойтись без этого файла и указать формулы прямо в конфигурации.
|
||
|
||
Пример для калькулятора свечей:
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
Это основной файл конфигурации калькулятора:
|
||
|
||
```typescript
|
||
// 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` и добавьте регистрацию:
|
||
|
||
```typescript
|
||
import { candlesCalculatorConfig } from '@/calculators/candles/config';
|
||
|
||
// В функции initializeCalculators():
|
||
registerCalculator(candlesCalculatorConfig);
|
||
```
|
||
|
||
## Типы полей
|
||
|
||
### text
|
||
Текстовое поле ввода:
|
||
```typescript
|
||
{
|
||
id: 'productName',
|
||
type: 'text',
|
||
label: 'Название продукта',
|
||
defaultValue: '',
|
||
gridCols: 1, // 1 или 2 колонки
|
||
}
|
||
```
|
||
|
||
### number
|
||
Числовое поле ввода:
|
||
```typescript
|
||
{
|
||
id: 'weight',
|
||
type: 'number',
|
||
label: 'Вес, г',
|
||
defaultValue: '',
|
||
gridCols: 2,
|
||
validation: {
|
||
min: 0,
|
||
max: 10000,
|
||
},
|
||
// Группировка полей (опционально)
|
||
groupName: 'base', // Имя группы для группировки связанных полей
|
||
showStepAfter: 'base', // ID шага расчета, который показать после этой группы
|
||
}
|
||
```
|
||
|
||
### file
|
||
Поле для загрузки файла (обычно фото):
|
||
```typescript
|
||
{
|
||
id: 'photo',
|
||
type: 'file',
|
||
label: 'Фото продукта',
|
||
accept: 'image/*',
|
||
gridCols: 1,
|
||
}
|
||
```
|
||
|
||
## Группировка полей
|
||
|
||
Для правильного расположения блоков расчета сразу после соответствующих полей используйте группировку:
|
||
|
||
```typescript
|
||
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" автоматически появится здесь
|
||
]
|
||
```
|
||
|
||
## Формулы расчета
|
||
|
||
Формулы — это функции, которые принимают значения полей и возвращают число:
|
||
|
||
```typescript
|
||
{
|
||
id: 'baseCost',
|
||
name: 'Базовая стоимость',
|
||
formula: (values) => {
|
||
const weight = values.weight || 0;
|
||
const price = values.price || 0;
|
||
return weight * price;
|
||
},
|
||
formulaDescription: 'вес * цена', // Опционально, для документации
|
||
}
|
||
```
|
||
|
||
В формуле доступны все значения полей через объект `values`, где ключ — это `id` поля.
|
||
|
||
## Подитоги
|
||
|
||
Подитоги могут зависеть от шагов расчета:
|
||
|
||
```typescript
|
||
{
|
||
id: 'total',
|
||
name: 'Итого',
|
||
formula: (values, steps) => {
|
||
// values — значения полей
|
||
// steps — результаты всех шагов расчета
|
||
return (steps.base || 0) + (steps.packaging || 0);
|
||
},
|
||
highlight: true, // Выделить итоговое значение
|
||
}
|
||
```
|
||
|
||
## Дополнительные расчеты
|
||
|
||
Используются для расчетов, которые зависят от подитогов или других дополнительных расчетов:
|
||
|
||
```typescript
|
||
{
|
||
id: 'finalPrice',
|
||
name: 'Итоговая цена',
|
||
formula: (values, steps, subtotals, additional) => {
|
||
const total = subtotals.total || 0;
|
||
const markup = values.markup || 0;
|
||
return total * (1 + markup / 100);
|
||
},
|
||
}
|
||
```
|
||
|
||
**Важно**: Дополнительные расчеты выполняются последовательно. Если один расчет зависит от другого, расположите зависимый расчет **после** того, от которого он зависит:
|
||
|
||
```typescript
|
||
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`, будет использовано универсальное форматирование. Но лучше создать свою функцию:
|
||
|
||
```typescript
|
||
formatTelegramMessage: (values, steps, subtotals, additional) => {
|
||
// values — все поля формы (строки или числа)
|
||
// steps — результаты шагов расчета
|
||
// subtotals — результаты подитогов
|
||
// additional — результаты дополнительных расчетов
|
||
|
||
let text = `📊 <b>Расчёт:</b>\n\n`;
|
||
text += `Итого: ${subtotals.total.toFixed(1)} ₽\n`;
|
||
|
||
return text;
|
||
}
|
||
```
|
||
|
||
## Проверка работы
|
||
|
||
1. Убедитесь, что калькулятор появился в меню на главной странице
|
||
2. Заполните форму и проверьте расчеты
|
||
3. Отправьте результат в Telegram и убедитесь, что сообщение корректно форматировано
|
||
|
||
## Советы
|
||
|
||
- Используйте функцию `round()` для округления результатов (обычно до 1 знака после запятой)
|
||
- Для сложных расчетов выносите логику в `calc.ts`
|
||
- Группируйте связанные поля визуально, используя `gridCols`
|
||
- Всегда указывайте `formulaDescription` для документации
|
||
- Тестируйте с нулевыми и пустыми значениями
|
||
|
||
## Примеры
|
||
|
||
Полные примеры калькуляторов можно найти в:
|
||
|
||
**Калькулятор мыла:**
|
||
- `frontend/calculators/soap/config.ts`
|
||
- `frontend/calculators/soap/calc.ts`
|
||
|
||
**Калькулятор свечей (пример простого калькулятора):**
|
||
- `frontend/calculators/candles/config.ts`
|
||
- `frontend/calculators/candles/calc.ts`
|
||
|
||
Оба примера демонстрируют использование группировки полей, шагов расчета, подитогов и дополнительных расчетов.
|
||
|