# Worker shifts, clock-in/out, breaks, and location — plan

Branch: `worker-shifts` (off `master`, after `worker-checklists` was merged).

## What was decided

- **Location**: track a worker's location while they are clocked in on a
  shift. On clock-in, and periodically during the shift, capture a GPS
  point. If a worker moves more than ~100m from the branch while clocked
  in, notify the owner. Show the worker's current location to the owner
  while they're on shift (not a full continuous trail/map history in the
  first pass — a "here's where they are right now" point).
- **Shift scheduling**: the owner defines shifts (start time, end time,
  which days, which branch) and assigns specific workers to specific
  shifts/days. Workers see their own upcoming shifts in the app.
- **Clock-in/out**: not shift-gated for the clock-in action itself, but
  shift-gated for *checklist visibility* — a worker cannot see today's
  checklists until they've clocked in, and loses sight of them again once
  they clock out. (So in effect, shifts + clock-in state control what a
  worker's Home tab shows.)
- **Breaks**: while clocked in, a worker can tap "Go on break." The shift
  (or a default) has a break duration; a countdown starts, and presumably
  the owner/worker can see when the break should end / whether it's been
  overrun.
- **Editable history**: owner can review and correct clock-in/out times
  after the fact (people forget to clock out).
- **Team page integration**: for each currently-scheduled-or-clocked-in
  worker, the owner sees: clocked in or not, on break or not, roughly
  where they are / whether they're at the branch, and their progress on
  today's checklists.
- **Rollout**: one thin vertical slice first — the smallest end-to-end
  version working and tested — then layer on the rest.

## Decisions (resolved)

- **Auto clock-out**: a worker is automatically clocked out at their
  shift's scheduled end time. No branch-level "closing time" concept is
  needed — the shift's own end_time is the trigger.
- **Overtime**: if a worker is genuinely still working past shift end,
  they must explicitly press "Working overtime" (before auto-clockout
  would otherwise fire) and enter/confirm the extra hours. This keeps
  their attendance record open past the scheduled end instead of being
  silently auto-closed. Overtime hours are auto-approved (not held
  pending) but are clearly flagged as overtime everywhere they appear —
  on the owner's attendance/Team view now, and in reporting later. There
  is no cryptographic way to verify hours claimed from a phone; the
  flagging plus (later) location data during the claimed window are the
  realistic mitigations, not a hard block.
- **One shift per worker per day** for v1 — no overlapping/multiple
  shifts per day yet.
- **Break is a fixed clock-time window on the shift** (e.g. 13:00–14:00),
  not a duration and not per worker — every worker assigned to a given
  shift gets the same break window. (Originally built as a plain
  break-minutes duration; changed to a real window per user feedback,
  matching the pattern already used for a checklist's visibility window
  and a task's alarm time.)
- **Location** (unchanged from earlier): track while clocked in; capture
  a point on clock-in and periodically during the shift; alert the owner
  if a worker drifts >100m from the branch; owner sees a live "here they
  are now" point while a worker is on shift, not a full trail history —
  deferred to a follow-up pass (see below), not in the thin slice.

## Remaining open questions (not blocking the thin slice, revisit before those passes)

1. **Background location on iOS/Android** requires explicit permission
   flows (and on iOS, "Always" location access needs a real justification
   string and is a common App Store review sticking point). Foreground-only
   tracking (updates only while the app is open) is drastically simpler
   than true background tracking (works with the app closed/screen off,
   needs platform channels, Android battery-optimization exemptions, an
   iOS background modes entitlement). Decide when we get to the location
   pass, not now.
2. **Geofence check interval** (battery vs. responsiveness trade-off) —
   needs a concrete default (e.g. every 5 minutes while clocked in) when
   we build that pass.

## Proposed data model (sketch, not final — refine when implementing)

- `shifts` — id, branch_id, name, start_time, end_time, days_mask,
  break_start, break_end, created_by. (Recurring template, same days_mask
  pattern already used for checklists/tasks; break_start/break_end are a
  fixed clock-time window, both null together when there is no break.)
- `shift_assignments` — id, shift_id, user_id. (Who is on this shift.)
- `shift_instances` or computed on the fly — "today's occurrence" of a
  shift for a given date, derived from `shifts` + `days_mask`, same
  approach as checklists' daily reset (no physical row needed unless we
  need to attach state to a specific day's occurrence beyond
  clock-in/out, which we do — see below).
- `attendance` — id, shift_id (nullable, if clock-in isn't shift-gated),
  user_id, branch_id, clock_in_at, clock_out_at, clock_in_lat/lng,
  clock_in_within_range (bool), is_overtime (bool), overtime_minutes
  (nullable), auto_clocked_out (bool, so the owner/reporting can tell a
  system auto-clockout apart from a real one), edited_by/edited_at (for
  the owner correction feature).
- `attendance_breaks` — id, attendance_id, started_at, ended_at (nullable
  while ongoing), expected_minutes.
- `location_pings` — id, attendance_id, lat, lng, captured_at, distance_
  from_branch_meters. Periodic while clocked in; used for the "show
  current location" and "notify if >100m away" features.

## Progress

- **Backend done and curl-verified**: migration 008 (shifts,
  shift_assignments, attendance), shifts CRUD
  (backend/api/shifts/{index,update,delete}.php — index.php handles both
  GET list and POST create), attendance endpoints
  (backend/api/attendance/{today,clock_in,clock_out,status}.php), and the
  checklist-visibility gate wired into checklists/today.php (workers see no
  checklists until clocked in; owners/managers bypass, same as targeting).
  One shift per worker per day is enforced via find_shift_day_conflict() in
  helpers.php. Found and fixed a real bug during testing: a bind_param type
  string in shifts/index.php was missing one 's' for the two TIME columns,
  which silently corrupted end_time on insert.
- **Frontend done and verified.** flutter analyze clean. New screens:
  ShiftsScreen + CreateShiftScreen (dual create/edit, reusing
  DaysOfWeekPicker/TimeFieldRow from the checklist-alarms work),
  EmployeeOverviewScreen (shift status + today's completed tasks for one
  worker), ShiftGatedChecklists (worker Home's shift card + clock in/out,
  hides TodayChecklistsView until clocked in). Team tab: added a Shifts
  entry point alongside Groups, a clock-in status pill per member ("Clocked
  in · 09:14" / "Not clocked in yet" / "Clocked out · 17:02"), and split
  each member row's action into two explicit buttons -- Contact (existing
  PersonProfileScreen) and About (new EmployeeOverviewScreen) -- per the
  user's request, rather than one implicit tap. Added `completed_by_id` to
  checklists/today.php's task JSON (worker_id was already selected, just
  not exposed) so EmployeeOverviewScreen can filter to one person's
  completions by id instead of matching on name strings.
  Full loop re-verified via curl after the l10n pass: create shift → clock
  in → complete a task → owner sees correct status + completed_by_id →
  clock out → test data cleaned up, only the two real checklists remain.
- **Location tracking, Stage 1 (foreground-only) done and curl-verified.**
  Decided with the user: true background tracking is the long-term goal
  (not foreground-only forever), and the map is an embedded live map (not
  just an address/Maps-link), using flutter_map + OpenStreetMap tiles
  rather than Google Maps, since it needs no API key/billing account.
  Given the size of true background tracking (iOS "Always" location +
  App Store review risk, Android foreground service + battery-optimization
  exemption -- neither verifiable without a real device), this stage
  ships the foreground half first: migration 011 (location_pings, one row
  per ping tied to an attendance id), attendance/ping.php (worker sends
  lat/lng, no-ops if not clocked in) and attendance/location.php (owner
  reads the latest ping for a worker's *currently open* attendance row --
  location disappears the moment they clock out). Worker side
  (ShiftGatedChecklists): requests "when in use" location permission and
  pings every 2 minutes via a Timer while clocked in and the app is open;
  starts on clock-in, stops on clock-out, also catches "already clocked in
  when the screen loads" (app reopened mid-shift). A denied/unavailable
  permission silently skips pinging rather than blocking clock-in or
  nagging the worker. Owner side (EmployeeOverviewScreen): a new "Current
  location" section, shown only while the worker is clocked in, with an
  embedded FlutterMap + marker that self-refreshes every 30 seconds
  (polling, no push channel yet) and a "no location yet" state for the gap
  between clocking in and the first ping landing. Added the
  NSLocationWhenInUseUsageDescription / ACCESS_FINE_LOCATION permission
  entries to both platforms. Stage 2 (true background tracking + the
  geofence-exit alert to the owner) is the next piece of this feature --
  needs the iOS/Android permission flow decisions (open question #1 above)
  made concrete and a real device to verify against, so it should not be
  attempted blind the way Stage 1 could be curl-tested.
- **Break changed from a duration to a fixed window** (migration 009):
  break_minutes → break_start/break_end (nullable together). Owner picks
  two times (e.g. 13:00–14:00) via the same TimeFieldRow used for
  checklist schedules, instead of typing a minute count. Backend validates
  both bounds are given together, break_start < break_end, and the break
  falls within the shift's own hours -- re-checked on update whenever
  either the break or the shift's hours change, so shrinking a shift's
  hours out from under an existing break is rejected rather than silently
  producing an inconsistent shift. Curl-verified: creation, the
  outside-shift-hours rejection, the one-bound-only rejection, the
  shrink-hours-orphans-break rejection, and clearing a break via explicit
  nulls -- all correct.

## Thin vertical slice (build this first)

1. Owner can create a shift (name, start/end time, days, branch) — no
   assignment yet beyond picking who's on it at creation, reusing the
   existing days-of-week picker and time-field-row widgets already built
   for checklist schedules.
2. Owner assigns workers to a shift.
3. Worker sees "your shift today" (if any) on their Home tab.
4. Worker can clock in / clock out (foreground-only location capture at
   clock-in only, no continuous tracking yet, no break timer yet).
5. Checklists are hidden from a worker's Home tab until clocked in, and
   hidden again after clocking out.
6. Owner's Team tab shows, per worker on shift today: clocked in/out
   status, clock-in time, and whether the clock-in was within range of the
   branch.
7. Basic backend + frontend, tested via curl + flutter analyze, same
   workflow as the checklist feature.

Auto clock-out at shift end, the "Working overtime" flow, and break
tracking are NOT in this first slice — see follow-up passes below. The
slice establishes shifts + assignment + manual clock-in/out +
checklist-visibility gating first, since everything else builds on top of
that.

## Follow-up passes (after the slice works)

1. **Auto clock-out at shift end** + the "Working overtime" override flow
   (worker presses it before auto-clockout fires, enters/confirms extra
   hours; record stays open past scheduled end; overtime is auto-approved
   but flagged everywhere in the UI, and later in reporting).
2. **Break tracking** with a countdown timer, using the shift's
   break_minutes.
3. **Editable attendance history** for the owner (correct clock in/out
   times, and by extension correct/dispute overtime claims, after the
   fact).
4. **Location**: periodic pings while clocked in, geofence-exit
   notification to the owner (>100m from branch), and a live "current
   location" shown to the owner while a worker is on shift. Resolve the
   background-vs-foreground tracking strategy (open question #1 above)
   as part of this pass.
