Measure first, and be prepared to be wrong in both directions
Majal's mobile position was established by loading a running instance in Chromium at an iPhone 13 viewport — 390 × 664 — and reading the numbers, rather than by opening a page on somebody's phone and forming an impression.
The result was better than expected in the frame and worse than expected in the
content. Nothing overflowed: backend home, defect list, inspection form and the portal
all measured scrollWidth 390 against a 390 viewport. Inspection form
fields stacked one per row. The portal has always fitted, because it is Bootstrap.
The failure was somewhere a responsive-design audit does not look. The inspection checklist was a table 577 px wide inside a 390 px screen, so it truncated the questions to "Wall finish…", "Ceiling tile…", "Door clos…" — that is, it truncated exactly the words that distinguish one checklist line from another, on the screen a person uses standing in the room being inspected.
The fix was not CSS and not JavaScript. Majal's responsive view layer selects the
kanban sub-view of a one2many whenever the screen is small — so a phone
layout is a sub-view. One question per card, its section above it, the
answer as a badge, and tapping a card opens a form with radio buttons instead of a
dropdown.
Two assumptions that turned out to be wrong
Both were in the original plan, and both changed it.
A PWA already existed. Majal ships a manifest and a registered service worker with its own name, colours, icons and a start URL on the relevant workspace. Installed to a phone, the product opens as Majal rather than a generic backend root.
Installable is not the same as offline. Majal defines safe offline boundaries for field drafts and synchronisation rather than pretending every screen can work without a connection.
Installable ≠ offline. The gap between those two words is the whole of the expensive phase, and it is the gap most mobile-app conversations skate over.
The three asks, priced
1. "It works on my phone"
Largely true the day it was measured. Finishing it meant fixing the screens a person uses standing in a room — the checklist above, and the defect form.
The defect form was a different kind of problem, and a more instructive one. It is a top-level form with no small-screen variant to select, so there is no sub-view trick. Groups stack on a phone, which means the field order is the screen order — and the photo was tenth, below nine fields and a description, on a screen where the photo is the entire point of a snag.
Reordering to title → photo → location → severity → project → description measures as one screen at 390 × 664 with nothing below the fold. Trade, phase, who fixes it and by when moved to an Assignment tab; the rectified photo to a Rectification tab. The empty reference heading is hidden until save, where it had been costing a third of a screen above the first thing you type.
2. "It is on my home screen and feels like our product"
Nearly true; a manifest override and a start URL away. Majal's own name, short name, brand colours, description, a start URL on the construction workspace rather than the construction workspace, with Construction and Facilities shortcuts for the long-press menu.
One thing in this phase was wrong rather than missing, and it is the kind of detail
that only shows up on a device. The maskable icon was declared, not drawn: a
single manifest entry claimed purpose: "any maskable" while pointing at an
icon with rounded corners, so a launcher's own mask would turn its transparent corners
into notches — and whose ground line ran 56 units from the centre when the safe circle
has a radius of 51.2. Android would have sliced both ends off. There are now two
entries: one for any, and a full-bleed maskable variant with
the mark scaled to 0.78 about the centre.
There is a test asserting both properties by parsing the served SVG — written after the first version of that test matched the comment explaining the rule instead of the markup obeying it.
3. "It works in a basement with no signal and syncs later"
This is the expensive one. A full web client is not built to run offline. Anything genuinely offline needs a purpose-built surface for the field workflows, talking to the same server.
The honest options, cheapest first:
| Option | What it buys | Rough cost |
|---|---|---|
| Read-only offline | Cache the current project's open defects, today's inspections and the drawings behind them. A site engineer can see their list in a basement. | About a week — and it covers a surprising share of the real complaint |
| Queued writes | New defects, answered checklists and photos written locally and posted when the connection returns, with a visible queue. | Weeks, not days. Photo storage limits, retry, partial failure, and deciding what happens when two people close the same defect. |
| Native shell | Camera and filesystem integration and a store presence, by wrapping the PWA. | Worth it only if the store presence is itself the requirement |
A Flutter or React Native rewrite against a JSON-RPC API is a second product to maintain forever, and it is what "we need a mobile app" most often turns into if nobody asks which of the three asks it was.
What Majal actually shipped, and where the line is
Majal Field is a dedicated field PWA rather than an attempt to make the whole ERP work
offline. It stores only the current signed-in user's field pack, in per-user device
storage, with idempotent synchronisation and visible write_date conflicts.
Opening it as a different user purges the previous user's local database and cache from
that browser.
In airplane mode: assigned records and previously saved drawings remain available, safe actions enter a local sync queue, going online triggers synchronisation, applied operations disappear, and conflicts and rejected operations stay visible for review. Clear this device removes cached records and unsynchronised changes.
The boundary is the design Only safe draft, checklist and scan actions work offline. Approvals, financial posting, deletions and BIM stay online. The value of a bounded offline surface is that you can say exactly what it does; an offline mode that queues everything is an offline mode that cannot tell you whether the thing you did has happened.
What I would not do
Build a native app first. The portal already serves clients, consultants and subcontractors — the majority of people who will ever open this on a phone — and it measures clean at 390 px today. The group with a genuine offline need is the site team snagging in a building with no signal, and that need is specific enough to serve with a focused offline surface rather than a second copy of the whole ERP.
And the part most likely to disappoint
The BIM viewer on a phone. web-ifc is a 6 MB WASM download before the model itself, and parsing happens on the device. Pinch-to-zoom was missing entirely until it was measured; it works now, along with two-finger pan and suppressed page zoom over the canvas. A single-discipline model of one floor is fine on a modern handset.
A federated whole-tower model is not, and no front-end work changes that — it would need server-side geometry tiling, which is a project in its own right. On a phone, treat the viewer as "look at this level". Coordination happens at a desk.
majal_field_offline ships the bounded field PWA;
majal_branding ships the manifest and icons.
See the feature summary →