Шесть дней назад Villa Metrics была набором моделей: виллы, бронирования, платежи. Красивый CRUD, но бесполезный для реальной работы. Менеджеры вилл на Бали живут в чатах — WhatsApp, Telegram, Email. Вся коммуникация с гостями там. А в системе управления — тишина.
За два дня (25-26 января) я собрал полноценную CRM с чатами, AI-ассистентом и гостевой аналитикой. Около 50 коммитов.
Backend: три канала, одна модель
Начал с моделей. Guest хранит данные гостя: имя, контакты, язык, VIP-статус. CommunicationChannel — абстракция над каналом связи. Один гость может иметь WhatsApp, Telegram и Email одновременно. Каждый канал — отдельная запись с типом и credentials.
Message — центральная модель. Текст, канал, направление (входящее/исходящее), статус доставки, timestamps. Все сообщения из всех каналов попадают в одну таблицу. Это упрощает поиск и аналитику.
class Message(TimeStampedModel):
channel = models.ForeignKey(CommunicationChannel, on_delete=models.CASCADE)
direction = models.CharField(choices=[('in', 'Incoming'), ('out', 'Outgoing')])
content = models.TextField()
status = models.CharField(choices=STATUS_CHOICES, default='pending')
delivered_at = models.DateTimeField(null=True, blank=True)
Шаблоны сообщений (MessageTemplate) — с переменными вроде {{ guest_name }} и {{ check_in_date }}. Для каждого канала свой набор шаблонов: WhatsApp требует pre-approved templates, Telegram — свободный формат, Email — HTML-шаблон.
API endpoints получились стандартные: CRUD для гостей, каналов, сообщений. Плюс /api/conversations/ — агрегированный список диалогов с последним сообщением и счётчиком непрочитанных. Celery tasks для отправки: send_whatsapp_message, send_telegram_message, send_email. Webhooks для приёма входящих.
Frontend: три колонки и Zustand
Десктопный интерфейс — классический трёхколоночный layout для чатов. Левая колонка — ConversationList со списком диалогов, поиском и фильтрами. Центр — ChatWindow с историей сообщений и полем ввода. Правая — GuestInfoPanel с карточкой гостя, бронированиями и заметками.
Для стейта выбрал Zustand. После Redux Toolkit это глоток воздуха. Весь store — один файл:
interface ChatStore {
conversations: Conversation[]
activeConversation: string | null
messages: Record<string, Message[]>
setActiveConversation: (id: string) => void
addMessage: (conversationId: string, message: Message) => void
}
React Query для серверных данных, Zustand для UI-стейта. Чёткое разделение: React Query кеширует и синхронизирует данные с backend, Zustand управляет тем, какой чат открыт, виден ли GuestInfoPanel, состояние набора текста.
TypeScript-типы генерировал руками — отдельный файл types/crm.ts с интерфейсами для всех сущностей. Да, можно было автогенерировать из OpenAPI-схемы. Но на 50 коммитов за два дня — проще написать руками и не тратить время на настройку кодогена.
Мобильная версия — отдельные страницы вместо колонок. Список диалогов на /crm/chats, переход в чат на /crm/chats/[id], информация о госте по свайпу.
WebSocket: real-time без polling
Написал WebSocketManager — класс-обёртка над нативным WebSocket. Автоматический реконнект с exponential backoff: 1 секунда, 2, 4, 8, максимум 30. Heartbeat каждые 30 секунд, чтобы соединение не дропалось за nginx proxy.
На backend — Django Channels с Redis как channel layer. Каждый диалог — отдельная группа. Когда приходит webhook от WhatsApp — Celery task создаёт Message, отправляет в WebSocket-группу. Фронтенд получает мгновенно.
Была ошибка с потерей сообщений при реконнекте. WebSocket отвалился на 3 секунды — а в это время пришло сообщение. Добавил last_message_id при подключении: сервер досылает пропущенные.
AI-ассистент: Claude в каждом чате
Самая интересная часть. Подключил Claude API как провайдера AI-функций. Три задачи: классификация сообщений, анализ тональности, генерация ответов.
Классификация определяет тип входящего сообщения: вопрос о бронировании, жалоба, запрос информации, благодарность. Тональность — шкала от -1 до 1. Генерация ответов учитывает контекст: данные бронирования, историю переписки, язык гостя.
В UI — SuggestionPanel справа от поля ввода в ChatWindow. Показывает предложенный ответ с кнопками «Отправить», «Редактировать», «Отклонить». Менеджер видит, что предлагает AI, и решает сам.
Настройки AI вынес на отдельную страницу: выбор модели, температура, системный промпт для каждого типа задач. Промпт для генерации ответов включает контекст виллы: описание, правила, FAQ.
Гостевые сегменты и метрики
Добавил сегментацию гостей: VIP, returning, first-time, problematic. Сегмент считается автоматически по истории бронирований и средней тональности сообщений. Returning guest с тремя бронированиями и средним sentiment > 0.5 — автоматически VIP.
Метрики на дашборде: среднее время ответа, процент ответов через AI-подсказки, распределение по каналам, NPS на основе тональности. Плюс трекинг изменений бронирований — кто, когда, что поменял. История хранится в отдельной таблице BookingChangeLog.
Результат
50 коммитов за два дня. CRM с тремя каналами связи (WhatsApp, Telegram, Email), real-time чатом через WebSocket, AI-ассистентом на Claude API и гостевой аналитикой.
Честно — многое сделано на уровне MVP. WhatsApp webhook ещё не подключен к реальному Business API, Telegram bot тоже в тестовом режиме. Но архитектура готова: добавить credentials и переключить на production — дело нескольких часов.
Zustand + React Query оказались отличной комбинацией для real-time приложения. Никакого boilerplate, понятный data flow.