Skip to content

Commit 6384b7d

Browse files
tomal214cursoragent
andcommitted
feat: reports tier 1 and fix client navigation DOM errors
Add week summary cards, surgery/nurse/category breakdowns, incidents CSV export, richer demo seed, and stop removing the PWA boot splash node outside React during route changes. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 84706e4 commit 6384b7d

19 files changed

Lines changed: 985 additions & 99 deletions

File tree

docs/demo-script.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@ Use the seeded demo practice after `supabase db reset`.
3838
3. Highlight session warning banner when within 30 minutes of lock (13:15 / 18:00)
3939
4. Review per-surgery and per-nurse breakdown tables
4040

41-
## 5. Reports + export (30s)
41+
## 5. Reports + export (1 min)
4242

4343
1. Open **Reports** → 8-week completion + incidents chart
44-
2. Select a week → **Export CSV**
45-
3. Optional: **History** → date filter → export audit CSV
44+
2. Select a week → review summary cards (completion, mandatory missed, incidents, photos)
45+
3. Switch tabs: **By surgery** / **By nurse** / **By category**
46+
4. **Export tasks CSV** and **Export incidents CSV**
47+
5. Optional: **History** → date filter → export audit CSV
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Reports Tier 1 Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.
4+
5+
**Goal:** Enrich `/app/reports` with week summary cards, surgery/nurse/category breakdown tables, incidents CSV export, tests, and demo seed data.
6+
7+
**Architecture:** Pure aggregation in `src/lib/reports/week-breakdown.ts`; `getWeekReportDetail` in reports service; `GET /api/reports/week` + `GET /api/reports/export/incidents`; extend `ReportsView` UI.
8+
9+
**Tech Stack:** Next.js App Router, Vitest, Supabase, shadcn Tabs
10+
11+
---
12+
13+
## Files
14+
15+
| File | Action |
16+
|------|--------|
17+
| `src/lib/reports/week-breakdown.ts` | Create — summary + breakdown aggregators |
18+
| `src/lib/reports/incidents-csv.ts` | Create — incidents CSV builder |
19+
| `src/lib/services/reports.ts` | Modify — week detail + incidents export |
20+
| `src/lib/app/page-data.ts` | Modify — `loadWeekReportDetail` |
21+
| `src/app/api/reports/week/route.ts` | Create |
22+
| `src/app/api/reports/export/incidents/route.ts` | Create |
23+
| `src/components/app/ReportsView.tsx` | Modify — cards, tabs, dual export |
24+
| `supabase/seed.sql` | Modify — 8 weeks history, incidents, photos |
25+
| `tests/unit/reports/week-breakdown.test.ts` | Create |
26+
| `tests/unit/reports/incidents-csv.test.ts` | Create |

docs/testing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ Local setup: `supabase db reset` then `pnpm dev` → **http://localhost:3000**
5353

5454
## What to test (by flow)
5555

56-
**Manager** — dashboard stats, staff edit, template create/edit (category, priority, evidence), rota publish, reports CSV export.
56+
**Manager** — dashboard stats, staff edit, template create/edit (category, priority, evidence), rota publish, reports (week summary + breakdown tabs + tasks/incidents CSV export).
5757

5858
**Nurse** — progress bar, category chips, complete task with checklist/photo, morning sign-off, surgery switcher.
5959

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { createClient } from '@/lib/supabase/server'
2+
import { createAdminClient } from '@/lib/supabase/admin'
3+
import {
4+
MemberAuthError,
5+
requireManagerViewerOrAdmin,
6+
} from '@/lib/auth/member'
7+
import { jsonError } from '@/lib/api/response'
8+
import { reportsExportQuerySchema } from '@/lib/validation/reports'
9+
import { exportIncidentsCsv } from '@/lib/services/reports'
10+
11+
export async function GET(request: Request) {
12+
try {
13+
const supabase = await createClient()
14+
const member = await requireManagerViewerOrAdmin(supabase)
15+
16+
const { searchParams } = new URL(request.url)
17+
const parsed = reportsExportQuerySchema.safeParse({
18+
from: searchParams.get('from'),
19+
to: searchParams.get('to'),
20+
})
21+
22+
if (!parsed.success) {
23+
return jsonError(parsed.error.issues[0]?.message ?? 'Invalid request', 400)
24+
}
25+
26+
const admin = createAdminClient()
27+
const csv = await exportIncidentsCsv(
28+
admin,
29+
member,
30+
parsed.data.from,
31+
parsed.data.to
32+
)
33+
34+
return new Response(csv, {
35+
status: 200,
36+
headers: {
37+
'Content-Type': 'text/csv; charset=utf-8',
38+
'Content-Disposition': `attachment; filename="effinic-incidents-${parsed.data.from}-${parsed.data.to}.csv"`,
39+
},
40+
})
41+
} catch (error) {
42+
if (error instanceof MemberAuthError) {
43+
return jsonError(error.message, error.status)
44+
}
45+
console.error('Incidents export failed:', error)
46+
return jsonError('Something went wrong', 500)
47+
}
48+
}

src/app/api/reports/week/route.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { createClient } from '@/lib/supabase/server'
2+
import { createAdminClient } from '@/lib/supabase/admin'
3+
import {
4+
MemberAuthError,
5+
requireManagerViewerOrAdmin,
6+
} from '@/lib/auth/member'
7+
import { jsonError, jsonOk } from '@/lib/api/response'
8+
import { reportsWeekQuerySchema } from '@/lib/validation/reports'
9+
import { getWeekReportDetail } from '@/lib/services/reports'
10+
11+
export async function GET(request: Request) {
12+
try {
13+
const supabase = await createClient()
14+
const member = await requireManagerViewerOrAdmin(supabase)
15+
16+
const { searchParams } = new URL(request.url)
17+
const parsed = reportsWeekQuerySchema.safeParse({
18+
weekStart: searchParams.get('weekStart'),
19+
})
20+
21+
if (!parsed.success) {
22+
return jsonError(parsed.error.issues[0]?.message ?? 'Invalid request', 400)
23+
}
24+
25+
const admin = createAdminClient()
26+
const detail = await getWeekReportDetail(
27+
admin,
28+
member,
29+
parsed.data.weekStart
30+
)
31+
32+
return jsonOk(detail)
33+
} catch (error) {
34+
if (error instanceof MemberAuthError) {
35+
return jsonError(error.message, error.status)
36+
}
37+
console.error('Week report detail failed:', error)
38+
return jsonError('Something went wrong', 500)
39+
}
40+
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import BrandedBootScreen from '@/components/app/loading/branded-boot-screen'
1+
import DashboardLoadingSkeleton from '@/components/app/loading/dashboard-skeleton'
22

33
export default function DashboardLoading() {
4-
return <BrandedBootScreen message="Loading dashboard…" />
4+
return <DashboardLoadingSkeleton />
55
}

src/app/app/loading.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
1-
import BrandedBootScreen from '@/components/app/loading/branded-boot-screen'
1+
import {
2+
AppPageLoadingShell,
3+
PageHeaderSkeleton,
4+
} from '@/components/app/loading/page-shell'
5+
import { Skeleton } from '@/components/ui/skeleton'
26

37
export default function AppLoading() {
4-
return <BrandedBootScreen message="Opening Effinic…" />
8+
return (
9+
<AppPageLoadingShell label="Loading page">
10+
<PageHeaderSkeleton />
11+
<Skeleton className="h-48 w-full rounded-xl" />
12+
</AppPageLoadingShell>
13+
)
514
}

src/app/app/template.tsx

Lines changed: 0 additions & 11 deletions
This file was deleted.

src/components/app/AppNav.tsx

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -225,28 +225,30 @@ function MobileMoreSheet({
225225
const [open, setOpen] = useState(false)
226226

227227
return (
228-
<Sheet open={open} onOpenChange={setOpen}>
228+
<>
229229
<MobileMoreTab active={active} onOpen={() => setOpen(true)} />
230-
<SheetContent
231-
side="bottom"
232-
showCloseButton
233-
className="rounded-t-2xl pb-[max(1rem,env(safe-area-inset-bottom))]"
234-
>
235-
<SheetHeader className="text-left">
236-
<SheetTitle>More</SheetTitle>
237-
</SheetHeader>
238-
<nav className="flex flex-col gap-1 px-1">
239-
{items.map((item) => (
240-
<NavLink
241-
key={item.href}
242-
item={item}
243-
pathname={pathname}
244-
variant="sheet"
245-
onNavigate={() => setOpen(false)}
246-
/>
247-
))}
248-
</nav>
249-
</SheetContent>
250-
</Sheet>
230+
<Sheet open={open} onOpenChange={setOpen}>
231+
<SheetContent
232+
side="bottom"
233+
showCloseButton
234+
className="rounded-t-2xl pb-[max(1rem,env(safe-area-inset-bottom))]"
235+
>
236+
<SheetHeader className="text-left">
237+
<SheetTitle>More</SheetTitle>
238+
</SheetHeader>
239+
<nav className="flex flex-col gap-1 px-1">
240+
{items.map((item) => (
241+
<NavLink
242+
key={item.href}
243+
item={item}
244+
pathname={pathname}
245+
variant="sheet"
246+
onNavigate={() => setOpen(false)}
247+
/>
248+
))}
249+
</nav>
250+
</SheetContent>
251+
</Sheet>
252+
</>
251253
)
252254
}

src/components/app/PwaBootSplash.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ export default function PwaBootSplash() {
3131
if (!splash) return
3232

3333
if (!isStandalonePwa()) {
34-
splash.remove()
34+
splash.style.display = 'none'
35+
splash.setAttribute('aria-hidden', 'true')
3536
return
3637
}
3738

@@ -43,7 +44,10 @@ export default function PwaBootSplash() {
4344
if (cancelled) return
4445
splashEl.style.opacity = '0'
4546
splashEl.style.pointerEvents = 'none'
46-
window.setTimeout(() => splashEl.remove(), FADE_MS)
47+
window.setTimeout(() => {
48+
splashEl.style.display = 'none'
49+
splashEl.setAttribute('aria-hidden', 'true')
50+
}, FADE_MS)
4751
}
4852

4953
function tryDismiss() {

0 commit comments

Comments
 (0)