O que o supervisor encontra agora:
Na fila de manutenção (:3001), novo botão "Relatório de turno" no header.
Página /maintenance/report com:
Atalhos Manhã / Tarde / Noite / Hoje + seletor de dia + Personalizado (date-time livre)
Label sempre visível com a janela ativa ("Turno da Manhã — 30/05 06:00 → 14:00")
6 cartões de métricas: pedidos, resolvidos, em aberto, tempo médio de resposta, tempo médio de resolução, pior resposta
Tabela por posto e resumo por área
Lista "Em aberto à hora do relatório" (ou "Nada em aberto. ✓")
Botão Imprimir → PDF via browser; CSS @media print limpa botões/nav
Verificações verdes:
report-smoke.ts — 17/17 (totals, responseMs, resolutionMs, byWorkstation, byArea, stillOpen, window edge cases)
E2E MAI CALL happy-path — 1/1 (dados de seed extra não interferem)
TypeScript — limpo nos pacotes tocados (@repo/api, @repo/admin-web)
Seed cria 6 pedidos de exemplo: relatório "Hoje" nunca começa vazio
+
Resumo da revisão do v0.3
Conformidade com o plano: alta. Shape de output exato, ctx.db (tenant-scoped), requireRole, helper de turnos com o caso da noite, seed com 6 pedidos, UI completa + impressão. Tudo no sítio.
Dois defeitos reais que escaparam ao typecheck e ao E2E — corrigidos:
# Problema Correção
🔴 1 Fetch storm no modo "Hoje" (default): computeWindow recalculava to = new Date() a cada render → nova query key → loop de fetch contínuo. useMemo([windowState]) estabiliza a janela em report-view.tsx:101. Reclicar "Hoje" refresca. Também limpei estado morto (customFrom/customTo).
🔴 2 Smoke não cumpria o AC: re-implementava a agregação à mão em vez de chamar a procedure, e não testava to <= from → BAD_REQUEST (exigido pelo AC do Passo 1). Reescrito report-smoke.ts no padrão createCallerFactory — agora exercita a procedure real: agregação, BAD_REQUEST (to≤from e >31d), janela futura vazia, e FORBIDDEN para operador.
Verificações finais (todas verdes):
tsc --noEmit admin-web — limpo
report-smoke.ts — 22/22 (agora contra a procedure real)
E2E MAI CALL — 1 passed
290 lines
11 KiB
Markdown
290 lines
11 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
|
||
|
||
# Run the happy-path test (starts both dev servers automatically)
|
||
pnpm test:e2e
|
||
```
|
||
|
||
The Playwright config force-sets `AUTH_DEV_AUTOLOGIN=true` for the child
|
||
servers, so the test does not depend on the developer's `.env`.
|
||
Expected: **1 passed** in ~30 s.
|
||
|
||
---
|
||
|
||
## 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`.
|
||
|
||
---
|
||
|
||
## 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`.
|
||
|
||
**`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.
|