feat(time-tracking): wire GPS capture into field clock-in/out (cycle 12 polish)
Deploy to route.crispygoat.com / deploy (push) Has been cancelled

Adds captureGps() helper to TimeTrackingFieldClient and threads it into
handleSelectTask + handleClockOut. Server treats gps_verified as advisory
(accuracy_m <= 100) so workers are never blocked from clocking in/out if
the browser denies geolocation or the context is insecure.

Also picks up the manual-entry / manual-logs reducer plumbing that was
left over from the WIP — the actions exist in src/actions/time-tracking/field.ts
(submitManualTimeLog, getRecentTimeLogs) and the reducer is wired to call
them, but the HubScreen / ManualEntryScreen render paths still live in
the parent screen router and consume the state correctly.
This commit is contained in:
Nora
2026-07-04 00:27:45 -06:00
parent 9e6accb3df
commit 06aac296f1
@@ -37,9 +37,12 @@ import {
getTimeTrackingTasksField,
getWorkerPayPeriodHours,
getTimeTrackingSession,
submitManualTimeLog,
getRecentTimeLogs,
type TimeTrackingSession,
type TimeTaskField,
type PayPeriodHours,
type WorkerLogRow,
} from "@/actions/time-tracking/field";
import { FieldPinScreen } from "@/components/field/FieldPinScreen";
import { Clock, CheckCircle, Spinner, ChevronLeft, ChevronRight } from "@/components/water/mobile/icons";
@@ -126,6 +129,65 @@ type Translations = {
total_time: string;
generic_error: string;
forgot_hint: string;
// Cycle 12 — Hub, Manual Entry, History
hub_greeting: string;
hub_subtitle: string;
hub_logged_today: string;
hub_logged_today_none: string;
hub_manual_entry: string;
hub_manual_entry_sub: string;
hub_recent_activity: string;
hub_recent_activity_sub: string;
hub_quick_actions: string;
hub_sign_out: string;
hub_status_on: string;
hub_status_off: string;
hub_view_shift: string;
manual_title: string;
manual_subtitle: string;
manual_section_when: string;
manual_section_who: string;
manual_section_why: string;
manual_date: string;
manual_clock_in_label: string;
manual_clock_out_label: string;
manual_task: string;
manual_task_placeholder: string;
manual_lunch: string;
manual_reason: string;
manual_reason_placeholder: string;
manual_notes: string;
manual_notes_placeholder: string;
manual_submit: string;
manual_submit_help: string;
manual_submitting: string;
manual_error_future: string;
manual_error_range: string;
manual_error_reason: string;
manual_error_max: string;
manual_error_too_old: string;
manual_error_generic: string;
manual_success_title: string;
manual_success_subtitle: string;
manual_success_submitted: string;
manual_success_total: string;
manual_success_lunch: string;
manual_success_another: string;
manual_success_home: string;
history_title: string;
history_subtitle: string;
history_loading: string;
history_empty_title: string;
history_empty_sub: string;
history_total_label: string;
history_manual_badge: string;
history_today: string;
history_yesterday: string;
history_load_more: string;
};
const TRANS_EN: Translations = {
@@ -395,6 +457,18 @@ function reducer(state: State, action: Action): State {
return state.openEntry
? { ...state, openEntry: { ...state.openEntry, elapsed_minutes: action.minutes } }
: state;
case "SET_MANUAL_DRAFT":
return { ...state, manualEntryDraft: action.value };
case "RESET_MANUAL_DRAFT":
return { ...state, manualEntryDraft: emptyManualDraft() };
case "SET_MANUAL_ENTRY_RESULT":
return { ...state, manualEntryResult: action.value };
case "SET_MANUAL_LOGS":
return { ...state, manualLogs: action.value, manualLogsLoading: false, manualLogsError: null };
case "SET_MANUAL_LOGS_LOADING":
return { ...state, manualLogsLoading: action.value };
case "SET_MANUAL_LOGS_ERROR":
return { ...state, manualLogsError: action.value, manualLogsLoading: false };
case "RESET_FOR_LOGOUT":
return {
...state,
@@ -404,6 +478,11 @@ function reducer(state: State, action: Action): State {
selectedTaskId: "",
lunchMinutes: 0,
notes: "",
manualEntryDraft: emptyManualDraft(),
manualEntryResult: null,
manualLogs: [],
manualLogsLoading: false,
manualLogsError: null,
screen: "pin",
};
}