192 lines
8.7 KiB
Markdown
192 lines
8.7 KiB
Markdown
# KROW Worker
|
|
|
|
A Flutter app for shift workers: find hospitality shifts, clock in on site, get
|
|
timecards filed automatically, and keep compliance documents in order.
|
|
|
|
Built from the workflow in the KROW worker-app prototype — the HTML was used as
|
|
the reference for screens and flow, not as a pixel spec.
|
|
|
|
## Running it
|
|
|
|
```bash
|
|
flutter pub get
|
|
flutter run
|
|
```
|
|
|
|
Targets Android, iOS and web. To try it in a browser:
|
|
|
|
```bash
|
|
flutter run -d chrome
|
|
```
|
|
|
|
**Demo sign-in:** any 10-digit mobile number, then the code `123456`.
|
|
|
|
## What works
|
|
|
|
Everything below is wired to real state, not static screens. Shifts, timecards
|
|
and compliance live in memory for the session; onboarding, sign-in,
|
|
availability, skills and preferences persist via `shared_preferences`.
|
|
|
|
**Getting in** — splash routes by how far you got last time · 3-screen
|
|
onboarding with skip · phone entry with validation · OTP with a resend
|
|
countdown, a wrong-code path and a 3-try budget · 3-step permission run whose
|
|
grants are recorded and reflected in Settings.
|
|
|
|
**Today** — greeting, period earnings and hours logged, a compliance nudge while
|
|
anything is outstanding, your next shift, quick tiles and matched open shifts.
|
|
|
|
**Shifts** — Booked / Open / History. The week strip filters booked shifts by
|
|
day and dots the days that have work. Open supports search and filters, and
|
|
Apply books the shift immediately. History carries completed, cancelled and
|
|
no-show shifts with a stat rail.
|
|
|
|
**Shift detail** — full schedule and pay breakdown, plus a requirements check
|
|
that compares what the business asks for against what you actually have on
|
|
file. Actions change with status: apply, cancel (with confirmation), or jump to
|
|
the clock.
|
|
|
|
**Clock** — the button unlocks 30 minutes before the shift, and only once you
|
|
are at the venue. Clocking in starts a live stopwatch against a progress ring;
|
|
clocking out completes the shift, deducts the unpaid break and files a timecard.
|
|
|
|
**Timecards** — every filed card with its scheduled vs clocked window, variance
|
|
in minutes, and pay. Review one to approve it or raise a dispute with a note.
|
|
The tab badge counts what is still waiting on you.
|
|
|
|
**Pay** — gross pre-tax earnings across week / month / year / all, a trend chart
|
|
split paid vs pending, per-payment detail, and a semi-monthly payroll schedule
|
|
that computes the current and next period from today's date.
|
|
|
|
**Profile & compliance** — live stats and a trust index derived from the shifts
|
|
you have actually worked. The compliance hub tracks documents, certificates, tax
|
|
forms and attire; uploading sends an item back for review, removing it drops it
|
|
to missing, and expiry dates are picked from a calendar. Plus emergency
|
|
contacts, experience and skills, availability, benefits, direct deposit,
|
|
settings, support, FAQs and privacy.
|
|
|
|
## Motion
|
|
|
|
Motion is used to explain what changed, not to decorate. The rules:
|
|
|
|
- **Going deeper pushes.** Detail screens slide in from the right while the
|
|
screen behind eases back and dims, so the stack reads as a stack. Going
|
|
sideways — sign-up steps, replacing a screen — cross-fades instead.
|
|
- **Selection travels.** The bottom nav indicator and the segmented controls
|
|
slide between positions and squash slightly at speed. Labels light up as the
|
|
indicator arrives over them, driven by the same animated value, so nothing
|
|
changes ahead of the thing causing it.
|
|
- **Numbers grow.** Progress bars and chart bars tween to new values, so an
|
|
uploaded document or a switched pay range is something you watch happen.
|
|
- **State changes animate together.** Availability blocks, week-strip days and
|
|
the clock dial move every part of themselves in one gesture rather than
|
|
flipping several properties at once.
|
|
- **Consequential actions are felt.** Clocking in and out give a medium impact;
|
|
selecting a tab, segment, or applying for a shift gives a selection click.
|
|
|
|
**Headers stay put.** On the five tabs the blue header is pinned: content
|
|
scrolls beneath it rather than pushing it off the top. A tall header held at
|
|
full size would cost a quarter of a phone screen for the whole session, so it
|
|
collapses as you scroll and cross-fades to a compact bar that keeps what still
|
|
matters — your name and the period figure on Today, the gross total on Pay.
|
|
|
|
Where a header holds a control, the collapsed state keeps it. Pay stops
|
|
shrinking while the range switcher is still on screen; collapsing that away
|
|
would mean scrolling back to the top just to change what you are looking at.
|
|
Shifts keeps its whole header fixed for the same reason — the Booked / Open /
|
|
History switch has to stay reachable.
|
|
|
|
Scrolling uses bouncing physics on every platform (`KrowScrollBehavior`). Under
|
|
a pinned header a rubber-band edge tells you that you have reached the end of
|
|
the content rather than the end of the screen; clamping just stops dead. The
|
|
overscroll glow is turned off, since the bounce already says it.
|
|
|
|
Two deliberate exceptions:
|
|
|
|
- **Tab content does not slide.** Switching bottom tabs fades and lifts the
|
|
content instead. An `AnimatedSwitcher` would hold two copies of the shell in
|
|
the tree, and the branch navigators are keyed by `GlobalKey`, so duplicating
|
|
them throws. `_TabTransition` animates one subtree in place, which keeps each
|
|
tab's scroll position.
|
|
- **No `Hero` between list and detail.** The tab branches all stay alive in an
|
|
`IndexedStack`, so the same shift can be on screen in two branches at once
|
|
and duplicate hero tags would throw.
|
|
|
|
## Layout
|
|
|
|
```
|
|
lib/
|
|
core/ theme, router, formatters
|
|
models/ shift, timecard, availability, compliance, worker
|
|
state/ Riverpod controllers + seed data
|
|
screens/ one file per area of the app
|
|
widgets/ shift card, week strip, shared pieces
|
|
```
|
|
|
|
## Packages
|
|
|
|
| Package | Why |
|
|
| --- | --- |
|
|
| `flutter_riverpod` | State: shifts, timecards, availability, compliance, profile, session |
|
|
| `go_router` | Routing, including the 5-tab shell where each tab keeps its own stack |
|
|
| `google_fonts` | Poppins, the brand typeface |
|
|
| `lucide_icons_flutter` | Icon set matching the prototype's line icons |
|
|
| `intl` | Currency, date and time formatting |
|
|
| `shared_preferences` | Persisting session, availability and preferences |
|
|
|
|
## Tests
|
|
|
|
```bash
|
|
flutter test
|
|
```
|
|
|
|
Covers pay calculation (break deduction, clocked vs scheduled hours, zero-pay
|
|
states), timecard variance, and the formatters — plus the custom nav and
|
|
segmented control: that the indicator actually travels rather than jumping,
|
|
that taps report the right index, and that each tab reaches the semantics tree
|
|
with its selected state (the dot alone is not readable).
|
|
|
|
## Device permissions
|
|
|
|
Location, notifications and camera are real requests through
|
|
`permission_handler`, wrapped in `lib/core/permissions.dart` so nothing else
|
|
imports the plugin. Two rules that layer enforces:
|
|
|
|
- **The OS is the source of truth.** The flags on `SessionState` are a cache for
|
|
the UI. A permission can be revoked in system settings while the app is
|
|
closed, so any screen that shows their state calls `refresh()` to re-read.
|
|
- **Platforms the plugin does not implement degrade to granted.** On web and
|
|
desktop it returns `true` rather than throwing — the browser prompts at the
|
|
point of use, so asking up front is neither possible nor useful.
|
|
|
|
There is no API to revoke a permission, and re-requesting a granted one is a
|
|
no-op, so switching one off in Settings opens the system settings page instead.
|
|
Same when a permission is permanently denied and the OS will no longer show its
|
|
dialog.
|
|
|
|
Native declarations live in `AndroidManifest.xml` and `ios/Runner/Info.plist`;
|
|
iOS rejects builds that request a permission without a usage string.
|
|
|
|
> `permission_handler_android` compiles against Android API 37, and its AAR
|
|
> metadata requires dependents to match, so `android/app/build.gradle.kts` pins
|
|
> `compileSdk = 37` rather than following `flutter.compileSdkVersion`. Building
|
|
> needs the `platforms;android-37` SDK package installed.
|
|
|
|
## Wiring it to a backend
|
|
|
|
The seams are deliberate. `lib/state/seed.dart` is the only source of demo data;
|
|
swap each controller's `build()` for an API call and the screens are unchanged.
|
|
|
|
What still stands in for something real:
|
|
|
|
- **Geofence** — `ClockScreen` has an "at the venue" switch in place of GPS.
|
|
Location permission is real, but nothing measures distance yet. Add
|
|
`geolocator` and a 150 m radius check against the venue.
|
|
- **PDF upload** — compliance items take a photo or a gallery image through
|
|
`image_picker`. Picking a document needs `file_picker`, so that option is not
|
|
offered rather than offered and faked.
|
|
- **Upload** — a picked file updates local state only; nothing is sent
|
|
anywhere yet.
|
|
- **Notifications** — permission is requested, but nothing is scheduled or
|
|
received. Needs a messaging service and a local-notifications plugin.
|
|
- **SMS verification** — the OTP is always `123456`.
|