DoSoapCalc/frontend/docs/calculator-creation-guide.md
DosAi 02c7520c90 Refactor: Modular calculator architecture
Created modular system for calculators, added soap and candles calculators, universal components, updated backend
2025-11-02 15:45:07 +03:00

12 KiB
Raw Permalink Blame History

Руководство по созданию новых калькуляторов

Это руководство поможет вам быстро создать новый калькулятор в модульной системе.

Структура модуля калькулятора

Каждый калькулятор состоит из двух файлов в папке 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;
}

Проверка работы

  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

Оба примера демонстрируют использование группировки полей, шагов расчета, подитогов и дополнительных расчетов.