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

Встал выбор: нативное приложение или PWA. Выбрал PWA — и за день добавил мобильную адаптацию, анимации, второй channel manager и портал инвесторов. 120 коммитов.

Почему PWA, а не нативное

Три причины. Первая — скорость разработки. Нативное приложение под iOS и Android удвоило бы сроки. React Native или Flutter — всё равно отдельная кодовая база, отдельные баги, отдельный deploy.

Вторая — PWA на iOS стал достаточно хорош. Safari поддерживает service workers, push-нотификации (с iOS 16.4), Add to Home Screen. Не идеально, но для бизнес-приложения хватает.

Третья — мои пользователи не из App Store качают рабочие инструменты. Отправил ссылку в WhatsApp, человек открыл, нажал "Добавить на экран" — готово.

// manifest.json
{
  "name": "Villa Metrics",
  "short_name": "VillaMetrics",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#F5E6D3",
  "theme_color": "#2B6CB0",
  "orientation": "portrait"
}

Service worker кеширует статику и API-ответы. Офлайн-режим пока базовый — показывает последние загруженные данные. Полноценную офлайн-синхронизацию оставил на потом.

Мобильная навигация

Десктопная версия использует боковое меню. На мобильном это не работает — экран слишком узкий. Переделал в два компонента: bottom navigation и slide menu.

Bottom navigation — пять кнопок внизу экрана: Dashboard, Бронирования, Виллы, Финансы, Ещё. Всё по стандартным паттернам мобильных приложений. Активная вкладка подсвечивается терракотовым.

Slide menu выезжает слева по свайпу или по кнопке. Здесь — профиль пользователя, настройки, аналитика, нотификации, выход. Framer Motion анимирует появление: translateX(-100%) в translateX(0) за 300ms с easing.

<motion.div
  initial={{ x: '-100%' }}
  animate={{ x: isOpen ? 0 : '-100%' }}
  transition={{ type: 'spring', damping: 25, stiffness: 200 }}
  className="slide-menu"
>
  {/* menu content */}
</motion.div>

Safari на iOS добавил проблем. 100vh не учитывает адресную строку — элементы уползают за экран. Переключился на dvh (dynamic viewport height). Bottom navigation теперь фиксированный, с padding-bottom: env(safe-area-inset-bottom) для iPhone с чёлкой.

Анимации: Framer Motion

Добавил Framer Motion для всего интерфейса. Не ради красоты — ради ощущения нативного приложения. Переходы между страницами, появление карточек, микроанимации кнопок.

Карточки бронирований появляются каскадом: первая через 0ms, вторая через 50ms, третья через 100ms. Выглядит живо, но не раздражает.

<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ delay: index * 0.05 }}
>
  <BookingCard booking={booking} />
</motion.div>

Потратил час на настройку AnimatePresence для переходов между страницами. Первая попытка — элементы мигали при переключении. Проблема была в отсутствии key на motion.div внутри AnimatePresence. Классическая ошибка.

Zodomus: второй channel manager

Кроме Hostaway, добавил интеграцию с Zodomus — ещё один channel manager, популярный в Юго-Восточной Азии. API у Zodomus проще, чем у Hostaway, но с особенностями.

Главная — пагинация. Zodomus возвращает максимум 50 записей за запрос. Для вилл с сотнями бронирований в год нужно делать много запросов. Написал рекурсивный fetcher с rate limiting: максимум 10 запросов в секунду, автоматический retry при 429-ой ошибке.

Обе интеграции (Hostaway и Zodomus) теперь работают через Celery. Периодическая синхронизация каждые 15 минут: celery-beat ставит задачу, celery-worker её выполняет. Redis как брокер сообщений.

Портал инвесторов

У вилл на Бали часто несколько владельцев. Каждый хочет видеть свою долю: доходы, расходы, дивиденды. Отдельный раздел — портал инвесторов.

Модель Investor связана с Property через промежуточную таблицу с процентом владения. Дивиденды рассчитываются автоматически: доход виллы минус расходы, умножить на долю инвестора. Выплаты фиксируются отдельной моделью Payout.

На фронтенде — дашборд инвестора. Графики доходности по месяцам, история выплат, текущий баланс. Инвестор видит только свои виллы и свои цифры. Role-based access: менеджер видит всё, инвестор — только своё.

Управление виллами

Расширил раздел вилл: загрузка фотографий, настройка ценообразования по сезонам (высокий, низкий, пиковый), описания для разных площадок. Иконки источников бронирований — Airbnb оранжевый, Booking.com синий, прямые бронирования зелёные.

Сезонное ценообразование — отдельная модель Season с датами начала и конца, множителем цены. Перекрытие сезонов валидируется на уровне модели: clean() проверяет, что даты не пересекаются для одной виллы.

Результат

21 января 2026 года. 120 коммитов — вдвое больше, чем в первый день. Villa Metrics теперь работает на телефоне как полноценное приложение. PWA устанавливается на домашний экран, анимации делают интерфейс отзывчивым, два channel manager\'а синхронизируют бронирования.

Что получилось за два дня: 180 коммитов, Django 5 backend, React 18 frontend с Framer Motion, Docker Compose на 4 контейнера, PostgreSQL 16, Celery + Redis, интеграции с Hostaway и Zodomus, портал инвесторов с расчётом дивидендов, полная мобильная адаптация.

Ошибка, которую я допустил: начал анимации до того, как закончил мобильную навигацию. Пришлось переделывать AnimatePresence дважды — сначала для десктопной навигации, потом адаптировать для bottom navigation. Правильный порядок: сначала layout, потом анимации.