Files
solelog/docs/reference/legacy-mobile-app.md

552 lines
29 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 `<AuthModal />` 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: 1216`, 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<number | null>(null);
const [insoleType, setInsoleType] = useState<InsoleType>('Kurk');
const [isRunning, setIsRunning] = useState(false);
const [isPaused, setIsPaused] = useState(false);
const [startTime, setStartTime] = useState<Date | null>(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 <jwt>` 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.
```