O que mudou
Infra (por app):
i18n/locales.ts — lista de locales (pt, en), default pt, labels para o seletor
i18n/request.ts — lê o cookie NEXT_LOCALE, carrega as mensagens
messages/pt.json + messages/en.json — todas as strings extraídas
next.config.ts — envolvido com withNextIntl (operator-pwa: withPWA(withNextIntl(...)))
app/layout.tsx — <html lang={locale}> dinâmico, NextIntlClientProvider
app/language-switcher.tsx — seletor PT | EN (cookie + router.refresh())
23 ficheiros de UI atualizados — todos os textos visíveis agora usam t('...') ou getTranslations.
Datas no relatório passaram de toLocaleString('pt-PT') fixo para useFormatter() do next-intl — localizam-se automaticamente.
Plurais em ICU no sync-chip: {count, plural, one {# pedido...} other {# pedidos...}}.
Resultado dos testes:
pnpm test:e2e — 3/3 ✓
pnpm test:e2e:auth — 4/4 ✓
tsc --noEmit em ambas as apps — limpo ✓
Para adicionar uma língua futura: criar messages/<locale>.json + adicionar o locale a i18n/locales.ts em cada app. O seletor aparece automaticamente.
370 lines
14 KiB
Markdown
370 lines
14 KiB
Markdown
# FieldOps — MAI CALL v0.3
|
||
|
||
Modular industrial SaaS monorepo. **MAI CALL v0.3** is the latest shipped
|
||
feature: a full maintenance-request loop (offline-first, operator PIN + admin
|
||
password auth) with an **end-of-shift report** for supervisors.
|
||
|
||
## What's here
|
||
|
||
```
|
||
apps/
|
||
operator-pwa/ Next.js 15 — operator mobile console (port 3000)
|
||
admin-web/ Next.js 15 — maintenance queue for admin/supervisor (port 3001)
|
||
packages/
|
||
db/ Prisma 6 schema + multi-tenant extension
|
||
api/ tRPC v11 routers (maintenanceRequest, workstation, user, storage)
|
||
storage/ ObjectStorage abstraction + MinIO/S3 implementation
|
||
ui/ Shared shadcn-style components
|
||
domain/ Pure domain logic (reserved for later modules)
|
||
config/ TypeScript / ESLint / Tailwind presets
|
||
e2e/ Playwright happy-path test
|
||
```
|
||
|
||
## Prerequisites
|
||
|
||
| Tool | Version | Notes |
|
||
| --- | --- | --- |
|
||
| Node.js | 22 LTS | Required by Next.js 15 |
|
||
| pnpm | 11+ | `npm i -g pnpm@11` or `corepack enable pnpm` |
|
||
| Docker Desktop | any recent | Provides Postgres 16 + MinIO |
|
||
| Git | any | — |
|
||
|
||
---
|
||
|
||
## Quick-start (< 15 min)
|
||
|
||
Run these commands from the **repo root** in order.
|
||
|
||
```sh
|
||
# 1. Copy and configure the environment file
|
||
cp .env.example .env
|
||
# The defaults work out of the box for local dev.
|
||
# AUTH_DEV_AUTOLOGIN is already "true" in .env — leave it.
|
||
|
||
# 2. Install dependencies
|
||
pnpm install
|
||
|
||
# 3. Start Postgres + MinIO (creates the fieldops bucket automatically)
|
||
docker compose up -d
|
||
|
||
# 4. Apply the database schema
|
||
pnpm db:migrate
|
||
|
||
# 5. Seed the demo tenant, 3 operators, and 3 workstations
|
||
pnpm db:seed
|
||
|
||
# 6. Start both apps
|
||
pnpm --filter @repo/operator-pwa dev &
|
||
pnpm --filter @repo/admin-web dev
|
||
```
|
||
|
||
| URL | What it is |
|
||
| --- | --- |
|
||
| http://localhost:3000 | Operator PWA |
|
||
| http://localhost:3001 | Admin / Manutenção queue |
|
||
| http://localhost:9001 | MinIO console (see credentials below) |
|
||
|
||
---
|
||
|
||
## Demo flow
|
||
|
||
### As an operator (port 3000)
|
||
|
||
1. Open http://localhost:3000.
|
||
With `AUTH_DEV_AUTOLOGIN=true` you land on the home page as
|
||
`admin@demo.local`. To simulate a real operator log-in, navigate to
|
||
http://localhost:3000/select-operator, tap **op1@demo.local**, then
|
||
enter PIN **1111** on the keypad.
|
||
(op2 = **2222**, op3 = **3333**)
|
||
2. Tap **Pedir manutenção**.
|
||
3. Select a workstation, optionally attach a photo, write a description,
|
||
and tap **Enviar pedido**.
|
||
4. The page shows **"Pedido enviado"** once the sync completes (usually
|
||
within 1–2 seconds when online).
|
||
|
||
**Offline test:**
|
||
Chrome DevTools → Network → Offline → create 3 requests → Network → Online.
|
||
The requests sync automatically within ~10 s; "Tudo sincronizado" appears.
|
||
|
||
> ⚠️ The offline navigation test only works against a **production build** of
|
||
> `operator-pwa`. In `pnpm dev` mode, Next.js generates page chunks on demand,
|
||
> so the service worker has nothing to precache and navigation to
|
||
> `/maintenance/new` fails with `Failed to fetch`. See the
|
||
> [Troubleshooting](#troubleshooting) section for the workaround.
|
||
|
||
### As admin / maintenance supervisor (port 3001)
|
||
|
||
1. Open http://localhost:3001.
|
||
With `AUTH_DEV_AUTOLOGIN=true` you land on the maintenance queue
|
||
automatically. Without it, you see a login form — use
|
||
**admin@demo.local** / **admin1234**.
|
||
2. The queue refreshes every 5 s; new requests appear automatically.
|
||
3. Click **Aceitar** to claim a request (status: Em curso).
|
||
4. Click **Marcar resolvido**, optionally add a note, click **Confirmar**
|
||
(status: Resolvido).
|
||
5. The document tab title shows `(N) FieldOps — Manutenção` when there are
|
||
open requests.
|
||
|
||
### Shift report (admin-web only)
|
||
|
||
1. In the maintenance queue, click **Relatório de turno** (top-right header).
|
||
2. Choose a window — **Manhã** (06–14 h), **Tarde** (14–22 h), **Noite** (22–06 h),
|
||
**Hoje** (midnight → now), or **Personalizado** (free date-time range).
|
||
3. The report shows: total/resolved/open counts, avg & max response time,
|
||
avg resolution time, breakdown by workstation and area, and a list of
|
||
requests still open at report time.
|
||
4. Click **Imprimir** to print / save as PDF via the browser.
|
||
|
||
After `pnpm db:seed`, the "Hoje" window already has 6 sample requests
|
||
(3 resolved, 1 claimed, 2 open) so the report is never empty on first boot.
|
||
|
||
---
|
||
|
||
## MinIO (photo storage)
|
||
|
||
| Setting | Value |
|
||
| --- | --- |
|
||
| API endpoint | http://localhost:9000 |
|
||
| Web console | http://localhost:9001 |
|
||
| Root user | `fieldops` |
|
||
| Root password | `fieldops123` |
|
||
| Bucket | `fieldops` |
|
||
|
||
Photos are stored as presigned-URL objects under
|
||
`tenants/{tenantId}/maintenance/{uuid}.jpg`.
|
||
|
||
To back up locally:
|
||
```sh
|
||
# Install mc (MinIO client) then:
|
||
mc alias set local http://localhost:9000 fieldops fieldops123
|
||
mc mirror local/fieldops ./backup-photos
|
||
```
|
||
|
||
---
|
||
|
||
## Running the E2E tests
|
||
|
||
```sh
|
||
# One-time browser install (downloads ~170 MB of Chromium)
|
||
pnpm --filter @repo/e2e install-browsers
|
||
```
|
||
|
||
### `pnpm test:e2e` — happy-path + shift report (autologin ON)
|
||
|
||
Starts both dev servers with `AUTH_DEV_AUTOLOGIN=true`. Safe to run at any
|
||
time — reuses servers already running on 3000/3001 if available.
|
||
|
||
Expected: **3 passed** (~45 s):
|
||
- MAI CALL happy path: create → claim → resolve
|
||
- Shift report: renders with seed data and reacts to window selection
|
||
- Shift report: accessible from the maintenance queue link
|
||
|
||
### `pnpm test:e2e:auth` — real login (autologin OFF)
|
||
|
||
Tests that the login UI actually works — operator PIN keypad and admin
|
||
email/password — without the dev back door.
|
||
|
||
**Precondition: no servers running on ports 3000 or 3001.** This command
|
||
starts its own servers with `AUTH_DEV_AUTOLOGIN=false`. If ports are busy:
|
||
|
||
```sh
|
||
# Windows
|
||
Get-Process node | Stop-Process -Force
|
||
```
|
||
|
||
Also requires `pnpm db:seed` to have been run.
|
||
|
||
Expected: **4 passed** (~60 s):
|
||
- Operator: wrong PIN shows error, correct PIN enters the app
|
||
- Operator: unauthenticated root redirects to picker
|
||
- Admin: protected route redirects to /login without session
|
||
- Admin: wrong password shows error, correct password enters the queue
|
||
|
||
---
|
||
|
||
### Manual smoke checklist (5 min — covers print + offline)
|
||
|
||
Automated tests don't cover the print PDF or offline sync. Run this when
|
||
you want end-to-end confidence before a demo:
|
||
|
||
```
|
||
1. docker compose up -d && pnpm db:seed
|
||
|
||
2. Operator (real login):
|
||
pnpm --filter @repo/operator-pwa dev → http://localhost:3000/select-operator
|
||
Try PIN 9999 → see error. Then op1 → PIN 1111 → enters app.
|
||
|
||
3. Admin (real login):
|
||
pnpm --filter @repo/admin-web dev → http://localhost:3001/login
|
||
admin@demo.local / admin1234 → maintenance queue.
|
||
|
||
4. Shift report:
|
||
Click "Relatório de turno" in header → click Manhã/Tarde/Noite/Hoje
|
||
→ numbers update each time.
|
||
Click "Imprimir" → confirm PDF is clean (no buttons/selector/nav).
|
||
|
||
5. Offline (requires production build of operator-pwa — see Troubleshooting):
|
||
DevTools → Network → Offline → create 2 requests → Online → they sync.
|
||
```
|
||
|
||
---
|
||
|
||
## Known limitations — v0.1 (demo only)
|
||
|
||
| Limitation | Detail | Target |
|
||
| --- | --- | --- |
|
||
| **No real authentication in dev** | `AUTH_DEV_AUTOLOGIN=true` lets anyone in as admin (dev/test only — ignored when `NODE_ENV=production`). Real auth is implemented: admin uses email + password, operators use list + PIN. | — |
|
||
| **Operator picker + PIN, not TAG/card** | Operator identity is chosen from a list and confirmed with a PIN, rather than read from an RFID badge. | MY QUALITY module |
|
||
| **No multi-tenant onboarding UI** | Tenants are created via `pnpm db:seed` / SQL only. | when 2nd customer onboards |
|
||
| **No scheduled reports** | The shift report is on-demand (open the page, print). Auto-email at shift end requires a scheduler + email service. | v0.4 or post-pilot |
|
||
| **Single photo per request** | No video, audio, or multiple photos. | when pilot asks |
|
||
| **Safari / iOS Background Sync** | Background Sync API is not supported on Safari; sync falls back to main-thread polling every 10 s when the tab is open. | acceptable for pilot |
|
||
| **No push notifications** | Polling at 5 s on the admin-web tab is the notification mechanism. | if pilot requires it |
|
||
| **Dev-only storage** | MinIO runs in Docker. No backup cron, no lifecycle policy, no cloud migration yet. | before pilot |
|
||
| **No i18n** | Hardcoded Portuguese (Mangualde plant). | v0.2 with pilot |
|
||
| **No observability** | Structured logs via Pino; no trace/metric pipeline. | when pilot requires it |
|
||
|
||
> ⚠️ **NEVER deploy with `AUTH_DEV_AUTOLOGIN=true`.** That flag is a
|
||
> back door for local dev and CI only. It is **ignored at the code level**
|
||
> when `NODE_ENV=production`, so a misconfigured `.env` in production won't
|
||
> open a hole. The chokepoints are `apps/*/lib/auth.ts → resolveUser()` and
|
||
> `apps/*/middleware.ts`.
|
||
|
||
---
|
||
|
||
## Languages (i18n)
|
||
|
||
Both apps support **Portuguese (PT, default) and English (EN)**. The language is
|
||
stored in a `NEXT_LOCALE` cookie and selected via the **PT | EN** switcher in the
|
||
app header.
|
||
|
||
### Changing language
|
||
|
||
Click the **PT | EN** pill in the header of either app. The preference is saved
|
||
in a cookie (1-year expiry) — no account change required.
|
||
|
||
### Adding a new language
|
||
|
||
1. Create `apps/<app>/messages/<locale>.json` (copy `en.json` as a starting point).
|
||
2. Add the locale to `LOCALES` in `apps/<app>/i18n/locales.ts`.
|
||
3. That's it — the switcher picks it up automatically.
|
||
|
||
See **[docs/i18n.md](docs/i18n.md)** for the full guide, including the
|
||
key-parity and ICU validation scripts to run before shipping a new language.
|
||
|
||
---
|
||
|
||
## Common commands
|
||
|
||
| Command | What it does |
|
||
| --- | --- |
|
||
| `pnpm install` | Install all workspace deps |
|
||
| `docker compose up -d` | Start Postgres + MinIO (detached) |
|
||
| `docker compose down` | Stop services (data persists in volumes) |
|
||
| `docker compose down -v` | Stop and **delete all data** |
|
||
| `pnpm --filter @repo/operator-pwa dev` | Operator PWA only (port 3000) |
|
||
| `pnpm --filter @repo/admin-web dev` | Admin web only (port 3001) |
|
||
| `pnpm db:migrate` | Apply pending Prisma migrations |
|
||
| `pnpm db:seed` | Re-seed demo data (idempotent, safe to re-run) |
|
||
| `pnpm db:reset` | Drop, recreate, migrate, seed |
|
||
| `pnpm db:studio` | Open Prisma Studio at http://localhost:5555 |
|
||
| `pnpm typecheck` | Typecheck every package |
|
||
| `pnpm test:e2e` | Run the happy-path Playwright test |
|
||
| `pnpm format` | Prettier write across the workspace |
|
||
| `pnpm tsx scripts/storage-smoke.ts` | Verify MinIO presigned upload/download |
|
||
| `pnpm tsx scripts/maintenance-smoke.ts` | Verify the full create→claim→resolve cycle |
|
||
| `pnpm tsx scripts/auth-smoke.ts` | Verify hashing, PIN/password login, and lockout |
|
||
| `pnpm tsx scripts/report-smoke.ts` | Verify shift-report aggregation against seeded data |
|
||
|
||
---
|
||
|
||
## Architecture notes
|
||
|
||
### Multi-tenancy
|
||
|
||
Every Prisma model except `Tenant` carries a `tenantId` column. Tenant
|
||
scoping is enforced at the Prisma layer via an extension in
|
||
`packages/db/src/tenant-extension.ts`. Application code always goes
|
||
through `ctx.db.*` (scoped) — the unscoped `ctx.prisma` is a code-review
|
||
red flag.
|
||
|
||
**Note on `$transaction`:** The interactive-callback form of `$transaction`
|
||
receives a Prisma client that does NOT support `$extends`. Tenant scoping
|
||
inside transactions is done by manually injecting `tenantId` into each
|
||
`where`/`data` clause. See `packages/api/src/routers/maintenance-request.ts`
|
||
for the pattern.
|
||
|
||
### Offline sync
|
||
|
||
The operator PWA stores pending requests in IndexedDB (Dexie 4). The sync
|
||
loop (`lib/queue/sync.ts`) runs in the main thread — triggered by the
|
||
`online` event, `visibilitychange`, and a 10 s polling interval. The
|
||
Background Sync API is registered opportunistically (works in Chrome,
|
||
ignored elsewhere). Communication between the sync loop and UI is via
|
||
`BroadcastChannel('mai-call-sync')`.
|
||
|
||
### Storage
|
||
|
||
Photos are uploaded directly from the browser to MinIO via S3 presigned
|
||
PUT URLs. The tRPC layer signs the URL; the browser performs the PUT. The
|
||
object key is returned and stored in the `MaintenanceRequest` row.
|
||
`packages/storage` implements the `ObjectStorage` interface with MinIO (via
|
||
AWS SDK v3 S3 protocol) and is portable to AWS S3, Cloudflare R2, or Wasabi
|
||
by changing the endpoint env var.
|
||
|
||
---
|
||
|
||
## Troubleshooting
|
||
|
||
**`UNAUTHORIZED` on every page** — `AUTH_DEV_AUTOLOGIN` is `false` in your
|
||
`.env`. Set it to `true` for local dev, or sign in via the operator picker.
|
||
|
||
**`Tenant not found`** — the seed was wiped. Run `pnpm db:seed`.
|
||
|
||
**Admin login redirects to `:3000` / the operator picker** — the admin-web needs
|
||
its own `AUTH_URL=http://localhost:3001` (the shared `.env` points at the operator
|
||
on :3000). This is handled automatically: the admin `dev` script loads
|
||
`apps/admin-web/.env.admin` with precedence. If you deleted or edited that file
|
||
and hit this, restore `AUTH_URL="http://localhost:3001"` there. In production,
|
||
each app gets its own `AUTH_URL` from the deploy environment.
|
||
|
||
**`DATABASE_URL not found`** — `.env` is missing or Docker Postgres is not
|
||
running. Run `docker compose up -d` then retry.
|
||
|
||
**`ERR_PNPM_IGNORED_BUILDS` after adding a package** — pnpm 11 blocks
|
||
postinstall scripts by default. Add the package to `pnpm-workspace.yaml`
|
||
`allowBuilds:`.
|
||
|
||
**Playwright: port already in use** — kill leftover dev servers:
|
||
```sh
|
||
# Windows
|
||
Get-Process node | Stop-Process -Force
|
||
# macOS / Linux
|
||
pkill -f 'next dev'
|
||
```
|
||
|
||
**MinIO photos not loading in admin-web** — verify MinIO is running
|
||
(`docker compose ps`) and that the `fieldops` bucket exists
|
||
(`docker logs fieldops-minio-init-1`).
|
||
|
||
**Offline navigation fails with `Failed to fetch` / `NetworkOnly`** — the PWA
|
||
service worker can only serve pages it has precached, and `pnpm dev` builds
|
||
pages on demand (no precache). To test the full offline flow, run the
|
||
operator PWA in production mode:
|
||
```sh
|
||
# Stop the running `pnpm dev` for operator-pwa first, then:
|
||
pnpm --filter @repo/operator-pwa build
|
||
pnpm --filter @repo/operator-pwa start
|
||
```
|
||
Load http://localhost:3000 once while online (so the SW precaches the shell),
|
||
reload the tab to activate the new SW, then switch DevTools to Offline. The
|
||
admin-web can stay in `dev` mode — only the operator PWA needs the production
|
||
build for offline navigation. The IndexedDB sync queue itself works in both
|
||
modes; only the page-navigation layer requires precache.
|
||
|
||
---
|
||
|
||
## License
|
||
|
||
Internal. All rights reserved.
|