feat(mobile): UX overhaul — My Day, PM, EntityListCard, offline queue, a11y - #255
Closed
stephen20020 wants to merge 22 commits into
Closed
feat(mobile): UX overhaul — My Day, PM, EntityListCard, offline queue, a11y#255stephen20020 wants to merge 22 commits into
stephen20020 wants to merge 22 commits into
Conversation
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>
👷 Deploy request for atlasjs-home pending review.Visit the deploys page to approve it
|
Contributor
|
Hi. Thanks for this PR. Too many changes. Good and bad ones. Next time please create multiple PRs for each feature |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Mobile field UX overhaul across 21 commits on
feat/mobile-ux-overhaul:ApiError, connectivity banner, error boundary, offline queue for 5 WO field mutations (mutex-hardened), accessibility labels on search bars and filter chipsPaginatedEntityList+EntityListCardon 10 entity index screens, nested WO lists on asset/part/location tabs, asset downtime tab with CRUD, i18n for WO form placeholdersgetNotifications; asset status PATCH sends{ status }only; Docker entrypoint tolerates missing.env; nginx proxies/api/instance-configto backendTest plan
Core navigation & lists
test2@test.com/password123against local backend (http://localhost:3001)Work orders & PM
Asset downtimes
Notifications & errors
Theme & a11y (spot check)
Optional — offline queue (lower priority)
Docker / nginx (if deploying stack)
.envis absent in nginx html dirGET /api/instance-configreturns backend LDAP flag (not hardcoded stub)Notes
HostTarget::registerInstanceassert after long sessions; full Metro restart clears it (dev-only).workOrder↔offlineMutations↔offlineQueueare known and non-blocking.Made with Cursor