ESWF Mobile — Driver App + Sales App¶
ESWF має два мобільні застосунки на одному стеку (Expo SDK 51 + React Native + WatermelonDB):
| App | Локація | Призначення | Backend API |
|---|---|---|---|
| Driver App | c:\eswf\mobile\ |
АРМ водія: рейси, путні листи, одометр, паливо | mobile_api/ (/api/v1/mobile/) |
| Sales App | c:\eswf\mobile-sales\ |
АРМ торгового представника: візити, рахунки, GPS, фото, підпис | sales_mobile_api/ (/api/v1/sales-mobile/) |
Цей документ — детальний посібник по Driver App. Для Sales App див. розділ §13. Sales App — короткий огляд у кінці. Повна довідка по Sales App — у CLAUDE.md (секція «Mobile Sales App»).
Driver App — повна документація¶
Локація: c:\eswf\mobile\
Платформа: iOS / Android (React Native + Expo)
Дата імплементації: 2026-02-18
Статус: ✅ Реалізовано (потребує expo prebuild для запуску)
Зміст¶
- Навіщо цей застосунок
- Технологічний стек — вибір і обґрунтування
- Архітектура офлайн-sync
- Структура файлів — що і навіщо
- Backend: Mobile API
- Встановлення для розробки
- Встановлення на мобільний телефон
- Як користуватися застосунком
- Sync: як це працює детально
- Конфігурація — де змінювати URL backend
- Production збірка (EAS Build)
- Що ще можна додати
1. Навіщо цей застосунок¶
Водій вантажівки не завжди має доступ до інтернету. Але йому потрібно:
- бачити свої маршрути та замовлення навіть без зв'язку
- вводити показники одометра в кінці рейсу
- фіксувати витрати пального по дорозі
- дивитися деталі вантажу і дату доставки
Веб-застосунок DOP (erp.eswf.dev) вирішує це для диспетчерів, але не підходить водію — він потребує нативного мобільного застосунку з офлайн-першою логікою.
Сценарій використання:
Вранці (є Wi-Fi/4G):
→ Застосунок синхронізується з сервером
→ Завантажує путні листи і замовлення на сьогодні
Вдень (немає інтернету):
→ Водій відкриває список своїх рейсів — все є в кеші
→ Вводить показники одометра в кінці маршруту
→ Дані зберігаються локально на телефоні
Ввечері (знову Wi-Fi):
→ Натискає "Sync" — дані відправляються на сервер
→ Диспетчер бачить актуальні показники в DOP
2. Технологічний стек¶
Чому React Native + Expo?¶
| Альтернатива | Чому не обрали |
|---|---|
| Flutter | Інша мова (Dart), немає переиспользования JS коду з web-частини |
| Native iOS/Android | Дві окремі кодові бази — вдвічі більше роботи |
| Capacitor/Ionic | Web-view з обгорткою — гірша продуктивність, не нативний feel |
| React Native + Expo | ✅ Один код для iOS+Android, React (знайомий), нативна продуктивність |
Expo — toolchain поверх React Native, що дає:
- expo prebuild — генерація native проектів без Xcode/Android Studio для основних налаштувань
- EAS Build — хмарна збірка .apk/.ipa без локального SDK
- Expo Go — швидке тестування (але не підходить для WatermelonDB)
- expo-secure-store — захищене зберігання токенів
- Автоматичне підписання та OTA-оновлення
Чому WatermelonDB?¶
| Альтернатива | Чому не обрали |
|---|---|
| AsyncStorage | Не реляційна, немає реактивних запитів |
| expo-sqlite напряму | Потрібно вручну писати SQL, немає ORM, немає sync-протоколу |
| Realm | Комерційна ліцензія, складна схема |
| WatermelonDB | ✅ Безкоштовна, SQLite під капотом, реактивна (RxJS), є sync-протокол |
WatermelonDB дає: - Lazy loading — не завантажує всі дані одразу - Reactive queries — компоненти автоматично перерендеряться при змінах в БД - Sync protocol — стандартизований pull/push формат між клієнтом і сервером - JSI — прямий зв'язок з native SQLite через JavaScript Interface (без bridge) — дуже швидко - Optimistic updates — зміни відразу видно в UI, навіть до sync
Чому expo-secure-store для токенів?¶
AsyncStorage зберігає дані у відкритому вигляді (можна прочитати через adb). SecureStore використовує Keychain (iOS) / Keystore (Android) — зашифровані сховища ОС. JWT-токени — це credentials, їх треба захищати.
3. Архітектура офлайн-sync¶
┌─────────────────────────────────────────────────────────────┐
│ Mobile App (React Native) │
│ │
│ ┌──────────────────────────┐ ┌─────────────────────────┐ │
│ │ React UI │ │ WatermelonDB │ │
│ │ (screens, components) │◄──│ (SQLite via JSI) │ │
│ │ withObservables() │ │ │ │
│ │ → auto-rerender │ │ Tables: │ │
│ └──────────────────────────┘ │ - waybills │ │
│ │ - orders │ │
│ ┌──────────────────────────┐ │ - vehicles │ │
│ │ useSync hook │ │ - routes │ │
│ │ - isSyncing │ │ │ │
│ │ - lastSyncAt │ └──────────┬──────────────┘ │
│ │ - sync() │ │ │
│ └──────────┬───────────────┘ │ synchronize() │
└─────────────┼────────────────────────────────────────────────┘
│ (тільки при наявності інтернету)
▼
┌─────────────────────────────────────────────────────────────┐
│ Django Backend │
│ │
│ GET /api/v1/mobile/sync/pull?last_pulled_at=<ms> │
│ ← { changes: { waybills: {...}, orders: {...} }, │
│ timestamp: 1703000000000 } │
│ │
│ POST /api/v1/mobile/sync/push │
│ → { changes: { waybills: { updated: [...] } } } │
└─────────────────────────────────────────────────────────────┘
Ключовий принцип: UI ніколи не звертається до API напряму. Він читає з локальної WatermelonDB. Sync — окремий процес у фоні.
Результат: - Відкрив список путніх листів → миттєво (з SQLite) - Немає інтернету → все одно працює - З'явився інтернет → натиснув sync → данні оновились
4. Структура файлів¶
mobile/App.tsx — Точка входу¶
<DatabaseProvider database={database}> // WatermelonDB контекст
<AuthProvider> // JWT стан
<StatusBar />
<AppNavigator /> // Навігація
</AuthProvider>
</DatabaseProvider>
Три провайдери вкладені в правильному порядку. DatabaseProvider дає доступ до WatermelonDB для всіх дочірніх компонентів через useDatabase() хук.
src/database/schema.ts — Схема бази даних¶
Визначає 4 таблиці WatermelonDB. Версія 1 — при зміні схеми треба збільшити версію і додати міграцію.
appSchema({
version: 1,
tables: [
tableSchema({ name: 'waybills', columns: [...] }),
tableSchema({ name: 'orders', columns: [...] }),
tableSchema({ name: 'vehicles', columns: [...] }), // reference
tableSchema({ name: 'routes', columns: [...] }), // reference
]
})
Навіщо зберігати vehicles і routes локально? Щоб у путньому листі показати назву ТЗ і маршрут без додаткових запитів до API.
Важливо про типи колонок:
- type: 'string' — текст (WatermelonDB зберігає як TEXT в SQLite)
- type: 'number' — числа (INTEGER або REAL в SQLite)
- isOptional: true — поле може бути порожнім
src/database/index.ts — Ініціалізація БД¶
const adapter = new SQLiteAdapter({
schema,
migrations,
dbName: 'eswf_driver', // ім'я файлу SQLite на пристрої
jsi: true, // JSI = JavaScript Interface (швидший bridge)
onSetUpError: (error) => { console.error(error); }
});
const database = new Database({
adapter,
modelClasses: [Waybill, Order, Vehicle, Route],
});
jsi: true — це оптимізація. JSI дозволяє JavaScript коду напряму викликати native функції без серіалізації через JSON. Результат — ~10x швидше ніж через стандартний React Native bridge.
src/database/models/ — Моделі WatermelonDB¶
Кожна модель — TypeScript клас з декораторами:
export default class Waybill extends Model {
static table = 'waybills'; // ← ім'я таблиці зі схеми
@text('number') number!: string; // @text — строкове поле
@field('odometer_start') odometerStart!: number; // @field — числове
}
Декоратори WatermelonDB:
- @text('column_name') — для рядкових полів
- @field('column_name') — для числових і булевих полів
- @date('column_name') — для дат (зберігає як timestamp)
- @relation('table', 'fk_column') — для зв'язків між таблицями
src/database/migrations.ts — Міграції схеми¶
Зараз порожній (початкова версія 1). Коли потрібно додати нове поле:
// Приклад майбутньої міграції:
schemaMigrations({
migrations: [
{
toVersion: 2,
steps: [
addColumns({
table: 'waybills',
columns: [{ name: 'notes', type: 'string', isOptional: true }]
})
]
}
]
})
Важливо: При зміні schema.ts (версія 1 → 2) завжди треба додавати міграцію. Інакше WatermelonDB скине всі локальні дані при апгрейді.
src/api/client.ts — HTTP клієнт¶
Axios + автоматичний refresh JWT токену:
Запит → 401 Unauthorized
↓
Черга запитів (queue)
↓
POST /auth/token/refresh/
↓
Успіх? → retry всіх запитів з нового токену
Провал? → clearTokens() → logout
Навіщо черга? Якщо одночасно прийшло 3 запити з 401, не треба робити 3 refresh — достатньо одного. Всі інші чекають у черзі і потім повторюються з новим токеном.
URL для різних середовищ:
const BASE_URL = __DEV__
? 'http://10.0.2.2:8000/api/v1' // Android emulator
: 'https://api.eswf.dev/api/v1'; // Production
10.0.2.2 — це спеціальний alias для localhost хост-машини в Android emulator. Для iOS simulator можна використовувати localhost.
src/api/sync.ts — WatermelonDB Sync¶
Головна функція синхронізації:
await synchronize({
database,
pullChanges: async ({ lastPulledAt }) => {
// lastPulledAt = timestamp останньої успішної sync (null якщо перший раз)
const response = await fetch(`/mobile/sync/pull?last_pulled_at=${lastPulledAt ?? 0}`);
const { changes, timestamp } = await response.json();
return { changes, timestamp };
// changes = { waybills: { created: [...], updated: [...], deleted: [...] } }
// timestamp = поточний час сервера в мілісекундах (для наступного pull)
},
pushChanges: async ({ changes, lastPulledAt }) => {
// changes = локальні зміни (те що водій ввів офлайн)
await fetch('/mobile/sync/push', {
method: 'POST',
body: JSON.stringify({ changes }),
});
},
});
WatermelonDB сам керує lastPulledAt — зберігає після кожної успішної sync і передає наступного разу.
src/api/auth.ts — Авторизація¶
Три функції:
- login(username, password) → POST /auth/login/ → { access, refresh }
- getDriverProfile() → GET /mobile/me/ → user + driver info
- logout(refreshToken) → POST /auth/logout/ (ігнорує помилки)
src/auth/tokenStorage.ts — Зберігання токенів¶
// Зберегти
await SecureStore.setItemAsync('eswf_driver_tokens', JSON.stringify({ access, refresh }));
// Прочитати
const raw = await SecureStore.getItemAsync('eswf_driver_tokens');
return JSON.parse(raw);
// Видалити (logout)
await SecureStore.deleteItemAsync('eswf_driver_tokens');
SecureStore — це Keychain на iOS і EncryptedSharedPreferences / Keystore на Android. Дані зашифровані і прив'язані до застосунку.
src/auth/AuthContext.tsx — Стан авторизації¶
При старті застосунку:
1. getTokens() → є токени?
2. Так → GET /mobile/me/ → встановити profile
3. Ні → показати LoginScreen
4. Помилка 401 → clearTokens() → LoginScreen
Надає через контекст: isLoading, isAuthenticated, profile, login(), logout(), refreshProfile().
src/navigation/AppNavigator.tsx — Навігація¶
AppNavigator
├── (isLoading) → Loading spinner
├── (isAuthenticated = false) → LoginScreen
└── (isAuthenticated = true) → MainTabs
├── DashboardTab → DashboardScreen
├── WaybillsTab → WaybillsStack
│ ├── WaybillsList → WaybillsScreen
│ └── WaybillDetail → WaybillDetailScreen
├── OrdersTab → OrdersStack
│ ├── OrdersList → OrdersScreen
│ └── OrderDetail → OrderDetailScreen
└── ProfileTab → ProfileScreen
При logout isAuthenticated стає false → навігація автоматично перемикається на LoginScreen.
src/screens/DashboardScreen.tsx — Головний екран¶
Використовує withObservables для реактивних запитів до WatermelonDB:
const enhance = withObservables([], () => {
const waybills$ = database.get('waybills')
.query(Q.where('status', Q.oneOf(['draft', 'approved'])))
.observeWithColumns(['status']); // ← реактивно, оновлюється при зміні БД
const pendingOrders$ = database.get('orders')
.query(Q.where('status', Q.oneOf(['draft', 'approved'])))
.observeCount(); // ← тільки кількість
return { activeWaybill: waybills$.pipe(map(list => list[0])), pendingOrdersCount: pendingOrders$ };
});
Навіщо withObservables? Без нього треба вручну підписуватись/відписуватись від RxJS Observable і оновлювати стан. withObservables — HOC, що робить це автоматично.
Pull-to-refresh: RefreshControl з onRefresh={sync} — потягнути вниз = синхронізація.
src/screens/WaybillDetailScreen.tsx — Деталь путнього листа¶
Дозволяє водію вводити дані в кінці рейсу:
- Одометр (кінець) — відстань рахується автоматично: odometerEnd - odometerStart
- Паливо (кінець) — споживання рахується: fuelStart + fuelIssued - fuelEnd
Збереження — в локальну WatermelonDB (офлайн):
await database.write(async () => {
await waybill.update((record) => {
record.odometerEnd = parseInt(odometerEnd, 10);
record.fuelEnd = parseFloat(fuelEnd);
record.fuelConsumed = parseFloat(fuelConsumed);
record.distance = record.odometerEnd - record.odometerStart;
});
});
WatermelonDB позначає запис як _status: 'updated' і при наступній sync надсилає на сервер.
src/hooks/useSync.ts — Хук синхронізації¶
Захищає від паралельних sync-запитів через syncInProgress ref. Перевіряє мережу перед sync — якщо isConnected === false, одразу повертає помилку.
src/hooks/useNetworkStatus.ts — Моніторинг мережі¶
Підписується на NetInfo.addEventListener і повертає { isConnected, isInternetReachable }. Використовується для:
- Показу OfflineBanner ("Offline — showing cached data")
- Блокування sync при відсутності мережі
src/components/OfflineBanner.tsx — Банер офлайн¶
Показується тільки при isConnected === false. Жовта смуга вгорі екрану — нагадування, що дані можуть бути застарілими.
src/components/StatusBadge.tsx — Бейдж статусу¶
| Статус | Колір |
|---|---|
| draft | Темно-сірий |
| approved | Синій |
| posted | Зелений |
| cancelled | Червоний |
Відповідає кольоровій схемі DOP веб-застосунку.
src/theme.ts — Дизайн-система¶
Темна тема, узгоджена з DOP (Mantine dark). Всі кольори, відступи і розміри шрифтів — константи. Не використовувати StyleSheet з хардкодованими числами — завжди брати з COLORS, SPACING, FONT.
src/i18n/ — Інтернаціоналізація¶
Підтримує en і ua (як і DOP). За замовчуванням — ua. Перемикання в ProfileScreen → Lang button. setLanguage('en') / setLanguage('ua') — зміна застосовується миттєво для всього UI.
5. Backend: Mobile API¶
backend/mobile_api/views.py — Sync Views¶
SyncPullView — GET /api/v1/mobile/sync/pull¶
# Параметр last_pulled_at — мілісекунди з Unix epoch
since = datetime.fromtimestamp(int(last_pulled_at) / 1000, tz=timezone.utc)
# Знаходить Driver пов'язаний з поточним User
driver = Driver.objects.filter(user=request.user, tenant=tenant).first()
# Повертає тільки дані цього водія
waybill_qs = Waybill.objects.filter(tenant=tenant, driver=driver)
# Розбиває на created/updated/deleted
changes = _build_changes(waybill_qs, serialize_waybill, since)
Денормалізація: замість FK-посилань, сервер повертає вже готові рядки (vehicle_name, route_name) — WatermelonDB не підтримує JOIN, тому плоска структура.
Безпека: водій бачить тільки свої дані (driver=driver). Якщо driver = None (наприклад, адмін) — бачить всі дані тенанту.
SyncPushView — POST /api/v1/mobile/sync/push¶
Водій може оновлювати тільки обмежений набір полів:
updatable = ['odometer_end', 'fuel_end', 'fuel_consumed', 'distance',
'date_departure', 'date_arrival', 'description']
Статус дозволено змінювати тільки в межах: draft → approved. Переведення в posted або cancelled — тільки з DOP (диспетчер).
DriverProfileView — GET /api/v1/mobile/me/¶
Повертає User + пов'язаний Driver запис. Якщо водій не прив'язаний до User — driver: null (застосунок все одно працює, але без інформації про ліцензію).
Прив'язка Driver до User¶
В Django admin або через DOP потрібно прив'язати Driver → User:
Або через Django shell:
from fleet.models import Driver
from core.models import User
user = User.objects.get(username='driver1')
driver = Driver.objects.get(name='Іваненко Іван')
driver.user = user
driver.save()
6. Встановлення для розробки¶
Що потрібно¶
| Інструмент | Версія | Навіщо |
|---|---|---|
| Node.js | 18+ | npm, expo |
| Git | будь-яка | клонування |
| Android Studio | Flamingo+ | Android Emulator |
| Xcode | 14+ | iOS Simulator (тільки macOS) |
| Java JDK | 17 | Android SDK build tools |
| EAS CLI | latest | Cloud builds |
Крок 1 — Встановити Expo CLI¶
Крок 2 — Встановити залежності¶
Основні пакети, які встановляться:
- @nozbe/watermelondb — офлайн БД
- @nozbe/with-observables — реактивні компоненти
- @react-navigation/* — навігація
- expo-secure-store — зберігання токенів
- @react-native-community/netinfo — стан мережі
- i18next + react-i18next — інтернаціоналізація
Крок 3 — Prebuild (генерація native проектів)¶
Ця команда:
- Генерує android/ і ios/ папки з native кодом
- Налаштовує WatermelonDB native модулі (через @nozbe/watermelondb/expo-plugin)
- Налаштовує expo-secure-store native binding
Важливо: після prebuild папки android/ і ios/ не слід редагувати вручну — вони генеруються Expo. При зміні app.json плагінів — повторити prebuild.
Крок 4A — Запуск на Android Emulator¶
або через готовий APK:
Крок 4B — Запуск на iOS Simulator (тільки macOS)¶
Backend URL для емулятора¶
У файлах src/api/client.ts і src/api/sync.ts:
// Android emulator
const BASE_URL = __DEV__ ? 'http://10.0.2.2:8000/api/v1' : '...';
// ^^^^^^^^
// 10.0.2.2 = localhost хост-машини для Android emulator
Для iOS Simulator замінити на:
Для реального телефону (якщо бекенд на локальній машині):
const BASE_URL = __DEV__ ? 'http://192.168.1.100:8000/api/v1' : '...';
// ^^^^^^^^^^^^
// IP вашої машини в локальній мережі (ifconfig / ipconfig)
7. Встановлення на мобільний телефон¶
Є 4 способи, від найпростішого до production:
Спосіб 1 — Expo Go (ТІЛЬКИ для тестування UI, БЕЗ WatermelonDB)¶
⚠ Обмеження: WatermelonDB потребує native модулів, які недоступні в Expo Go. Застосунок запуститься, але при спробі відкрити БД впаде.
Для тестування UI без sync:
Відскануйте QR-код в Expo Go (iOS App Store / Google Play).Використовуйте тільки для перевірки layout і навігації, не для функціоналу з БД.
Спосіб 2 — Development Build (рекомендовано для розробки)¶
Development Build — це кастомна версія Expo Go з вашими native модулями.
Крок 1: Налаштувати Expo account (безкоштовно)
Крок 2: Ініціалізувати EAS проект
Крок 3: Зібрати dev build для Android
Або локально (потрібен Android Studio + Java):
Крок 4: Встановити APK на телефон
Після cloud build EAS надасть посилання на .apk. Відкрийте на телефоні — встановиться як звичайний APK.
Крок 5: Запустити Metro bundler
Development build підключиться до Metro bundler по IP.
Спосіб 3 — APK для внутрішнього тестування (preview)¶
Повноцінна збірка без Google Play. Підходить для тестування з реальними данними.
Якщо eas.json немає, створіть:
{
"cli": { "version": ">= 12.0.0" },
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal",
"android": { "buildType": "apk" }
},
"production": {
"android": { "buildType": "app-bundle" }
}
}
}
Збірка preview APK:
EAS надасть QR-код і посилання для скачування APK. Надішліть водіям — вони встановлять самостійно.
На телефоні потрібно увімкнути:
Налаштування → Безпека → Невідомі джерела → Дозволити
# або:
Налаштування → Програми → Особливий доступ → Встановлення невідомих застосунків
Спосіб 4 — Локальна збірка без EAS (без хмари)¶
Якщо не хочете EAS аккаунт:
Android (потрібен Android Studio):
cd c:\eswf\mobile
npx expo prebuild --clean
cd android
./gradlew assembleRelease
# APK буде тут: android/app/build/outputs/apk/release/app-release.apk
Перенесіть APK на телефон (USB або будь-яким способом) → встановіть.
Підключення реального телефону через USB:
# Увімкніть USB debugging в телефоні:
# Налаштування → Про телефон → натисніть "Номер збірки" 7 разів
# → Для розробників → USB-відлагодження → Увімкнути
# Перевірте що телефон видно:
adb devices
# Запустіть на телефоні:
npx expo run:android --device
Спосіб 5 — Google Play / App Store (production)¶
# Android (App Bundle для Google Play)
eas build --platform android --profile production
# iOS (IPA для App Store, потрібен Apple Developer аккаунт $99/рік)
eas build --platform ios --profile production
# Відправка в магазини
eas submit --platform android
eas submit --platform ios
8. Як користуватися¶
Перший запуск¶
- Встановити застосунок (будь-яким способом вище)
- Відкрити — показується LoginScreen
- Ввести
usernameіpasswordвід ESWF DOP акаунту - Натиснути "Увійти" / "Sign In"
Вимоги до аккаунту: - Аккаунт повинен існувати в DOP (Django User) - До аккаунту повинен бути прив'язаний Driver (Fleet → Drivers) - Якщо Driver не прив'язаний — вхід спрацює, але застосунок не знайде путні листи
Dashboard (Головний екран)¶
- Вітання + статус синхронізації (час останньої sync або помилка)
- Активний путній лист — найсвіжіший путній лист зі статусом
draftабоapproved - Кількість активних замовлень — лічильник
- Pull-to-refresh (потягнути вниз) — запускає синхронізацію
Путні листи (Waybills)¶
- Список всіх путніх листів, відсортованих за датою
- Бейдж статусу (Draft / Approved / Posted / Cancelled)
- Натиснути на путній лист → деталь
В деталях путнього листа (редагування):
| Поле | Дії |
|---|---|
| Одометр (початок) | Тільки читання |
| Одометр (кінець) | ✏️ Вводить водій |
| Паливо на початку | Тільки читання |
| Видано палива | Тільки читання |
| Паливо в кінці | ✏️ Вводить водій |
| Витрачено палива | Розраховується автоматично, або можна ввести вручну |
При введенні "Паливо в кінці" — "Витрачено" рахується автоматично:
Натиснути "Зберегти" → зберігається локально → при наступній sync надсилається на сервер.
Путні листи зі статусом posted або cancelled — тільки для читання.
Замовлення (Orders)¶
- Список замовлень на перевезення
- Деталі: клієнт, маршрут, вантаж, вага, дата доставки, ціна
Замовлення тільки для читання в мобільному застосунку. Статус змінює диспетчер в DOP.
Профіль¶
- Ім'я, email, телефон
- Дані посвідчення водія (категорія, номер, дата закінчення)
- Перемикач мови (🇺🇦 UA ↔ 🇬🇧 EN)
- Кнопка "Вийти" — підтвердження → logout
Синхронізація¶
Ручна sync: - Dashboard → натиснути ↻ (кнопка справа вгорі) - Dashboard → потягнути список вниз (pull-to-refresh)
Автоматична sync: - При відкритті Dashboard (якщо є інтернет)
Офлайн: - Жовтий банер "Offline — showing cached data" вгорі екрану - Всі дані доступні з кешу - Введені зміни зберігаються локально - При відновленні зв'язку → ручна або автоматична sync
9. Sync: детально¶
Перша синхронізація (initial sync)¶
last_pulled_at = 0 (нуль = "дай все")
GET /api/v1/mobile/sync/pull?last_pulled_at=0
Response:
{
"changes": {
"waybills": {
"created": [{ "id": "1", "number": "WB-001", ... }],
"updated": [],
"deleted": []
},
"orders": { "created": [...], "updated": [], "deleted": [] },
...
},
"timestamp": 1703000000000 ← зберігається як new lastPulledAt
}
WatermelonDB вставляє всі записи в локальну БД.
Інкрементальна синхронізація¶
last_pulled_at = 1703000000000 (час попередньої sync)
GET /api/v1/mobile/sync/pull?last_pulled_at=1703000000000
Response:
{
"changes": {
"waybills": {
"created": [],
"updated": [{ "id": "5", "status": "approved" }], ← тільки що змінилось
"deleted": []
},
...
},
"timestamp": 1703100000000
}
WatermelonDB оновлює тільки змінені записи — ефективно.
Push (відправка змін водія)¶
POST /api/v1/mobile/sync/push
Body:
{
"changes": {
"waybills": {
"created": [],
"updated": [
{
"id": "3",
"odometer_end": 152500,
"fuel_end": 45.5,
"fuel_consumed": 67.0,
"distance": 380
}
],
"deleted": []
}
}
}
Сервер знаходить Waybill з id=3 і оновлює дозволені поля.
ID стратегія¶
Django використовує integer PK (1, 2, 3, ...). WatermelonDB потребує string ID. Сервер повертає str(pk) як ID:
WatermelonDB зберігає "1" як string ID. При push сервер конвертує назад:
10. Конфігурація¶
Де змінити URL бекенду¶
Два файли (обидва треба змінити):
src/api/client.ts рядок 7:
const BASE_URL = __DEV__
? 'http://10.0.2.2:8000/api/v1' // DEV (Android emulator)
: 'https://api.eswf.dev/api/v1'; // PRODUCTION
src/api/sync.ts рядок 5:
const BASE_URL = __DEV__
? 'http://10.0.2.2:8000/api/v1' // DEV
: 'https://api.eswf.dev/api/v1'; // PRODUCTION
Для різних сценаріїв:
| Сценарій | URL |
|---|---|
| Android Emulator (localhost backend) | http://10.0.2.2:8000/api/v1 |
| iOS Simulator (localhost backend) | http://localhost:8000/api/v1 |
| Реальний телефон (Wi-Fi, backend на ноутбуці) | http://192.168.X.X:8000/api/v1 |
| Production | https://api.eswf.dev/api/v1 |
Де змінити ім'я бази даних¶
src/database/index.ts:
Де змінити мову за замовчуванням¶
src/i18n/index.ts:
11. Production збірка¶
Підготовка¶
1. Оновити app.json:
{
"expo": {
"name": "ESWF Driver",
"version": "1.0.1", ← інкрементувати при кожному релізі
"ios": {
"buildNumber": "2", ← для iOS
"bundleIdentifier": "dev.eswf.driver"
},
"android": {
"versionCode": 2, ← для Android (цілі числа, тільки збільшувати)
"package": "dev.eswf.driver"
}
}
}
2. Змінити URL на production:
// src/api/client.ts і src/api/sync.ts
const BASE_URL = __DEV__ ? '...' : 'https://api.eswf.dev/api/v1';
3. Налаштувати CORS на бекенді — додати мобільний клієнт (по origin якщо є, або залишити без обмежень для API):
# backend/eswf/settings/production.py
CORS_ALLOWED_ORIGINS = ['https://erp.eswf.dev', 'https://shop.eswf.dev']
Збірка¶
# Android (APK для прямого розповсюдження)
eas build --platform android --profile preview
# Android (App Bundle для Google Play)
eas build --platform android --profile production
# iOS (потрібен Apple Developer аккаунт)
eas build --platform ios --profile production
OTA Updates (оновлення без перебілда)¶
Якщо зміни тільки в JS (не native):
Застосунок завантажить оновлення автоматично при наступному запуску.
12. Що ще можна додати¶
| Функція | Пріоритет | Складність |
|---|---|---|
| Push-нотифікації (нове замовлення) | Високий | Середня |
| Фото вантажу (expo-camera) | Середній | Середня |
| GPS трекінг маршруту (expo-location) | Середній | Висока |
| Підпис водія (react-native-signature-canvas) | Середній | Низька |
| QR-сканер для ТТН | Низький | Низька |
| Автоматична sync по таймеру (background fetch) | Низький | Середня |
| Zod валідація форм | Низький | Низька |
| Друк документів (expo-print) | Низький | Середня |
| Темна/світла тема (system preference) | Низький | Низька |
| Drag-to-reorder маршрутних точок | Низький | Висока |
Push-нотифікації (наступний крок)¶
Бекенд може надсилати нотифікацію коли диспетчер призначає нове замовлення водію. WatermelonDB тоді автоматично sync при отриманні.
GPS трекінг¶
Зберігати координати в новій таблиці location_logs WatermelonDB. Sync на сервер → відображення на карті в DOP.
Швидкий старт (TL;DR)¶
# 1. Встановити залежності
cd c:\eswf\mobile && npm install
# 2. Згенерувати native проекти
npx expo prebuild
# 3. Прив'язати Driver до User в Django:
# python manage.py shell
# >>> driver.user = user; driver.save()
# 4. Запустити бекенд
cd c:\eswf\backend
python manage.py runserver 8000
# 5. Запустити на Android emulator
cd c:\eswf\mobile
npx expo run:android
# 6. Або зібрати APK для телефону
eas build --platform android --profile preview
13. Sales App — короткий огляд¶
Локація: c:\eswf\mobile-sales\
Backend API: backend/sales_mobile_api/ → /api/v1/sales-mobile/
Статус: ✅ Реалізовано (потребує expo prebuild)
Що вміє Sales App (відмінності від Driver)¶
| Можливість | Driver | Sales |
|---|---|---|
| Офлайн-режим (WatermelonDB) | ✅ 4 таблиці | ✅ 23 таблиці |
| Перегляд призначених завдань | ✅ Waybills, Orders | ✅ Clients, Visits, DayPlan |
| Введення значень | ✅ одометр, паливо | ✅ Invoice + рядки + ціноутворення |
| GPS-трекінг | ❌ | ✅ background location, GPS-радіус 200м для compliance |
| Фото | ❌ | ✅ камера + галерея, до рахунків і візитів |
| Цифровий підпис | ❌ | ✅ react-native-signature-canvas |
| Офлайн-створення документів | ❌ (тільки оновлення) | ✅ повна форма Invoice з рядками, знижками, ПДВ |
| Push-сповіщення | ❌ | ✅ FCM через expo-notifications |
| Дзвінки клієнтам | ❌ | ✅ expo-linking (tel:) + CallLog |
| AI-чат + DM месенджер | ❌ | ✅ SSE streaming + WebSocket |
| Дашборд KPI / рейтинг | ❌ | ✅ |
Структура mobile-sales/src/¶
api/ client.ts, auth.ts, sync.ts, photos.ts, chat.ts
auth/ AuthContext.tsx, tokenStorage.ts
database/ schema.ts (23 таблиці), index.ts, migrations.ts, models/ (25 моделей)
navigation/ AppNavigator.tsx — 7 bottom-tabs + nested stacks
screens/
Clients/ ClientsListScreen, ClientDetailScreen
Catalog/ CatalogScreen
Invoices/ InvoicesListScreen, InvoiceFormScreen, InvoiceDetailScreen
Visits/ VisitsListScreen, ActiveVisitScreen, DayPlanScreen
Payments/ PaymentScreen
Returns/ ReturnFormScreen, ReturnsListScreen
Chat/ ChatListScreen, ChatSessionScreen
Reports/ ReportsDashboardScreen
+ LoginScreen, DashboardScreen, ProfileScreen
components/ StatusBadge, DebtBadge, OfflineBanner, SyncIndicator,
CallLogModal, ItemPickerModal
hooks/ useSync, useNetworkStatus, useGpsTracking,
useChatStream, useRoomWebSocket
services/ GpsTracker.ts, PhotoService.ts, NotificationService.ts
i18n/ en.json, ua.json (~80 ключів)
theme.ts Темна тема (узгоджена з DOP)
Backend sales_mobile_api/¶
9 моделей: SalesVisit, VisitPhoto, InvoicePhoto, CallLog,
DayPlan + DayPlanItem, SalesReturn + SalesReturnLine,
ClientSignature, DeviceToken.
Endpoints: /sync/pull, /sync/push, /me/, /photos/, /signatures/,
/client-debt/, /reports/dashboard/, /reports/ranking/, /register-device/.
Зв'язок з Essentials: до Client додано FK manager (= sales_rep), за яким
фільтруються дані синхронізації.
Супервізор торгових представників (web, не mobile)¶
Серверна частина для керівника ТП — у Django app sales_field/, frontend
у DOP (SalesFieldManager.tsx). Можливості: KPI-дашборд, день-плани, копіювання
тижня, GPS-радіус 200м для compliance, рейтинг ТП. Деталі — CLAUDE.md.
Запуск Sales App¶
Публікація¶
Документація оновлена: 2026-04-21 (додано Sales App overview)