Разработка веб-приложения для общепита. Coffee Anytime, Hot, Шампур Клиент: Сеть кофеен Coffee Anytime
О клиенте
ООО «ВКУСНО 55» — крупнейший омский конгломерат в сфере общественного питания. Более пяти брендов, более 80 торговых точек по всему городу и области. Флагман группы — сеть кофеен “Coffee Anytime”.
Стек технологий — Laravel, React, InertiaJS, PostgreSQL
| Backend | Laravel (PHP), PostgreSQL, Queue Workers |
| Frontend | React, TypeScript, Inertia.js |
| UI | TailwindCSS, shadcn/ui (Radix UI) |
| Редактор | Quill.js + кастомизированный quill-table-better |
| Интеграция | r_keeper StoreHouse API |
Проблема
С ростом сети заказчик столкнулся с классической проблемой масштабирования ручных процессов. Каждая новая торговая точка — это не просто новый источник дохода, а ещё один поток заявок, который нужно обработать вручную.
Как выглядел процесс до нас
Сотрудник точки → Звонок/бумажка → Оператор → Ручной ввод в Excel → Перенос в StoreHouse → Накладная
На каждом этапе — потеря времени и риск ошибки.
Три критических барьера:
- Немасштабируемый ручной труд. Сотрудники точек передавали списки на бумаге или по телефону. Операторы вручную переносили данные в Excel, составляли сводные таблицы и формировали заявки в складской системе. При 80+ точках — это конвейер, который физически невозможно ускорить.
- Человеческий фактор. Ошибка в одной цифре при ручном переносе — и на точку приезжает не тот объём, не тот товар, или не приезжает вообще. Цена ошибки — сбой в работе целой точки на день.
- Потерянные дедлайны. Нет централизованной системы — нет контроля. Закрытые или перегруженные точки просто забывали отправлять заявки вовремя. Оператор узнавал об этом постфактум.
Задача
Исходя из выявленных проблем, перед нами поставили задачу разработать сервис, который позволит полностью автоматизировать процесс обработки заявок. Решение должно было учитывать все возможные сценарии и особенности оформления заказов, а также обеспечивать автоматическую выгрузку данных в складскую систему r_keeper.
Целью было минимизировать влияние человеческого фактора, исключить ошибки при ручном вводе данных и освободить операторов от монотонной работы по формированию документов и таблиц.
Архитектура решения
Мы выбрали монолитную SPA-архитектуру с чётким разделением ответственности:

Почему Inertia.js? Нужен SPA-опыт (мгновенные переходы, реактивность), но API для отдельного фронтенда — это лишняя сложность и дублирование валидации. Inertia.js решила обе проблемы: мы получили полноценное React-приложение с Laravel-контроллерами без API-прослойки.
Система работает с четырьмя уровнями доступа, каждый с точно выверенными полномочиями:
| Роль | Возможности | Ограничения |
|---|---|---|
| Администратор | Управление пользователями, создание временных доступов, полный контроль системы | — |
| Оператор | Создание шаблонов, контроль заявок, отправка накладных в StoreHouse, консолидация | Нет доступа к управлению пользователями |
| Сотрудник точки | Заполнение и отправка заявок по своей точке | Только свои заявки, запрет на изменение структуры шаблона |
| Менеджер | Просмотр заявок, архивов, консолидации | Только чтение, без возможности правки |
Каждая роль защищена на уровне middleware — отдельные классы проверяют права доступа ещё до того, как запрос дойдёт до контроллера.
Ключевые модули
Торговые сети и точки

Каждая торговая сеть содержит собственный набор торговых точек из под которых могут поступать запросы на поставку продукции.
Для реализации интерфейса приложения использовалась библиотека ui-компонентов от shadcn/ui, отлично интегрируемая в Laravel и React. На основе компонентов мы создали базовую структуру страницы, а также добавили всплывающие модальные окна для их управления.

Чтобы управлять торговой сетью создали полный набор инструментов, позволяющий вносить изменения в шаблоны заявок, просматривать список всех заявок за текущий день и общий архив заявок, а также просматривать консолидацию по всем торговым точкам в промежутке день/ночь.
На этой же странице вывели кнопку для ручной синхронизации со складом r_keeper, позволяющей получить новые торговые позиции, цеха, единицы измерения и т.д.
Отдельно был вынесен компонент торговой точки и привязан к общей таблице всех точек сети.


Для корректного создания накладных пришлось связать наш объект торговой точки с реальным предприятием внутри складской программы, чтобы обеспечить полную совместимость.
Интерфейс был реализован так, чтобы он включал в себя текущие заявки на день, которые сотрудник должен отправить до определенного времени, иначе заявка будет просрочена. Дополнительно создали компонент для просмотра истории всех сделанных сотрудником заявок.
Позже настроили маршрутизацию так, чтоб сотрудник точки при авторизации сразу же попадал в свой отдел и выполнял все действия из-под него.

Оператору мы добавили возможность проставлять нужные шаблоны, прямо из-под страницы торговой точки в виде модальных окон, напротив каждого типа заявки.
Каждый тип заявки имеет свою особенность — периодичность отправки, отсутствие или наличие нужных столбцов, поведение при создании накладных.
📝 Табличный редактор: самый сложный модуль
Это был, пожалуй, самый технически сложный блок проекта.
Заказчик хотел табличный интерфейс, максимально приближенный к Excel: цветные колонки, изменение размеров, группировка строк. Но при этом — с поиском товаров, валидацией и автоматическим созданием накладных.
Выбор пал на Quill.js с плагином quill-table-better. Но базовая версия плагина покрывала от силы 30% наших потребностей. Пришлось форкнуть и серьёзно доработать:
Что пришлось переписать: — Инициализация таблицы сразу с нужными колонками — Регистрация кастомных Blot-атрибутов для серверной обработки дельта-формата — Создание строк по клику вместо стандартного поведения — Защищённый режим: запрет на изменение структуры + ввод только в определённые колонки — Разделение функционала по ролям — сотрудник не может то, что может оператор — Обработка дробных значений.
Поиск товара — отдельный компонент, который появляется при вводе текста или кода продукции внутри ячейки. Поиск работает как по названию (текстовый), так и по числовому коду (RID товара в StoreHouse).
Формат данных: Quill оперирует Delta-объектами (JSON с операциями вставки и форматирования). На сервере мы написали собственный парсер parseDelta(), который извлекает из этого формата структурированные данные.

Шаблоны заявок
Шаблоны — это “скелеты” заявок, которые оператор создаёт один раз, а потом привязывает к торговым точкам. Каждый шаблон содержит:
- Набор товарных позиций (товар, единица измерения)
- Дни недели для использования
- Время дневной корректировки и вечерней
- Офсет отгрузки — на сколько дней вперёд отгрузка, с возможностью задать разный офсет на каждый день недели
Для экономии времени реализовали импорт шаблонов — оператор может создать новый шаблон на базе уже существующего.
Лимиты товаров вынесены в отдельный модуль (NetworkProducts): для каждой позиции внутри сети можно задать максимальное количество, которое будет использоваться при валидации на фронте.

Заказы: дневная и вечерняя корректировка

Один из ключевых бизнес-процессов — двухфазная корректировка заявок:
- Дневная корректировка — сотрудник заполняет основные объёмы до дедлайна
- Вечерняя корректировка — после 18:00 можно скорректировать значения, если ситуация на точке изменилась
Система программно определяет текущую фазу и блокирует/разблокирует соответствующие колонки. После отправки — изменять содержимое сможет только оператор.

Версионирование: Каждое сохранение заказа создаёт новую версию (OrderVersion). Оператор может просмотреть любую из предыдущих версий — полная история изменений сохраняется.
Запланированные заявки: Для точек, которые не работают в определённые дни (например, воскресенье), реализовали функцию планирования. Сотрудник создаёт заявку заранее с флагом auto_send. Команда, запускаемая по крону, автоматически активирует такие заявки в нужный день и отправляет уведомление оператору.
Позже по просьбе заказчика для сущности товара мы добавили максимальное значение. Если пользователь вводит в ячейку “Количество” большое значение, ему выводится предупреждение.

Список заявок и архив

Создали более масштабную версию сегодняшних заявок для торговой точки в список заявок для торговой сети.
Для удобства пользователя были выведены маркеры, показывающие прошла ли корректировка или нет, и просмотрена ли заявка оператором.
Когда заявка была создана — маркер окрашивается в желтый свет, когда оператор просмотрел заявку — маркер становится зеленым.

Отдельной страницей проработали архив заявок, здесь отображаются заявки, отправленные в Store House, пользователь может просмотреть заявку и распечатать.

Чтобы облегчить поиск нужных заявок, внедрили компоненты для фильтрации данных по типу заказа и по времени подачи.
Консолидация: сводные таблицы в реальном времени

Консолидация — это “панель управления” для оператора, где он видит все заявки всех точек одной сети в виде единой таблицы, сгруппированной по цехам и категориям.
Как это работает:
- Из каждой заявки извлекаются данные через парсер Quill Delta
- Для товаров определяется цех (workshop) и категории
- Строится матрица консолидации : по вертикали — товары, по горизонтали — точки, в ячейках — количество
- Заявки разделяются по дате отгрузки (оказалось, что у точек одной сети даты могут различаться из-за разных офсетов)
- Вечерние заявки визуально выделяются синим цветом, дневные — белым

Редактирование из сводной таблицы: Оператор может изменить количество товара прямо в консолидации — изменения применяются к конкретной заявке с созданием новой версии.
Мгновенная печать: По клику на имя торговой точки открывается окно печати заявки — без лишних переходов. И отдельную кнопку для пакетной печати по всем точкам сразу.
Отдельно создали консолидацию по напиткам — для отдельных категорий товаров с особой логикой группировки.
Интеграция с r_keeper StoreHouse
Интеграция со складской программой — ядро системы. Каждая заявка в итоге должна превратиться в реальную накладную.
Написали отдельный класс — наш адаптер для API StoreHouse. API складской программы спроектирован по своей уникальной схеме (endpoint’ы /api/sh5exec и /api/sh5enum, числовые идентификаторы полей вроде «210\\1», «206\\1»), поэтому мы инкапсулировали всю сложность в fluent-интерфейс:
$this->storeHouseUtility
->setProcName(‘InsGDoc11’)
->addInputData(«111», […], […], «Insert»)
->addInputData(«112», […], […])
->post();
Два типа документов: — Накладная — стандартный перевод товара от поставщика к точке — Комплектация — для позиций с комплектами (полуфабрикаты, сборные блюда)
Очередь отправки: Когда оператор нажимает “Отправить заявки”, каждая накладная уходит в очередь Laravel. Очередь обрабатывается одним воркером — это гарантирует последовательность и предотвращает конфликты, а также снижает нагрузку на базу SH. Благодаря уникальному атрибуту одна и та же пара сеть+тип не может оказаться в очереди дважды.
Оператор может продолжить работу, не дожидаясь завершения — уведомление об отправке придёт в фоне.
Синхронизация данных

Чтобы система всегда знала актуальный ассортимент, мы реализовали полную синхронизацию с r_keeper StoreHouse:
Команда (запускается cron’ом ежедневно в 00:00) выполняет: 1. Загрузку цехов 2. Загрузку иерархии групп товаров и категорий 3. Для каждой группы — загрузку товаров с атрибутами (единица измерения, цех, комплект, признак напитка) 4. Для каждого товара — загрузку единиц измерения 5. Удаление устаревших товаров и нотаций, которых больше нет в StoreHouse
Защита от спама: Ручная синхронизация через интерфейс блокируется на время выполнения — повторный запуск невозможен, пока предыдущий импорт не завершится.
Уведомления в реальном времени
Уведомления работают через кеш-слой (Laravel Cache), а не через прямые запросы к БД.
Механика: — Когда сотрудник отправляет заявку → метод класса добавляет ID заявки в кеш — Когда оператор открывает заявку, то удаляет её из кеша — Все операторы подписаны на единый канал: если один просмотрел — уведомление пропадает у всех
Каждый оператор настраивает свои подписки: за какие сети и типы заказов он хочет получать уведомления. Уведомления отображаются на главной странице, данные обновляются каждые 10 секунд, а кеш синхронизируется с базой раз в 30 минут.
Пакетная печать накладных
Штатные инструменты r_keeper StoreHouse не позволяли печатать накладные пакетно по нужному шаблону. Мы решили эту задачу на нашей стороне:
- Веб-форма с фильтрами
- Запрос к StoreHouse API через процедуру получения списка накладных
- Генерация единого формата документа для печати
- Вывод через print-шаблоны
Безопасность и удалённый доступ
Временный доступ без пароля. Для случаев, когда сотрудник точки не за рабочим местом (сломался компьютер, нет интернета на точке), администратор может сгенерировать одноразовую ссылку:
- Генерируется случайный токен
- Ссылка отправляется сотруднику через мессенджер
- При переходе — одноразовая авторизация без логина и пароля
- Сессия действует 24 часа, после чего автоматически завершается
- Токен уничтожается после первого использования
Администратор видит список активных токенов и может отозвать любой в любой момент.
Документация
Заключительным этапом работы над проектом стало создание документации для разработчиков:
- React-компоненты покрыты JSDoc с указанием пропсов и типов
- PHP-код описан DocBlock’ами
- На основе DocBlock’ов через phpDocumentor собирается HTML-документация
- Доступна из приложения через маршрут /docs
- Общая информация по работе с проектом описана через rst-шаблоны и интегрирована в структуру документации


Результаты
- Автоматизировали 80% ручной работы сотрудников
- Сократили риск человеческого фактора до 1%
- Упростили контроль над 83 торговыми точками
- Обеспечили бесперебойную обработку 250+ заявок в дневные и вечерние смены
- Внедрили прозрачную систему ответственности, исключив путаницу в процессах