# Legacy mobile app — reference for the Phase 1 greenfield rebuild > Source: the inherited Create/Anything export under `apps/mobile/` (LEGACY, being removed). > This document captures everything the Phase 1 greenfield rebuild of the **worker app** > needs to reproduce: screens, flows, every Dutch UI string, the stopwatch state machine, > selection + count behaviour, History/CSV export, Settings management, navigation, and styling. > It is a behavioural spec, **not** an instruction to reuse the legacy code. App name: **SoleLog** — a time-tracking app for insole (orthotics / inlegzolen) production. A worker picks an **insole type** (`Type zool`: Kurk / Berk / 3D), a **handling/task** (`Type handeling`), and a **count** (`Aantal zolen`, default `2`), then runs a stopwatch (start / pause / stop & save / double-press discard). The UI is entirely in **Dutch**. Files this doc is built from (all under `apps/mobile/src/`): | File | Role | |---|---| | `app/_layout.tsx` | Root layout: auth gate, splash, React Query provider, Stack | | `app/index.tsx` | Empty redirect stub (`export default () => null`) | | `app/(tabs)/_layout.tsx` | Bottom tab navigator (3 tabs) | | `app/(tabs)/index.tsx` | **Stopwatch** screen (the main worker flow) | | `app/(tabs)/history.tsx` | **Geschiedenis** (History) screen + CSV export | | `app/(tabs)/tasks.tsx` | **Instellingen** (Settings) — manage handelingen per zooltype | | `app/+not-found.tsx` | Platform 404 screen (Create-managed; not worker-facing) | | `__create/fetch.ts` | Monkey-patched global `fetch` (URL rewrite, headers, JWT) | --- ## 1. Navigation structure Expo Router, file-based. - **Root** `app/_layout.tsx`: a `Stack` with `initialRouteName="(tabs)"`, `headerShown: false`. Gated on `useAuth().initiate()` + `isReady` (loads the persisted session from SecureStore before first render; returns `null` until ready, with a 10 s splash timeout fallback). Wraps everything in `ErrorBoundary` → `QueryClientProvider` → `GestureHandlerRootView`. Renders `` alongside the `Stack`. - React Query defaults: `staleTime` 5 min, `gcTime` 30 min, `retry: 1`, `refetchOnWindowFocus: false`. - **`app/(tabs)/_layout.tsx`**: a bottom **`Tabs`** navigator with `headerShown: false` and **three tabs**, in this order: | Order | Route file | Tab title (Dutch) | Icon (`lucide-react-native`) | |---|---|---|---| | 1 | `index.tsx` | **Stopwatch** | `Timer` | | 2 | `history.tsx` | **Geschiedenis** | `History` | | 3 | `tasks.tsx` | **Instellingen** | `Settings` | Tab bar styling: white background, 1px top border `#E5E7EB`, `paddingTop: 4`; active tint `#2563EB`, inactive tint `#6B7280`; label `fontSize: 12, fontWeight: '500'`; icons `size={24}`. The greenfield rebuild should keep this 3-tab shell. Note the tab **route** is `index` but its **title** is `Stopwatch` — the first/landing tab is the stopwatch. --- ## 2. Shared visual language / styling Screens use inline React Native `StyleSheet`/style objects (NativeWind exists in the template but the worker screens do not use it). Notable, reusable tokens observed across all three tabs: - **Font**: Inter, loaded via `@expo-google-fonts/inter` — `Inter_400Regular` (`regular`) and `Inter_600SemiBold` (`semibold`). Each screen guards on `useFonts`: `if (!fontsLoaded && !fontError) return null;` — **but if font loading errors it renders anyway** (explicit comment: "prevents Android freeze"). On the Stopwatch screen the font family falls back to `undefined` when `fontError` is set. - **Palette**: - Primary blue `#2563EB`; light-blue surfaces `#EFF6FF` / `#F0F7FF`; blue border `#BFDBFE`. - Text: near-black `#111827`, dark grey `#374151`, mid grey `#6B7280`, muted `#9CA3AF`, disabled `#D1D5DB`. - Borders/surfaces: `#E5E7EB` (borders), `#F9FAFB` / `#F3F4F6` (light fills), white `#ffffff`. - Danger red `#DC2626` (Stop button, delete); danger surface `#FEF2F2`. - Paused / amber: text `#D97706`, dot `#F59E0B`, border `#FDE68A`, surface `#FFFBEB`. - Success green: `#16A34A` text, `#DCFCE7` surface (Settings "Opslaan"). - **Per-zooltype colour set** (`tasks.tsx`, `TYPE_COLORS`) — used for toggles and badges: | Type | bg | border | text | |---|---|---|---| | `Kurk` | `#FEF9C3` | `#FDE047` | `#854D0E` | | `Berk` | `#DCFCE7` | `#86EFAC` | `#166534` | | `3D` | `#EDE9FE` | `#C4B5FD` | `#5B21B6` | - **Shapes**: heavy rounding — section cards `borderRadius: 12–16`, pills `borderRadius: 999`, primary buttons `borderRadius: 16`, the stopwatch display `borderRadius: 24`. - **Safe areas**: each screen reads `useSafeAreaInsets()` and applies `paddingTop: insets.top`. --- ## 3. The three insole types (`Type zool`) Defined identically in both the Stopwatch and Settings screens: ```ts const INSOLE_TYPES = ['Kurk', 'Berk', '3D'] as const; // Stopwatch const ALL_TYPES = ['Kurk', 'Berk', '3D'] as const; // Settings ``` | Value (verbatim) | English meaning | |---|---| | `Kurk` | Cork insole | | `Berk` | Birch (birchwood) insole | | `3D` | 3D-printed insole | These are the only valid insole types and are **hard-coded** (not fetched). The default selected type on the Stopwatch is `'Kurk'`. --- ## 4. Stopwatch screen (`app/(tabs)/index.tsx`) — the core worker flow Vertically scrollable, white background. Five stacked sections, then a bottom sheet. ### 4.1 Local state ```ts const [activeTaskId, setActiveTaskId] = useState(null); const [insoleType, setInsoleType] = useState('Kurk'); const [isRunning, setIsRunning] = useState(false); const [isPaused, setIsPaused] = useState(false); const [startTime, setStartTime] = useState(null); const [elapsedTime, setElapsedTime] = useState(0); // seconds const [showPicker, setShowPicker] = useState(false); // bottom sheet const [discardPending, setDiscardPending] = useState(false); // double-press arm const [insoleCount, setInsoleCount] = useState(2); // numeric count (default 2) const [insoleCountText, setInsoleCountText] = useState('2'); // text mirror for the input ``` Refs: `timerRef` (the 1 s interval), `discardTimerRef` (3 s discard-confirm window), `slideAnim` (Animated value for the sheet, starts off-screen at `SHEET_HEIGHT`). `SHEET_HEIGHT = Dimensions.get('window').height * 0.75`. ### 4.2 Data - `tasks` via React Query key `['tasks']` → `GET {BASE_URL}/api/tasks`. - `saveLogMutation` → `POST {BASE_URL}/api/logs`; on success invalidates `['logs']`. Derived values: ```ts const selectedTask = tasks.find(t => t.id === activeTaskId); const canStart = !!activeTaskId; // a handling MUST be picked to start const filteredTasks = tasks.filter(t => Array.isArray(t.insole_types) ? t.insole_types.includes(insoleType) : true ); // handlings shown depend on chosen zooltype ``` ### 4.3 Section 1 — `Type zool` (insole type selector) - Label (uppercase, letter-spaced, grey): **`Type zool`**. - Three equal-width segmented buttons, one per `INSOLE_TYPES` value (`Kurk`, `Berk`, `3D`). - Selected: blue border `#2563EB`, light-blue fill `#EFF6FF`, blue text. Unselected: grey. - **Disabled while the stopwatch is running** (`disabled={isRunning}`, text greys to `#9CA3AF`). - Tapping a type when not running: `setInsoleType(type)` **and resets** `setActiveTaskId(null)` — i.e. **changing the zooltype clears the chosen handling** (because the handling list is filtered by type). ### 4.4 Section 2 — `Type handeling` (handling/task picker) - Label: **`Type handeling`**. - A single full-width dropdown row showing the chosen task name, or the placeholder **`Kies een handeling...`** ("Choose a handling…") when none is selected (`activeTaskId` null). Trailing `ChevronDown` icon. - Disabled while running. When tapped (and not running) it calls `openPicker()` → opens the bottom-sheet modal (see 4.8). ### 4.5 Section 3 — `Aantal zolen` (count of insoles) - Label: **`Aantal zolen`** ("Number of insoles"). - A combined stepper: a `−` button (width 64), a centred numeric `TextInput` (`keyboardType="number-pad"`), and a `+` button (width 64), inside one rounded bordered row. - **Default value `2`.** - `−` is disabled when `insoleCount <= 1` or running; `+` disabled when running; the field is `editable={!isRunning}`. - Behaviour: ```ts const handleInsoleCountChange = (text) => { setInsoleCountText(text); const parsed = parseInt(text, 10); if (!isNaN(parsed) && parsed > 0) setInsoleCount(parsed); // only accept >0 }; const adjustInsoleCount = (delta) => { const next = Math.max(1, insoleCount + delta); // floor of 1 setInsoleCount(next); setInsoleCountText(String(next)); }; ``` The text mirror lets the user type freely; the committed numeric `insoleCount` only updates for a valid positive integer. **Minimum is 1.** This value is sent as `pair_count` on save. ### 4.6 Section 4 — Stopwatch display (tap target) - A large rounded card showing the elapsed time formatted **`HH:MM:SS`** (each part zero-padded), font size 64. `formatTime(seconds)` does `hrs/mins/secs` with `padStart(2,'0')`. - The card itself is a tap target with overlaid status pill: - **Not running, can start** (a handling chosen): time greyed `#9CA3AF`; pill (blue dot + blue text) reads **`Tik om te starten`** ("Tap to start"). Tapping the card starts. - **Running, not paused**: time black; pill reads **`Tik om te pauzeren`** ("Tap to pause"). Tapping pauses. - **Running, paused**: time amber `#D97706`, card border amber `#FDE68A`; pill (amber) reads **`Gepauzeerd — tik om te hervatten`** ("Paused — tap to resume"). Tapping resumes. - **Not running, no handling chosen** (`!canStart`): no pill, time greyed, tap is a no-op (`activeOpacity` 1). ### 4.7 Section 5 — Action buttons (`Knoppen`) - **When not running**: a single full-width primary button with a `Play` icon, label **`Start Stopwatch`**. Enabled only when `canStart` (a handling is selected); otherwise greyed `#E5E7EB` and disabled. - **When running**: two stacked buttons: - Red button, `Square` icon, label **`Stop & Opslaan`** ("Stop & Save") → `handleStop()`. - Below it, the discard button (the **double-press discard**): - Idle label: **`Annuleren`** ("Cancel"), light grey fill `#F3F4F6`, grey text. - Armed label: **`Nogmaals tikken ter bevestiging`** ("Tap again to confirm"), dark fill `#374151`, white text. ### 4.8 Bottom-sheet handling picker (`Modal`) - Transparent `Modal`, `animationType="none"`, `statusBarTranslucent`. Backdrop is a `Pressable` (`rgba(0,0,0,0.45)`) that closes the sheet (explicit comment: uses `Pressable` not nested `TouchableWithoutFeedback` as an Android fix). The sheet is an `Animated.View` sliding via `translateY: slideAnim` (open → 0 over 300 ms; close → `SHEET_HEIGHT` over 250 ms). - A drag-handle bar at top. - **Header**: title **`Type handeling`**, subtitle **`Kies een handeling`** ("Choose a handling"). - **List**: the **filtered** tasks (only those whose `insole_types` includes the current `insoleType`). Each row shows `task.name`; the selected row is highlighted blue with a trailing `Check` icon. Tapping a row sets `activeTaskId` and closes the sheet. - **Empty state** (no handlings for the chosen type): centered grey text — **`Geen handelingen beschikbaar voor {insoleType} zolen. Voeg ze toe via Instellingen.`** ("No handlings available for {type} insoles. Add them via Settings.") — the `{insoleType}` is the live selected value (e.g. "Berk"). ### 4.9 Stopwatch state machine States are the cross product of `isRunning` × `isPaused` (plus a transient `discardPending`). ``` ┌───────────────────────────────────────────────┐ │ IDLE / STOPPED │ │ isRunning=false, isPaused=false, elapsed=0 │ │ (zooltype + handling + count are editable) │ └───────────────────────────────────────────────┘ │ Start (only if canStart === a handling is chosen) ▼ ┌───────────────────────────────────────────────┐ ┌──────▶ │ RUNNING │ ──────┐ │ │ isRunning=true, isPaused=false │ │ │ │ +1s every second via setInterval │ │ │ Resume │ (selectors locked: zool/handling/count read-only) │ Stop&Save │ └───────────────────────────────────────────────┘ │ │ │ Pause │ │ ▼ │ │ ┌───────────────────────────────────────────────┐ │ └─────── │ PAUSED │ │ │ isRunning=true, isPaused=true │ │ │ interval cleared (time frozen, amber styling) │ │ └───────────────────────────────────────────────┘ │ │ Cancel ×2 (within 3s) ── DISCARD ──────┐ │ ▼ ▼ ▼ back to IDLE (elapsed reset, nothing saved) POST /api/logs → IDLE ``` Transitions, from the handlers: - **Start** (`handleStart`): guard `if (!activeTaskId) return;` then `isRunning=true; isPaused=false; startTime=new Date()`. - **Tick**: `useEffect` on `[isRunning, isPaused]` — when running **and not paused**, sets a 1 s `setInterval` that does `setElapsedTime(prev => prev + 1)`; otherwise clears it. Cleared on unmount. (Time is counted purely by interval ticks, **not** by wall-clock diff — so background throttling could under-count; the rebuild may prefer wall-clock delta.) - **Pause** (`handlePause`): `isPaused=true`. **Resume** (`handleResume`): `isPaused=false`. - **Stop & Save** (`handleStop`): guard `if (!activeTaskId || !startTime) return;` then `isRunning=false; isPaused=false`, compute `endTime=new Date()`, fire `saveLogMutation` (see 4.10), then reset `startTime=null; elapsedTime=0; discardPending=false` and clear the discard timer. - **Discard / double-press cancel** (`handleDiscard`): - First tap: `discardPending=true` and start a 3 s timer that re-clears `discardPending` (so the confirm window auto-expires after 3 seconds). - Second tap **within 3 s**: clear the timer and fully reset (`isRunning=false; isPaused=false; startTime=null; elapsedTime=0; discardPending=false`) — **nothing is saved**. This is the "double-press discard": tap `Annuleren` once to arm (button changes to **`Nogmaals tikken ter bevestiging`**), tap again to actually discard. After a stop or discard, all three selectors (zooltype, handling, count) become editable again. Note: after Stop & Save, `activeTaskId`, `insoleType`, and `insoleCount` are **not** reset, so the next session keeps the previous selections (only the timer resets). ### 4.10 Save payload (`POST /api/logs`) ```ts saveLogMutation.mutate({ task_id: activeTaskId, start_time: startTime.toISOString(), end_time: endTime.toISOString(), duration_seconds: elapsedTime, pair_count: insoleCount, insole_type: insoleType, }); ``` `duration_seconds` is the accumulated tick count; `pair_count` is the `Aantal zolen` value; `insole_type` is one of `Kurk` / `Berk` / `3D`. (No `notes` are sent from mobile.) --- ## 5. History screen (`app/(tabs)/history.tsx`) — `Geschiedenis` ### 5.1 Layout - **Header row**: title **`Geschiedenis`** ("History") on the left; on the right a pill button with a `Download` icon and label **`Exporteer CSV`** ("Export CSV"). - **Body**: a scrollable list of session cards, fetched via React Query key `['logs']` → `GET {BASE_URL}/api/logs`. - **Empty state** (no logs, not loading): centered grey text **`Nog geen opgeslagen sessies.`** ("No saved sessions yet."). ### 5.2 Each log card Per `log`, a bordered white card showing: - **Title**: `log.task_name` (the handling name; joined from `production_tasks.name` server-side). - **Date/time line**: a `Calendar` icon + `{formatDate(log.start_time)} • {formatTime(log.start_time)}`. - `formatDate` → `toLocaleDateString(undefined, { month:'short', day:'numeric', year:'numeric' })`. - `formatTime` → `toLocaleTimeString(undefined, { hour:'2-digit', minute:'2-digit' })`. (Locale-default; `undefined` means the device locale.) - **Right-side badges** (rendered conditionally): - `log.insole_type` → a grey pill showing the type verbatim (`Kurk`/`Berk`/`3D`). - `log.pair_count != null` → a blue pill with a `Layers` icon and `{pair_count} {pair_count === 1 ? 'inlegzool' : 'inlegzolen'}` — i.e. Dutch singular/plural: **`inlegzool`** (1 insole) / **`inlegzolen`** (>1 insoles). - Always: a grey pill with a `Clock` icon showing `formatDuration(log.duration_seconds)`: ```ts if (hrs > 0) return `${hrs}h ${mins}m`; // e.g. "1h 5m" if (mins > 0) return `${mins}m ${secs}s`; // e.g. "3m 20s" return `${secs}s`; // e.g. "45s" ``` ### 5.3 CSV export ```ts const handleExport = async () => { const exportUrl = `${BASE_URL}/api/export`; if (await Linking.canOpenURL(exportUrl)) await Linking.openURL(exportUrl); else Alert.alert('Fout', 'Kan de export-URL niet openen'); }; ``` - Tapping **`Exporteer CSV`** opens `GET {BASE_URL}/api/export` in the OS browser/handler (the device downloads the CSV; the app does not parse it). - Failure alert: title **`Fout`** ("Error"), body **`Kan de export-URL niet openen`** ("Cannot open the export URL"). - The CSV is produced server-side (`apps/web/api/export`); for parity the rebuilt backend's CSV should match its shape. Observed columns (English headers, `nl-BE` formatting, quoted, `\n` joined, ordered by `start_time ASC`): `ID, Task, Insole Type, No. of Insoles, Date, Total Duration, Start Time, End Time` - `Insole Type` defaults to `Kurk` if null; `No. of Insoles` defaults to `2` if null. - `Date` = `nl-BE` `dd-mm-yyyy`; `Start/End Time` = `nl-BE` `HH:MM:SS`; `Total Duration` = `HH:MM:SS` derived from `duration_seconds`. - Filename: `insole-production-report.csv`; `Content-Type: text/csv; charset=utf-8`. --- ## 6. Settings screen (`app/(tabs)/tasks.tsx`) — `Instellingen` (handelingen per zooltype) This screen manages the **handelingen** (handlings/tasks), each tagged with the zooltypes it applies to. Wrapped in `KeyboardAvoidingView` (iOS `padding`). ### 6.1 Header - Title **`Instellingen`** ("Settings"). - Subtitle **`Beheer handelingen per zooltype`** ("Manage handlings per insole type"). ### 6.2 "Add new handling" card - Section label (uppercase): **`Nieuwe handeling toevoegen`** ("Add new handling"). - **Name input** with placeholder **`Naam van de stap, bijv. Leerrand`** ("Name of the step, e.g. Leerrand"). (`Leerrand` ≈ "leather edge/rim" — an example step name.) - A sub-label **`Van toepassing op`** ("Applies to") above three **`TypeToggle`** pills (`Kurk` / `Berk` / `3D`), each coloured per `TYPE_COLORS`, showing a `Check` when selected. **Default selection for a new handling is all three types** (`['Kurk', 'Berk', '3D']`). - **Add button** with a `Plus` icon, label **`Stap toevoegen`** ("Add step"). Disabled (greyed) unless the name is non-empty (trimmed) **and** at least one type is selected; shows an `ActivityIndicator` while the mutation is pending. ### 6.3 Handling list - Section label: **`Huidige stappen ({tasks.length})`** ("Current steps (N)"). - Loading: a blue `ActivityIndicator`. - Empty: **`Nog geen stappen. Voeg er een toe hierboven.`** ("No steps yet. Add one above."). - Each handling is a card. **Display mode**: - `task.name` on the left; on the right two square icon buttons: - blue `Pencil` button → enters edit mode for that row. - red `Trash2` button → triggers delete (see 6.5). - Below: a row of coloured **`TypeBadge`** pills, one per type in `task.insole_types`. - **Edit mode** (`editingId === task.id`): the card border turns blue and shows: - an auto-focused name `TextInput` (pre-filled with the current name); - the **`Van toepassing op`** label + three `TypeToggle` pills (pre-filled from the task's types; if `insole_types` isn't an array it defaults to all three); - two buttons: green **`Opslaan`** ("Save", `Check` icon) and grey **`Annuleren`** ("Cancel", `X` icon). Save is disabled while pending or if no types are selected. ### 6.4 Mutations / API All keyed off React Query `['tasks']`; success invalidates `['tasks']`. - **Add** (`addTaskMutation`) → `POST {BASE_URL}/api/tasks` body `{ name, insole_types }`. On success: clears the name and resets type selection to all three. - **Update** (`updateTaskMutation`) → `PUT {BASE_URL}/api/tasks/{id}` body `{ name, insole_types }`. On success: exits edit mode. - **Delete** (`deleteTaskMutation`) → `DELETE {BASE_URL}/api/tasks/{id}`. On success: invalidates **both** `['tasks']` and `['logs']` (because deleting a task cascades to its logs — see the server DELETE which removes `time_logs WHERE task_id` first). Validation: add/update require a non-empty trimmed name and ≥1 selected type; `name.trim()` is what's sent. ### 6.5 Delete confirmation ```ts Alert.alert( 'Taak verwijderen', `"${task.name}" verwijderen? Alle tijdsregistraties voor deze taak worden ook verwijderd.`, [ { text: 'Annuleren', style: 'cancel' }, { text: 'Verwijderen', style: 'destructive', onPress: () => deleteTaskMutation.mutate(task.id) }, ] ); ``` - Title **`Taak verwijderen`** ("Delete task"). - Body **`"{name}" verwijderen? Alle tijdsregistraties voor deze taak worden ook verwijderd.`** ("Delete "{name}"? All time registrations for this task will also be deleted."). - Buttons **`Annuleren`** (cancel) and **`Verwijderen`** (delete, destructive). --- ## 7. Complete Dutch UI string inventory (verbatim → English) | Screen | Dutch string (verbatim) | English meaning | |---|---|---| | Tabs | `Stopwatch` | Stopwatch (timer tab) | | Tabs | `Geschiedenis` | History | | Tabs | `Instellingen` | Settings | | Stopwatch | `Type zool` | Insole type (section label) | | Stopwatch | `Kurk` | Cork | | Stopwatch | `Berk` | Birch | | Stopwatch | `3D` | 3D (3D-printed) | | Stopwatch | `Type handeling` | Handling/operation type (label + sheet title) | | Stopwatch | `Kies een handeling...` | Choose a handling… (dropdown placeholder) | | Stopwatch | `Kies een handeling` | Choose a handling (sheet subtitle) | | Stopwatch | `Aantal zolen` | Number of insoles (count label; default 2) | | Stopwatch | `Tik om te starten` | Tap to start | | Stopwatch | `Tik om te pauzeren` | Tap to pause | | Stopwatch | `Gepauzeerd — tik om te hervatten` | Paused — tap to resume | | Stopwatch | `Start Stopwatch` | Start stopwatch (button) | | Stopwatch | `Stop & Opslaan` | Stop & Save (button) | | Stopwatch | `Annuleren` | Cancel (discard button, idle) | | Stopwatch | `Nogmaals tikken ter bevestiging` | Tap again to confirm (discard armed) | | Stopwatch | `Geen handelingen beschikbaar voor {type} zolen. Voeg ze toe via Instellingen.` | No handlings available for {type} insoles. Add them via Settings. | | History | `Geschiedenis` | History (header) | | History | `Exporteer CSV` | Export CSV (button) | | History | `Nog geen opgeslagen sessies.` | No saved sessions yet. | | History | `inlegzool` / `inlegzolen` | insole / insoles (singular/plural in count pill) | | History | `Fout` | Error (export-failure alert title) | | History | `Kan de export-URL niet openen` | Cannot open the export URL | | Settings | `Instellingen` | Settings (header) | | Settings | `Beheer handelingen per zooltype` | Manage handlings per insole type | | Settings | `Nieuwe handeling toevoegen` | Add new handling | | Settings | `Naam van de stap, bijv. Leerrand` | Name of the step, e.g. Leather edge (placeholder) | | Settings | `Van toepassing op` | Applies to | | Settings | `Stap toevoegen` | Add step (button) | | Settings | `Huidige stappen ({n})` | Current steps (N) | | Settings | `Nog geen stappen. Voeg er een toe hierboven.` | No steps yet. Add one above. | | Settings | `Opslaan` | Save (edit confirm) | | Settings | `Annuleren` | Cancel (edit / alert) | | Settings | `Taak verwijderen` | Delete task (alert title) | | Settings | `"{name}" verwijderen? Alle tijdsregistraties voor deze taak worden ook verwijderd.` | Delete "{name}"? All time registrations for this task will also be deleted. | | Settings | `Verwijderen` | Delete (alert destructive button) | > Terminology nuance: the UI uses **handeling** (label) and **stap** (button/list) somewhat > interchangeably for the same entity — the production_task. "Handeling" ≈ operation/handling, > "stap" ≈ step. The data model calls it `production_tasks`. The count noun shown in History is > **inlegzool / inlegzolen** ("insole/insoles"), distinct from the label "zolen" in > `Aantal zolen`. --- ## 8. Backend contract the worker app depends on The mobile client calls `fetch('/api/...')` (relative); `__create/fetch.ts` rewrites first-party URLs onto `EXPO_PUBLIC_BASE_URL`, injects Create project/host headers, and attaches a `Bearer ` from SecureStore. The greenfield backend (apps/api, Hono) must expose equivalent endpoints. Contract as exercised by the screens (and matched by the legacy `apps/web` routes): | Endpoint | Method | Request body | Response | Used by | |---|---|---|---|---| | `/api/tasks` | GET | — | `production_tasks[]` (`id`, `name`, `insole_types: string[]`), ordered `name ASC` | Stopwatch, Settings | | `/api/tasks` | POST | `{ name, insole_types[] }` | created task | Settings (add) | | `/api/tasks/:id` | PUT | `{ name, insole_types[] }` | updated task (404 if missing) | Settings (edit) | | `/api/tasks/:id` | DELETE | — | `{ success: true }` — **also deletes that task's `time_logs`** | Settings (delete) | | `/api/logs` | GET | — | `[{ id, task_name, task_id, start_time, end_time, duration_seconds, pair_count, insole_type, notes, created_at }]`, ordered `start_time DESC` | History | | `/api/logs` | POST | `{ task_id, start_time, end_time, duration_seconds, pair_count, insole_type }` | created log | Stopwatch (Stop & Save) | | `/api/export` | GET | — | CSV attachment (see §5.3) | History (CSV export) | Server-side rules observed in the legacy `apps/web` routes (worth preserving): - `insole_types` defaults to `['Kurk','Berk','3D']` when missing/empty on POST/PUT. - `POST /api/logs` requires `task_id`, `start_time`, `end_time`, and a defined `duration_seconds`; defaults `pair_count → 2`, `insole_type → 'Kurk'`, `notes → null`. - `task_name` in log responses comes from `JOIN production_tasks pt ON tl.task_id = pt.id`. - History orders `start_time DESC` (newest first); CSV export orders `start_time ASC`. Data model (two tables): - **`production_tasks`** — `id`, `name`, `insole_types text[]` (subset of `Kurk`/`Berk`/`3D`). - **`time_logs`** — `id`, `task_id` (FK), `start_time`, `end_time`, `duration_seconds`, `pair_count`, `insole_type`, `notes`, `created_at`. --- ## 9. Behavioural details easy to miss in the rebuild - **Landing tab is the Stopwatch** (`(tabs)/index`), titled `Stopwatch`. - **A handling must be selected to start** (`canStart = !!activeTaskId`); zooltype + count alone are not enough. - **Changing the zooltype clears the selected handling** (so you can never run a handling that doesn't apply to the chosen type). The handling picker only lists handlings whose `insole_types` include the current zooltype; types with no matching handling show the "Geen handelingen beschikbaar…" empty state pointing the worker to Instellingen. - **All three selectors lock while running** (zool buttons, handling dropdown, count stepper + field). They unlock on stop/discard. - **Count default is 2**, minimum 1, free-typed via the text field but only committed when a valid positive integer is typed; sent as `pair_count`. - **Discard is a deliberate two-tap action** with a 3-second arm window; a single tap never discards. `Stop & Opslaan` always saves immediately (no confirm). - **Time is counted by 1-second interval ticks**, not wall-clock difference; `start_time`/ `end_time` are real timestamps but `duration_seconds` is the tick count. Consider wall-clock delta in the rebuild to survive backgrounding. - **After a save, selections persist** (only the timer resets) — convenient for repeated identical sessions. - **Deleting a handling cascades to its logs**, and the client invalidates the History query so it refreshes. - **Font-load resilience**: render even if Inter fails to load (Android freeze workaround). - **Bottom sheet uses `Pressable` backdrop** (documented Android fix) and an `Animated` translateY at 75% screen height. ```