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, и калькулятор это показывает.
Настройки и контент проекта
Проект начинается с двух вкладок. Настройки держат валюту, локацию и базовые параметры; Контент — то, что рендерится на публичной странице проекта: медиа, мастерплан, 3D-виджеты, инфраструктуру, что вокруг. Здесь намеренно нет ничего умного: контент-менеджеры сидят в этих экранах каждый день, и именно плоская структура держит их понятными.
Таблица как inventory-софт: bulk и история
Застройщик ведёт здесь живой инвентарь: цены растут посреди стройки, юниты бронируют и освобождают каждый день. Поэтому таблица работает как складской софт. Мультиселект вызывает нижний тулбар, и статус, тип, блок, цена или площадь меняются сразу у десятков юнитов. Цену можно двигать процентом — «поднять этот этаж на 5%» это одно действие, а не сорок правок, — а любой bulk откатывается одним кликом. Откат сделан на снимках: перед применением собирается snapshot прежних значений, правки идут батчами с прогресс-тостом, а в тосте живёт кнопка «отменить», которая прогоняет обратный проход из этого снимка.
Большие каталоги приезжают файлом: импорт «Upload JSON» разбирает массив строк, сопоставляет названия фич и видов с реальными id по нечёткому совпадению имени и создаёт сотни юнитов разом, показав их в ревью-гриде перед сохранением. А поскольку одни и те же данные трогает много рук, у каждого юнита есть своя история изменений полей: поле, старое и новое значение, кто поменял и когда, с бесконечной подгрузкой. Автор в истории разбирается отдельно — обычный пользователь, e-mail, загрузчик или CRM-актор (Kommo, amo, Gene) со своей иконкой и ссылкой прямо в сделку. Когда цена выглядит странно, ответ в одном клике, а не в расследовании.
Векторный редактор фасадов
Кликабельный фасад — «найди свой юнит на рендере» — застройщики на этом рынке раньше заказывали как отдельную услугу, а потом перерисовывали вручную при каждой смене нумерации. Моё ограничение было такое: застройщик размечает свои планы сам, без нас и без дизайнера. Поэтому в админке есть маленький векторный редактор: инструменты 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, — иначе последний вебхук «побеждал» и юнит мог позеленеть, пока его ещё держит другая сделка.
Пересборка на shadcn
Первая версия админки жила на Mantine. В 2026-м я пересобрал её на shadcn/ui — новый UI это немалая часть работы над админкой, но далеко не вся: за интерфейсом стоит масса логики, а не только вёрстка. React 19, Next.js 15, около ста двадцати базовых компонентов, TanStack Table под таблицы, Konva под фасады, recharts под аналитику, dnd-kit под перетаскивания, TipTap под текст. Собрано атомарно: сначала кастомизированные базовые компоненты, потом блоки, потом целые шаблоны страниц, переиспользуемые так, что ничего не существует дважды. Смысл — чтобы новый экран собирался из готового, а не рисовался с нуля.
Что получилось
Бэк-офис для локальных отделов продаж — это складской софт, а не CMS. Bulk-операции над десятками юнитов, один источник правды и дефолты, которые приезжают сами через наследование, решают, будут ли инструментом вообще пользоваться. А фасад, размеченный за час, дальше обновляет себя из CRM.