diff --git a/apps/api/src/venues/venues.controller.ts b/apps/api/src/venues/venues.controller.ts index 5f3b36e..bc4046b 100644 --- a/apps/api/src/venues/venues.controller.ts +++ b/apps/api/src/venues/venues.controller.ts @@ -1,12 +1,10 @@ -import { Controller, Get, Param, UseGuards, Req } from '@nestjs/common'; +import { Controller, Get, Param, UseGuards } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; import { VenuesService } from './venues.service'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { PrismaService } from '../prisma/prisma.service'; @ApiTags('Venues') -@ApiBearerAuth() -@UseGuards(JwtAuthGuard) @Controller('venues') export class VenuesController { constructor( @@ -15,18 +13,22 @@ export class VenuesController { ) {} @Get() - @ApiOperation({ summary: 'List all venues' }) + @ApiOperation({ summary: 'List all venues (public)' }) async findAll() { return this.venuesService.findAll(); } @Get(':venueId') + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) @ApiOperation({ summary: 'Get venue by ID' }) async findOne(@Param('venueId') venueId: string) { return this.venuesService.findOne(venueId); } @Get(':venueId/devices') + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) @ApiOperation({ summary: 'List devices for a venue' }) async findDevices(@Param('venueId') venueId: string) { const devices = await this.prisma.device.findMany({ diff --git a/docs/flutter-integration-prompt.md b/docs/flutter-integration-prompt.md new file mode 100644 index 0000000..fc23fb5 --- /dev/null +++ b/docs/flutter-integration-prompt.md @@ -0,0 +1,163 @@ +## KoolTab Flutter App — Remote Backoffice Integration (Improved Pairing Flow) + +You are implementing the device-side integration for the KoolTab remote tab control system. The Flutter app (bar POS terminal) needs to register with a remote backend, sync open tabs, and process remote close-tab commands. + +### Project Location +`C:\Users\bram\StudioProjects\kooltab2` + +### Architecture Overview +- State management: `provider` (ChangeNotifier pattern) +- Routing: `go_router` +- Local DB: `drift` (SQLite ORM) +- HTTP: `http` package +- Auth tokens stored in `flutter_secure_storage` +- MVVM pattern: views → viewmodels → services +- Models in `lib/models/` +- Services in `lib/services/` +- ViewModels in `lib/viewmodels/` +- Views in `lib/views/` +- Config constants in `lib/utils/app_config.dart` +- Main app bootstrap in `lib/app/app_bootstrap.dart` + +### Pairing Flow (improved) +Instead of typing a UUID venueId, the flow is: + +1. Tablet calls `GET https://api.kooltab.com/api/v1/venues/` (public, no auth) +2. Gets list of `[{id, name}]` — admin picks venue from a dropdown +3. Admin enters device name (e.g. "Bar terminal 1") +4. Admin enters the 6-character registration code (generated from dashboard) +5. Tablet calls `POST /devices/register` with venueId + name + code +6. Stores deviceId + token + venueId + serverUrl in secure storage +7. Sync starts automatically + +### Existing Sync Infrastructure (already built) +Do NOT rewrite these files, just modify as needed: + +- **`lib/services/remote_sync_service.dart`** (410 lines) — `setCredentials()`, `startSync()`, `stopSync()`, sync loop, command processing, ACKs +- **`lib/viewmodels/remote_sync_view_model.dart`** (90 lines) — ChangeNotifier wrapper +- **`lib/models/remote_command.dart`** (135 lines) — DTOs: `RemoteCommand`, `SyncRequest`, `SyncResponse`, `SnapshotTab`, `CommandAck` +- **`lib/utils/app_config.dart`** — URL constants +- **`lib/models/payment_method.dart`** — payment enums + +### What to Build + +#### 1. Update Server URL +In `lib/utils/app_config.dart`: +```dart +const kRemoteServerUrl = 'https://api.kooltab.com/api/v1/'; +``` + +#### 2. Add Venue Model +Create `lib/models/venue.dart`: +```dart +class Venue { + final String id; + final String name; + Venue({required this.id, required this.name}); + factory Venue.fromJson(Map json) => Venue(id: json['id'], name: json['name']); +} +``` + +#### 3. Update RemoteSyncService — Add venueId Storage +In `lib/services/remote_sync_service.dart`: +- Add `venueId` parameter to `setCredentials()` signature +- Store venueId in FlutterSecureStorage alongside deviceId/token/serverUrl +- Add `String? get venueId` getter +- Update `isConfigured` to also check venueId exists +- Add `clearCredentials()` method (clears all stored credentials) + +#### 4. Registration ViewModel +Create `lib/viewmodels/registration_view_model.dart`: +- `Future> fetchVenues()` — calls `GET /api/v1/venues/` +- `Future registerDevice(String venueId, String name, String code)` — calls `POST /devices/register`, then stores credentials via `RemoteSyncService.setCredentials()` +- States: `initial`, `loading`, `venuesLoaded`, `error`, `registered` +- Expose as ChangeNotifier + +#### 5. Registration UI +Create `lib/views/registration_view.dart` — a full-screen registration flow with steps: + +**Step 1 — Select venue:** +- Dropdown list of venues fetched from API +- Show loading spinner while fetching +- Show error + retry if fetch fails +- "Next" button when venue selected + +**Step 2 — Device details:** +- Device name text field (with default "Bar terminal 1") +- Registration code text field (6 characters) +- "Register" button +- Loading state while registering +- Error display if registration fails + +**Step 3 — Success screen:** +- Success animation/message +- "Start using KoolTab" button → navigates to main bar screen and triggers sync + +#### 6. Wire Registration into Navigation +In `lib/app/app_bootstrap.dart`: +- On app startup, check `remoteSyncService.isConfigured` +- If NOT configured: show registration screen instead of main bar screen +- If configured: proceed to main bar screen as normal + +In `lib/app/router.dart`: +- Add route for registration screen as initial route when not configured + +#### 7. Disconnect Option +In the **Settings screen** (`lib/views/settings_view.dart`) or **Admin screen**: +- If configured, show "Connected to [venue name]" with device name +- "Disconnect" button → calls `clearCredentials()`, navigates back to registration screen + +### API Endpoints Summary + +| Method | Endpoint | Auth | Purpose | +|--------|----------|------|---------| +| GET | `/api/v1/venues/` | None | List venues (returns `[{id, name}]`) | +| POST | `/api/v1/devices/register` | None | Register device (body: venueId, name, registrationCode) | +| POST | `/api/v1/devices/:id/sync` | Bearer token | Sync tabs, receive commands | +| POST | `/api/v1/devices/:id/commands/:cmdId/ack` | Bearer token | Acknowledge command | + +### Registration API Details + +**Venues list:** +``` +GET https://api.kooltab.com/api/v1/venues/ +Response: [{"id": "uuid", "name": "KoolTab Demo Bar"}, ...] +``` + +**Device registration:** +``` +POST https://api.kooltab.com/api/v1/devices/register +{ + "venueId": "uuid-from-venue-list", + "name": "Bar terminal 1", + "registrationCode": "A8F3C2" +} +Response 201: +{ + "deviceId": "uuid", + "token": "dev_abc123...", + "tokenExpiresAt": "2026-..." +} +``` + +The registration code is generated from the dashboard (Devices → Add Device → generates 6-char code valid for 10 minutes). + +### Files to Create +- `lib/models/venue.dart` +- `lib/viewmodels/registration_view_model.dart` +- `lib/views/registration_view.dart` + +### Files to Modify +- `lib/utils/app_config.dart` — update URL +- `lib/services/remote_sync_service.dart` — add venueId + clearCredentials +- `lib/app/app_bootstrap.dart` — check isConfigured, route to registration if not +- `lib/app/router.dart` — add registration route +- `lib/views/admin_screen_view.dart` or `lib/views/settings_view.dart` — add disconnect option + +### Important Notes +- Do NOT break the existing sync loop — registration only configures credentials +- The existing sync sends `SnapshotTab` and processes `close_tab` commands stays unchanged +- Use `flutter_secure_storage` for persisting credentials +- Handle loading/error states in the UI +- After registration, call `remoteSyncViewModel.start()` to begin syncing +- Tabs appear on the dashboard within ~90 seconds of first sync \ No newline at end of file