Назад к проектамтекущий проект

Arbat Beauty CRM

Основной рабочий проект с мая 2025 года. Внутренняя CRM, в которой ежедневно работают администраторы салонов: запись клиентов, расписание мастеров, складской учёт, расчёт зарплат. Я первый контрибьютор проекта — веду миграцию легаси-фронтенда на React 19 и TypeScript, проектирую архитектуру по FSD и закрываю модули целиком, от макета до релиза.

  • React 19
  • TypeScript
  • Vite
  • TanStack Query
  • Zustand
  • React Hook Form
  • React Router
  • Sass
  • Storybook
  • Vitest
  • Playwright
  • Первый контрибьютор проекта: 1486 коммитов за 15 месяцев работы.
  • Своя реализация drag-and-drop на mouse-событиях вместо готовой библиотеки — вынесена в shared-хук и переиспользуется в сетке расписания и в общей таблице данных.
  • Веду модули целиком: расписание мастеров, визиты и клиенты, склад и технологические карты, зарплаты и выплаты.
  • Архитектура Feature-Sliced Design: 10 доменных сущностей с изолированными слоями.
  • Тестовая пирамида: Vitest на unit и компоненты, Playwright на e2e, 34 Storybook-стори.

Как это было устроено

Внутренний продукт для ежедневной работы администраторов сети салонов: запись клиентов, расписание мастеров, складской учёт расходников, расчёт зарплат сотрудников.

Задача

  • Легаси-фронтенд на JavaScript с дублированием бизнес-логики: одно изменение приходилось вносить в нескольких местах.
  • Расписание — самый нагруженный экран продукта: десятки сотрудников, месячная сетка, перестановка строк и смен без перезагрузки.
  • Готовые библиотеки drag-and-drop не подошли: нативный HTML5-драг решает о старте перетаскивания раньше, чем React успевает пометить строку, и тащит неуправляемый ghost с иконкой «копировать».
  • Переписывать модули нужно было без остановки поставки фич для бизнеса.

Решение

  • Переход на React 19 + TypeScript и декомпозиция по Feature-Sliced Design: 10 доменных сущностей вместо общей свалки компонентов.
  • Разделение server-state и client-state: TanStack Query для данных с сервера, Zustand для состояния интерфейса.
  • Собственный хук перестановки строк на mouse-событиях: копия строки летит за курсором, соседи разъезжаются через CSS-transform напрямую в DOM, минуя ререндер React.
  • Общие компоненты вместо копипасты: таблица данных, формы на React Hook Form и единый набор контролов подключены во всех модулях.
  • Тестовое покрытие на трёх уровнях, чтобы миграция не ломала уже работающие экраны.

Вклад в проект

1486 коммитов, первый контрибьютор

Срок

15 месяцев непрерывной работы

Архитектура

10 доменных сущностей по FSD

Переиспользование

Drag-and-drop работает в 2 модулях из одного хука

Перестановка строк расписания: соседи разъезжаются через transform
// Трансформации пишутся напрямую в DOM: проход через React означал бы
// ререндер всех строк на каждую пересечённую, а строка расписания — это
// ячейка сотрудника плюс по ячейке на каждый день месяца.
const applyShifts = (target: number) => {
  const pitch = pitchRef.current;
  const from = draggedRef.current;
  if (from === null || !pitch) return;

  rowElementsRef.current.forEach((elements, rowIndex) => {
    let shift = 0;
    if (rowIndex === from) shift = (target - from) * pitch;
    else if (rowIndex > from && rowIndex <= target) shift = -pitch;
    else if (rowIndex < from && rowIndex >= target) shift = pitch;

    elements.forEach((element) => {
      element.style.transform = shift ? `translateY(${shift}px)` : '';
    });
  });
};

const settle = () => {
  // Новый порядок и снятые трансформации должны попасть на экран в одном
  // кадре: применённые друг за другом, они дают моргание старым порядком.
  flushSync(() => {
    setDraggedIndex(null);
    if (from !== null && to !== null && from !== to) onReorder(from, to);
  });
};
Визуал компонента
Расписание мастера
10:30Анна Петроваnew
12:00Мария Ивановаconfirmed