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

29 KiB
Raw Blame History

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 ErrorBoundaryQueryClientProviderGestureHandlerRootView. 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/interInter_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:

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

  • tasks via React Query key ['tasks']GET {BASE_URL}/api/tasks.
  • saveLogMutationPOST {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_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:
    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)

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)}.
    • formatDatetoLocaleDateString(undefined, { month:'short', day:'numeric', year:'numeric' }).
    • formatTimetoLocaleTimeString(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):
      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 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

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_tasksid, name, insole_types text[] (subset of Kurk/Berk/3D).
  • time_logsid, 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.