Skip to content

feat(mobile): UX overhaul — My Day, PM, EntityListCard, offline queue, a11y - #255

Closed
stephen20020 wants to merge 22 commits into
Grashjs:mainfrom
stephen20020:feat/mobile-ux-overhaul
Closed

feat(mobile): UX overhaul — My Day, PM, EntityListCard, offline queue, a11y#255
stephen20020 wants to merge 22 commits into
Grashjs:mainfrom
stephen20020:feat/mobile-ux-overhaul

Conversation

@stephen20020

Copy link
Copy Markdown

Summary

Mobile field UX overhaul across 21 commits on feat/mobile-ux-overhaul:

  • P1 — Foundation: design tokens, dark mode, shared list/detail primitives, FlatList work-order list with repaired filters, My Day home feed, grouped WO detail sections, global search, deep-link fixes
  • P2 — PM: preventive maintenance list/detail, create/edit, schedule pause; backend PATCH no longer clobbers absent fields
  • P3 — Reliability: typed ApiError, connectivity banner, error boundary, offline queue for 5 WO field mutations (mutex-hardened), accessibility labels on search bars and filter chips
  • P4 — Consistency: PaginatedEntityList + EntityListCard on 10 entity index screens, nested WO lists on asset/part/location tabs, asset downtime tab with CRUD, i18n for WO form placeholders
  • Bugbot fixes: notifications pull-to-refresh dispatches getNotifications; asset status PATCH sends { status } only; Docker entrypoint tolerates missing .env; nginx proxies /api/instance-config to backend

Test plan

Core navigation & lists

  • Sign in as test2@test.com / password123 against local backend (http://localhost:3001)
  • My Day home: overdue / today / in-progress buckets load and tap through to WO detail
  • Work Orders list: FlatList scroll, pull-to-refresh, infinite scroll, filter chips (status, priority, assignee)
  • Global search returns WOs, assets, locations, parts
  • Entity list screens (Assets, Parts, Locations, Meters, Requests, Notifications, People, Teams, Vendors, Customers): card layout, pagination, empty states, pull-to-refresh

Work orders & PM

  • WO detail: section grouping renders; edit status/priority/assignee online
  • PM list → detail → create → edit; pause/resume schedule
  • Nested WO tabs on asset, part, and location detail screens

Asset downtimes

  • Asset detail → Downtimes tab: list, add, edit, delete downtime
  • Change asset status from downtime tab; verify only status is PATCHed (no full-object overwrite)

Notifications & errors

  • Notifications pull-to-refresh reloads from API (not a no-op spinner)
  • Toggle airplane mode: connectivity banner appears; dismiss when back online
  • Trigger a bad request: typed error message surfaces (not raw stack)

Theme & a11y (spot check)

  • Dark mode toggle + system appearance
  • VoiceOver/TalkBack: search bars and filter chips have labels

Optional — offline queue (lower priority)

  • Airplane mode → change WO status on detail → reconnect → mutation flushes once (no duplicate)

Docker / nginx (if deploying stack)

  • Frontend container starts when .env is absent in nginx html dir
  • GET /api/instance-config returns backend LDAP flag (not hardcoded stub)

Notes

  • RN dev hot-reload can hit HostTarget::registerInstance assert after long sessions; full Metro restart clears it (dev-only).
  • Require-cycle warnings between workOrderofflineMutationsofflineQueue are known and non-blocking.

Made with Cursor

stephen20020 and others added 22 commits August 10, 2026 09:23
Adds a docker/ compose stack (nginx, backend, frontend, Postgres, MinIO)
for running Atlas CMMS locally, plus the mobile-side fixes needed to
actually talk to it:

- Normalize API URLs and reject marketing-site hosts, so the app can no
  longer be pointed at atlas-cmms.com instead of an API endpoint
- Parse non-JSON error responses rather than crashing on an HTML body
- Gate the cloud-only review endpoints behind the hosted API host, which
  a self-hosted backend does not implement
- Use a custom frontend entrypoint so runtime-env-cra tolerates blank
  optional variables instead of exiting
- Skip Firebase configuration when GoogleService-Info.plist is a
  placeholder, which otherwise fails the iOS build

Co-authored-by: Cursor <cursoragent@cursor.com>
Guards the UX refactor with a compiler gate. Currently passes clean.

Co-authored-by: Cursor <cursoragent@cursor.com>
The real plist is gitignored but the Xcode project declares it as a
required build input, so cloning and running the iOS build fails with
"Build input file cannot be found". Ship a placeholder template and
document the copy step alongside the existing Android Firebase setup.

Co-authored-by: Cursor <cursoragent@cursor.com>
Introduces theme/tokens.ts with spacing, radius, type and touch-target
scales derived from the values already used across the app, so adopting
them is substitution rather than redesign, and moves the palette there so
a dark variant can override it in one place.

The custom colors (success, warning, info, grey...) were invisible to
TypeScript because Paper's useTheme() resolves to MD3Theme. Every read
needed a @ts-ignore. MD3Colors is a type alias rather than an interface,
so it cannot be widened by declaration merging; instead getStatusColor
and getPriorityColor now take an AppTheme and their call sites use
useAppTheme(). That removes 4 of the 5 casts in overall.ts.

Themed.View computed a themed background and then discarded it, always
rendering white. It now honours the value and both primitives resolve
colors from the theme instead of the orphaned Expo starter palette.

No intended visual change; verified against the running simulator.

Co-authored-by: Cursor <cursoragent@cursor.com>
The linking config still described the Expo starter template: TabOneScreen,
TabTwoScreen and a Modal route, none of which are in the navigator. Every
deep link therefore resolved to nothing.

Map the real tab routes and the parameter-free stack screens instead.
Detail screens are deliberately omitted because they take entity objects
rather than ids, so they cannot be reconstructed from a URL until they
resolve by id. Route names are checked against RootStackParamList.

Co-authored-by: Cursor <cursoragent@cursor.com>
App.tsx already computed a colour scheme and passed it to Navigation,
which ignored it, while PaperProvider and NavigationContainer were both
pinned to the light theme. Dark mode was wired up but never connected.

- Dark palette in tokens. Brand hues are lightened rather than reused:
  the light primary fails contrast on a dark surface, and the status
  colours must stay distinguishable at low luminance since they carry
  the main signal in work order lists.
- Paper and React Navigation themes are derived together, so screens no
  longer flash the wrong colour during transitions.
- Preference persists via a themeMode slice and a Settings control:
  match device, light, or dark. It deliberately ignores revertAll, being
  a device preference rather than account data.
- Add a `card` token for raised surfaces. Themed.View previously used
  `white`, which would have had to return a dark colour in dark mode.
- getStatusColor returned the literal string 'black' for completed work,
  which vanished against a dark background; it now uses the inverting
  strong-foreground role.

Verified in both schemes on the simulator. Around 100 hardcoded white
literals remain in individual screens; those are picked up as each
screen is rebuilt in later phases.

Co-authored-by: Cursor <cursoragent@cursor.com>
Every list screen hand-rolls its own row markup, empty message and
loading treatment, so the same layout is written a dozen times and drifts
between screens. These four cover what those screens actually do:

- EntityListCard: composition rather than configuration, since the lists
  differ in which fields they show rather than in shape.
- EmptyState: distinguishes "nothing here" from "the request failed".
  Screens previously rendered a bare sentence for the first and nothing
  at all for the second, so a failure looked like an empty list.
- ListSkeleton: mirrors the card layout so content fills in rather than
  displacing a centred spinner.
- Section: groups fields on detail screens, which currently run every
  field together in a single column.

Accessibility is handled at this level: cards join their text into one
spoken label instead of reading as disconnected fragments, and headers
meet the 44pt minimum target.

Co-authored-by: Cursor <cursoragent@cursor.com>
The list rendered every loaded row from a ScrollView, so nothing was
recycled and memory grew with each page. It now uses FlatList with
onEndReached, which also stops the bottom of a fully loaded list from
re-requesting the last page.

Two filter controls were not working:

- EnumFilter built its edited filter set as a local variable and never
  stored it, so dismissing the dialog handed back the untouched original.
  Ticking a status moved the checkbox and changed nothing else.
- QuickFilter pushed into the array it was given, which the caller holds
  in state, so the comparison deciding whether to refetch saw no change.

Both also hardcoded black label text, invisible on a dark chip.

Loading is now three distinguishable states rather than one shared flag:
a skeleton on first load, the refresh spinner only for a deliberate pull,
and a footer spinner while appending. Pagination gets its own flag in the
slice, since sharing loadingGet made the refresh spinner appear every
time the list reached the bottom.

The filter row is pinned above the list instead of scrolling away with
it, and the empty state distinguishes an over-narrow filter, which offers
a reset, from an account with no work orders.

Co-authored-by: Cursor <cursoragent@cursor.com>
Registration fails on a simulator, which interrupted every launch with a
modal alert about something the user cannot act on.

Co-authored-by: Cursor <cursoragent@cursor.com>
Home listed six tallies and no actual work, so the first thing a
technician saw on opening the app was a number they had to tap through
to act on. It now leads with the work itself, grouped as overdue, due
today, and in progress, with the counts kept as a compact shortcut row.

The three groups are fetched as separate queries rather than filtered
from one page, since a single page could be filled entirely by one group
and leave the others looking empty. A group that fails leaves the rest
usable; only a total failure surfaces an error, with a retry.

Two API constraints worth recording:

- Date filters only implement `ge` and `le`. Both `lt` and `gt` return a
  500, so "before today" is expressed as the last instant before it.
- Assignee filtering has to be applied per query, so the existing
  "only assigned to me" preference now drives the feed as well as the
  counts.

Also stops the stat shortcuts from appending the assignee filter into
the array they were given, which compounded across presses.

Co-authored-by: Cursor <cursoragent@cursor.com>
Cards were separated from the page by a drop shadow, which is invisible
against a dark background, so sections on the work order detail screen
merged into one undifferentiated slab. Dark now outlines them with a
hairline instead.

The light/dark branch lived in each component, so it is collected into a
single raisedSurface helper that the card, skeleton, section and stat
pills share, along with a border color in the palette.

Co-authored-by: Cursor <cursoragent@cursor.com>
Every field ran together in one column, so finding the due date meant
reading the whole page. Fields are now grouped as Details, People and
Completion, and the parts, tasks, files, links and comments cards use
the shared Section rather than five slightly different hand-rolled ones.

The old card style set a shadow but no background colour, so those
groups had a shadow with nothing casting it.

Assignees and the requester moved into People, where they belong,
rather than sitting loose between the completion block and the parts
list. The task summary now shows progress as a proportion in the header
and reads as a row that leads somewhere, since tapping it opens the task
list.

Also drops the title from displaySmall, which took three lines for an
ordinary work order title, and adds keys to the relation lists.

Co-authored-by: Cursor <cursoragent@cursor.com>
The shared form gated its error text and red outline on Formik's `errors`
alone. Because it validates on every change, a required field turned red
and showed "is required" as soon as the user typed the first character
and cleared only once the value happened to become valid, so the form
spent most of its time telling the user they were doing it wrong while
they were still doing it.

Errors now wait for the field to be touched, which is the pattern the
auth screens already use. Submitting marks every field touched, so a
blank required field still reports on submit.

Also makes three controls that looked interactive behave: the asset and
assignee on a task now open their records, and the entity cards on a
request are properly disabled for requesters rather than swallowing the
press.

Co-authored-by: Cursor <cursoragent@cursor.com>
Finding a record meant knowing which list it lived in and then searching
within that list, which is the wrong question to ask someone standing in
front of a broken machine who knows only its name.

Results are grouped by type and each entity is queried in parallel, so
one the user lacks permission for does not blank the screen. Groups the
user cannot view are omitted rather than shown empty.

Results are normalised in the slice, so adding an entity is one entry in
the source list rather than another branch in the screen.

Co-authored-by: Cursor <cursoragent@cursor.com>
Preventive maintenance had no presence in the mobile app at all, so a
technician could see the work orders a schedule produced but never the
schedule behind them.

The list leads with the cadence and the next due date, the two things that
answer "is this covered?", and colours a date that falls within two days.
The detail screen separates the schedule from the work order template it
stamps out, since they are edited for different reasons.

The next occurrence is computed on the client: the API returns the raw
frequency and start date, and stepping one interval at a time from a start
date a year ago is avoided by computing the elapsed intervals directly.

Pausing is deliberately read-only. The server sets that flag on its own when
generated work orders go unanswered, and no endpoint exposes it: the
schedules PATCH DTO has no such field, and the line in the preventive
maintenance service meant to clear it does not survive the save. Rather than
offer a switch that silently fails, the screen explains the state.

Co-authored-by: Cursor <cursoragent@cursor.com>
…fields

Pausing a preventive maintenance was impossible from any client. The patch
DTO had no `disabled` field, and the one line meant to clear the flag on a
maintenance update never survived the save, so a schedule the server paused
on its own stayed paused forever.

The mapper now leaves absent fields alone instead of overwriting them with
null, which is what a PATCH should have done all along: previously a caller
had to echo back the entire schedule, and omitting `frequency` sent a zero
that the entity rejected outright. `recurrenceType` and `recurrenceBasedOn`
lose their non-null constraints for the same reason - required on create,
but not on a partial update.

The controller already stops and recreates the Quartz jobs after a patch,
and job creation already checks the flag, so pausing and resuming take
effect without further changes there.

On the mobile side the schedule section regains its switch.

Co-authored-by: Cursor <cursoragent@cursor.com>
Schedules could be read but not written from the phone, so setting one up
meant finding a desk. The form is grouped as name, then recurrence, then the
work order the recurrence stamps out.

The due date delay sits with the recurrence rather than with the work order
fields, where the shared helper would otherwise drop it: it is stored on the
schedule, and a generated work order has no fixed due date for it to sit
beside. Saving writes both halves, since the recurrence is its own resource.

The My Day feed gains what is due in the next week. Those are schedules, not
work orders - nothing has been raised yet - so they sit below the outstanding
work rather than competing with it. The next date is worked out on the client
because the API does not report one.

Co-authored-by: Cursor <cursoragent@cursor.com>
Field edits on a work order used to fail silently or with a JSON string
when the network dropped mid-tap. ApiError now carries status, body, and
a network flag; getErrorMessage understands both that and the legacy shape.

Five mutations queue to AsyncStorage when offline — status, task answers,
timer start/stop, comments, and task photos — and flush in order on
reconnect. Status and task changes apply optimistically so the screen
still reflects what the technician did.

A connectivity banner reports offline state and pending sync count; tapping
it triggers a manual flush. An error boundary wraps the root layout so a
render crash in one screen does not white-screen the whole app.

Co-authored-by: Cursor <cursoragent@cursor.com>
EnumFilter chips now announce which field they filter. Work order and
entity list search bars expose an explicit search accessibility label.

Co-authored-by: Cursor <cursoragent@cursor.com>
FlatList-backed PaginatedEntityList replaces ScrollView rows on assets,
parts, locations, meters, requests, notifications, people, teams,
vendors, and customers. Nested work order tabs on asset, part, and
location detail reuse WorkOrderCard through a shared list helper.

Co-authored-by: Cursor <cursoragent@cursor.com>
Asset detail gains a Downtimes tab with status control, CRUD, and
duration history. Work order and template form placeholders now pull
from i18n keys instead of hard-coded English strings.

Co-authored-by: Cursor <cursoragent@cursor.com>
Serialize queue read/write with a mutex and dedupe concurrent flushes so
enqueued field mutations are not dropped or executed twice. Notifications
pull-to-refresh now reloads from the API; asset status changes PATCH only
the status field. Docker entrypoint tolerates a missing .env and nginx
proxies instance-config to the backend instead of stubbing LDAP off.

Co-authored-by: Cursor <cursoragent@cursor.com>
@netlify

netlify Bot commented Aug 10, 2026

Copy link
Copy Markdown

👷 Deploy request for atlasjs-home pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit 7ded78d

@iib0011

iib0011 commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Hi. Thanks for this PR. Too many changes. Good and bad ones. Next time please create multiple PRs for each feature

@iib0011 iib0011 closed this Aug 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants