Frontend Engineer · Июль 2025 — 2026

UnitBox · Админка

React 19 · Next.js 15 · shadcn/ui · TypeScript · Nest.js

Что это и что я делал

У UnitBox две половины. Агенты видят платформу: каталог, офферы, финмодели. Застройщик видит админку — здесь заводят проекты, ведут листинги на сотни юнитов, ставят цены и раздают доступ. Это девелоперская сторона: developer-side, операционное сердце всего продукта.

Именно здесь платформа выигрывает или проваливается: любая устаревшая цена, которую увидит инвестор, тянется к трению в этой панели. Моя часть — код: я построил админку, ~95% её кода, включая пересборку на shadcn. Дальше — фича за фичей: что делает, как собрано, зачем нужно.

Живой инвентарь, а не CMS

Проект — это один дом или комплекс вилл, и в нём может быть до 500 юнитов. Каждому нужны обложка, планировка, галерея, параметры, финансовая модель, планы оплат. Никто не станет заполнять это пятьсот раз руками, а если попробует — данные протухнут при первом же движении цен.

В этом и настоящее ограничение: это живой инвентарь. Цены двигаются посреди стройки, юниты бронируют и освобождают каждый день, а у отдела продаж уже есть своя система. Админка, которая просит сделать ту же работу дважды, просто не приживётся. Каждый пропуск превращается в устаревшую цифру перед инвестором — а против неё платформа и продаёт.

Решение, на котором стоит вся админка: тип юнита

Тип юнита — это шаблон, планировка. Он владеет контентом и дефолтами: спальни, санузлы, этажность, базовая цена, площадь, галереи, финмодель. Сам юнит владеет только тем, что делает его физически собой: номер, этаж, блок, статус, цена. Назначил тип — остальное приезжает само. Поменял базовую цену планировки один раз — обновились все юниты этого типа. И при этом юнит может переопределить что угодно: вид на океан, террасу побольше, свою цену.

Тип юнита — шаблон: спальни, санузлы, этажность, цена по умолчанию

Наследование: цепочка длиннее, чем кажется

Каскад устроен так: регион → проект → блок → тип юнита → конфигурация → юнит. Регион решает, какие поля вообще существуют: у проекта на Бали есть выбор типа земли, у тайского его нет. Конфигурация — это вариация типа: линейка «с видом на океан» той же планировки, два переопределённых поля, а не скопированный тип.

Наследуется не каждое поле по отдельности, а целыми блоками. Контент, планы оплат и финмодель отвязываются от типа одним тумблером «Inheritance» — блок разом перестаёт течь от родителя. Базовые поля вроде этажа и цены живут у самого юнита; спальни и санузлы приходят из типа. Внутри это сделано не абстрактным движком каскада, а флагом синхронизации на каждый раздел: пока блок «течёт», значения читаются от родителя; отвязка снимает флаг и снимает снимок текущих значений вниз, в сам юнит, а обратная привязка просто возвращает флаг. Оверрайд рвёт связь с родителем, ресет — восстанавливает.

Юнит наследует от типа и переопределяет поля; рядом — история изменений

Как заполняется проект — сверху вниз по цепочке

  • создать проект с общими параметрами: условия, планы оплат, финмодель
  • описать типы юнитов
  • добавить конфигурации там, где есть варианты
  • загрузить юниты массово файлом, каждая строка привязывает юнит к конфигурации
  • переопределить отдельные поля только там, где юнит и правда особенный

Одна настройка закрывает проект целиком, а исключения остаются исключениями, а не превращаются в копии.

Конструктор финмодели

Финмодель — то, чем агент защищает цифры доходности перед инвестором, поэтому она должна быть строгой и редактируемой одновременно. Это конструктор: каналы аренды (OTA против прямых продаж, со своими комиссиями), расходы, которые считаются от выручки и отдельно от прибыли, налоги, график капитализации (рост стоимости на годы вперёд, с пресетами), сезонность спроса (двенадцать месячных коэффициентов), выход на мощность, индексация цены. Всё это отдельные секции, собранные в один экран, со своим состоянием в Redux.

Конструктор финмодели: каналы продаж, рост стоимости, сезонность спроса

Планы оплат по фазам

Планы оплат едут по тому же водопаду наследования, что и финмодель: полная оплата со скидкой или рассрочка, привязанная к этапам стройки. Фазы — Booking, Construction, Handover, Post-Handover; у стройки и пост-хэндовера свои режимы (до сдачи, до сдачи с лимитом, произвольный график; частотами, произвольно или из аренды). Планы живут на уровне проекта, типа и юнита с тем же наследованием. Каждый план пересчитывается от цены юнита автоматически и кормит математику доходности: рассрочка меняет IRR, и калькулятор это показывает.

Планы оплаты по фазам: Booking · Construction · Handover · Post-Handover

Настройки и контент проекта

Проект начинается с двух вкладок. Настройки держат валюту, локацию и базовые параметры; Контент — то, что рендерится на публичной странице проекта: медиа, мастерплан, 3D-виджеты, инфраструктуру, что вокруг. Здесь намеренно нет ничего умного: контент-менеджеры сидят в этих экранах каждый день, и именно плоская структура держит их понятными.

Настройки и контент проекта — мастерплан с точками интереса, галерея

Таблица как inventory-софт: bulk и история

Застройщик ведёт здесь живой инвентарь: цены растут посреди стройки, юниты бронируют и освобождают каждый день. Поэтому таблица работает как складской софт. Мультиселект вызывает нижний тулбар, и статус, тип, блок, цена или площадь меняются сразу у десятков юнитов. Цену можно двигать процентом — «поднять этот этаж на 5%» это одно действие, а не сорок правок, — а любой bulk откатывается одним кликом. Откат сделан на снимках: перед применением собирается snapshot прежних значений, правки идут батчами с прогресс-тостом, а в тосте живёт кнопка «отменить», которая прогоняет обратный проход из этого снимка.

Большие каталоги приезжают файлом: импорт «Upload JSON» разбирает массив строк, сопоставляет названия фич и видов с реальными id по нечёткому совпадению имени и создаёт сотни юнитов разом, показав их в ревью-гриде перед сохранением. А поскольку одни и те же данные трогает много рук, у каждого юнита есть своя история изменений полей: поле, старое и новое значение, кто поменял и когда, с бесконечной подгрузкой. Автор в истории разбирается отдельно — обычный пользователь, e-mail, загрузчик или CRM-актор (Kommo, amo, Gene) со своей иконкой и ссылкой прямо в сделку. Когда цена выглядит странно, ответ в одном клике, а не в расследовании.

Мультиселект: цена сразу по трём юнитам, +10%, с уведомлением и отменой

Векторный редактор фасадов

Кликабельный фасад — «найди свой юнит на рендере» — застройщики на этом рынке раньше заказывали как отдельную услугу, а потом перерисовывали вручную при каждой смене нумерации. Моё ограничение было такое: застройщик размечает свои планы сам, без нас и без дизайнера. Поэтому в админке есть маленький векторный редактор: инструменты move, pen, bend, lasso. Загружаешь рендер, обводишь контуры, привязываешь каждую фигуру к своему юниту. Фигура считается готовой, только когда её последняя точка встречает первую — путь замыкается сам. Наведение на фигуру подсвечивает её строку в списке и наоборот; на этаже из сорока юнитов это и есть разница между «пользуемо» и «нет».

Внутри это гибрид: фон-рендер держит Konva (пан, зум, пинч, кроп по канвасу), а редактируемая геометрия рисуется своим SVG-слоем. Построитель пути отдаёт M/L для прямых, квадратичную Q и кубическую C-кривую там, где инструмент bend превращает отрезок в безье; привязка точек идёт через пространственную сетку с поиском ближайшего угла, а сзади всё держит своя история изменений на замыканиях do/undo, с коалесингом драгов и лимитом в двести шагов. Ядро геометрии и состояние покрыты юнит-тестами.

Два правила пришли из наблюдения за реальными проектами. Три уровня схлопываются, когда проекту они не нужны: один блок — мастерплан необязателен, один этаж — уровень этажа исчезает, и вилла размечается в один проход. А удаление намеренно асимметрично: снёс блок — забрал его фигуры с собой, но этаж, к которому ещё привязаны юниты, переживает уменьшение числа этажей. Потерять час обводки из-за опечатки в числе — так инструмент теряет чьё-то доверие насовсем. Час настройки один раз — а дальше статусы приходят из CRM, и картинка поддерживает себя сама.

Контур вручную по фасаду — перо, безье, лассо
Та же карта на стороне покупателя — кликабельные юниты поверх рендера

Аналитика

Отсюда руководитель продаж ведёт команду: кто пользовался платформой на этой неделе, какие офферы ушли, открыл ли их инвестор, активность по каждому агенту во времени, география посетителей (флаг страны собирается из ISO-кода). Это не графики ради графиков — они отвечают на вопрос, который застройщики задавали нам на каждом интервью: какие агенты реально продают. Чарты на recharts.

Аналитика: время на платформе, просмотры офферов, география посетителей

Люди и права

Админка несёт ещё и людей. Сотрудников зовут по e-mail с ролью; агенты регистрируются сами и попадают в очередь на подтверждение — застройщик одобряет из админки или прямо из Telegram, одним тапом. У каждого пользователя профиль, след активности и возможность архивации, когда он уходит.

Что видит каждый посетитель — это матрица, а не код. Для незарегистрированных, зарегистрированных и проверенных гостей застройщик включает и выключает проекты, цены, документы, финмодели, комиссии и фиксацию лида. Матрица — дерево зависимостей: выключил юниты, и всё под ними выключается тоже; включение более широкой роли само включает узкие. А контент говорит на восьми языках: любое текстовое поле разворачивается в поле-на-язык (английский основной, плюс русский, турецкий, французский, немецкий, венгерский, индонезийский, арабский), и застройщик сам выбирает, на каких языках говорит его каталог и какой из них главный. Богатый текст — на TipTap.

Валюты и интеграции: замкнуть контур

Настройки платформы замыкают контур: валюты с живыми курсами, брендинг, SEO. Проект может быть в любой валюте — все валюты мира, — а дефолтная каскадом прокидывается от типа юнита к проекту и к застройщику.

Но настоящая граница админки — это синк с CRM. Отдел продаж живёт в своей системе, и если забронированный там юнит остаётся зелёным на портале, портал врёт уже через день. Поэтому админка держит интеграции с тремя CRM — amoCRM, Kommo и Gene, — с маппингом воронок и стадий. Виджет, который оказался важнее самого синка, — это пикер юнитов, встроенный внутрь карточки сделки самой CRM (отдельный вэнилла-JS-виджет под amo и Kommo, который сам определяет систему по домену и подтягивает юнит из полей сделки через iframe). А статус юнита пересчитывается из всех сделок на нём разом, где sold старше reserved, а reserved старше available, — иначе последний вебхук «побеждал» и юнит мог позеленеть, пока его ещё держит другая сделка.

Валюты, права доступа по ролям, интеграции с CRM

Пересборка на shadcn

Первая версия админки жила на Mantine. В 2026-м я пересобрал её на shadcn/ui — новый UI это немалая часть работы над админкой, но далеко не вся: за интерфейсом стоит масса логики, а не только вёрстка. React 19, Next.js 15, около ста двадцати базовых компонентов, TanStack Table под таблицы, Konva под фасады, recharts под аналитику, dnd-kit под перетаскивания, TipTap под текст. Собрано атомарно: сначала кастомизированные базовые компоненты, потом блоки, потом целые шаблоны страниц, переиспользуемые так, что ничего не существует дважды. Смысл — чтобы новый экран собирался из готового, а не рисовался с нуля.

Что получилось

Бэк-офис для локальных отделов продаж — это складской софт, а не CMS. Bulk-операции над десятками юнитов, один источник правды и дефолты, которые приезжают сами через наследование, решают, будут ли инструментом вообще пользоваться. А фасад, размеченный за час, дальше обновляет себя из CRM.

~95% мойКод админки
до 500Юнитов в проекте
8Языки контента
все валюты мираВалюты