docs: extract port-worthy reference from legacy code before full cleanup
This commit is contained in:
551
docs/reference/legacy-mobile-app.md
Normal file
551
docs/reference/legacy-mobile-app.md
Normal file
@@ -0,0 +1,551 @@
|
||||
# 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: 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<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.
|
||||
```
|
||||
Reference in New Issue
Block a user