29 KiB
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: aStackwithinitialRouteName="(tabs)",headerShown: false. Gated onuseAuth().initiate()+isReady(loads the persisted session from SecureStore before first render; returnsnulluntil ready, with a 10 s splash timeout fallback). Wraps everything inErrorBoundary→QueryClientProvider→GestureHandlerRootView. Renders<AuthModal />alongside theStack.- React Query defaults:
staleTime5 min,gcTime30 min,retry: 1,refetchOnWindowFocus: false.
- React Query defaults:
-
app/(tabs)/_layout.tsx: a bottomTabsnavigator withheaderShown: falseand three tabs, in this order:Order Route file Tab title (Dutch) Icon ( lucide-react-native)1 index.tsxStopwatch Timer2 history.tsxGeschiedenis History3 tasks.tsxInstellingen SettingsTab bar styling: white background, 1px top border
#E5E7EB,paddingTop: 4; active tint#2563EB, inactive tint#6B7280; labelfontSize: 12, fontWeight: '500'; iconssize={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) andInter_600SemiBold(semibold). Each screen guards onuseFonts: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 toundefinedwhenfontErroris 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:
#16A34Atext,#DCFCE7surface (Settings "Opslaan").
- Primary blue
-
Per-zooltype colour set (
tasks.tsx,TYPE_COLORS) — used for toggles and badges:Type bg border text Kurk#FEF9C3#FDE047#854D0EBerk#DCFCE7#86EFAC#1665343D#EDE9FE#C4B5FD#5B21B6 -
Shapes: heavy rounding — section cards
borderRadius: 12–16, pillsborderRadius: 999, primary buttonsborderRadius: 16, the stopwatch displayborderRadius: 24. -
Safe areas: each screen reads
useSafeAreaInsets()and appliespaddingTop: insets.top.
3. The three insole types (Type zool)
Defined identically in both the Stopwatch and Settings screens:
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
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
tasksvia React Query key['tasks']→GET {BASE_URL}/api/tasks.saveLogMutation→POST {BASE_URL}/api/logs; on success invalidates['logs'].
Derived values:
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_TYPESvalue (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 resetssetActiveTaskId(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 (activeTaskIdnull). TrailingChevronDownicon. - 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 numericTextInput(keyboardType="number-pad"), and a+button (width 64), inside one rounded bordered row. - Default value
2. −is disabled wheninsoleCount <= 1or running;+disabled when running; the field iseditable={!isRunning}.- Behaviour:
The text mirror lets the user type freely; the committed numeric
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)); };insoleCountonly updates for a valid positive integer. Minimum is 1. This value is sent aspair_counton 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)doeshrs/mins/secswithpadStart(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) readsTik 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) readsGepauzeerd — 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 (activeOpacity1).
- Not running, can start (a handling chosen): time greyed
4.7 Section 5 — Action buttons (Knoppen)
- When not running: a single full-width primary button with a
Playicon, labelStart Stopwatch. Enabled only whencanStart(a handling is selected); otherwise greyed#E5E7EBand disabled. - When running: two stacked buttons:
- Red button,
Squareicon, labelStop & 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.
- Idle label:
- Red button,
4.8 Bottom-sheet handling picker (Modal)
- Transparent
Modal,animationType="none",statusBarTranslucent. Backdrop is aPressable(rgba(0,0,0,0.45)) that closes the sheet (explicit comment: usesPressablenot nestedTouchableWithoutFeedbackas an Android fix). The sheet is anAnimated.Viewsliding viatranslateY: slideAnim(open → 0 over 300 ms; close →SHEET_HEIGHTover 250 ms). - A drag-handle bar at top.
- Header: title
Type handeling, subtitleKies een handeling("Choose a handling"). - List: the filtered tasks (only those whose
insole_typesincludes the currentinsoleType). Each row showstask.name; the selected row is highlighted blue with a trailingCheckicon. Tapping a row setsactiveTaskIdand 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): guardif (!activeTaskId) return;thenisRunning=true; isPaused=false; startTime=new Date(). -
Tick:
useEffecton[isRunning, isPaused]— when running and not paused, sets a 1 ssetIntervalthat doessetElapsedTime(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): guardif (!activeTaskId || !startTime) return;thenisRunning=false; isPaused=false, computeendTime=new Date(), firesaveLogMutation(see 4.10), then resetstartTime=null; elapsedTime=0; discardPending=falseand clear the discard timer. -
Discard / double-press cancel (
handleDiscard):- First tap:
discardPending=trueand start a 3 s timer that re-clearsdiscardPending(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
Annulerenonce to arm (button changes toNogmaals tikken ter bevestiging), tap again to actually discard. - First tap:
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)
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 aDownloadicon and labelExporteer 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 fromproduction_tasks.nameserver-side). - Date/time line: a
Calendaricon +{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;undefinedmeans 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 aLayersicon 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
Clockicon showingformatDuration(log.duration_seconds):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
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 CSVopensGET {BASE_URL}/api/exportin the OS browser/handler (the device downloads the CSV; the app does not parse it). -
Failure alert: title
Fout("Error"), bodyKan 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-BEformatting, quoted,\njoined, ordered bystart_time ASC):ID, Task, Insole Type, No. of Insoles, Date, Total Duration, Start Time, End TimeInsole Typedefaults toKurkif null;No. of Insolesdefaults to2if null.Date=nl-BEdd-mm-yyyy;Start/End Time=nl-BEHH:MM:SS;Total Duration=HH:MM:SSderived fromduration_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 threeTypeTogglepills (Kurk/Berk/3D), each coloured perTYPE_COLORS, showing aCheckwhen selected. Default selection for a new handling is all three types (['Kurk', 'Berk', '3D']). - Add button with a
Plusicon, labelStap toevoegen("Add step"). Disabled (greyed) unless the name is non-empty (trimmed) and at least one type is selected; shows anActivityIndicatorwhile 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.nameon the left; on the right two square icon buttons:- blue
Pencilbutton → enters edit mode for that row. - red
Trash2button → triggers delete (see 6.5).
- blue
- Below: a row of coloured
TypeBadgepills, one per type intask.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 oplabel + threeTypeTogglepills (pre-filled from the task's types; ifinsole_typesisn't an array it defaults to all three); - two buttons: green
Opslaan("Save",Checkicon) and greyAnnuleren("Cancel",Xicon). Save is disabled while pending or if no types are selected.
- an auto-focused name
6.4 Mutations / API
All keyed off React Query ['tasks']; success invalidates ['tasks'].
- Add (
addTaskMutation) →POST {BASE_URL}/api/tasksbody{ 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 removestime_logs WHERE task_idfirst).
Validation: add/update require a non-empty trimmed name and ≥1 selected type;
name.trim() is what's sent.
6.5 Delete confirmation
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) andVerwijderen(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" inAantal 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_typesdefaults to['Kurk','Berk','3D']when missing/empty on POST/PUT.POST /api/logsrequirestask_id,start_time,end_time, and a definedduration_seconds; defaultspair_count → 2,insole_type → 'Kurk',notes → null.task_namein log responses comes fromJOIN production_tasks pt ON tl.task_id = pt.id.- History orders
start_time DESC(newest first); CSV export ordersstart_time ASC.
Data model (two tables):
production_tasks—id,name,insole_types text[](subset ofKurk/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), titledStopwatch. - 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_typesinclude 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 & Opslaanalways saves immediately (no confirm). - Time is counted by 1-second interval ticks, not wall-clock difference;
start_time/end_timeare real timestamps butduration_secondsis 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
Pressablebackdrop (documented Android fix) and anAnimatedtranslateY at 75% screen height.