Skip to content

Commit 46a5351

Browse files
scriptcodedclaude
andcommitted
Add staff dietary-per-day report + data-freshness controls
The real report: dietary information per day for funktionärer, ledar- funktionärer, and medföljande barn, read from the Scoutnet staff + participants projects. Domain (src/reports/staff-dietary/): - constants.ts — question/choice tables hardcoded from the live forms (staff 43853, child 43854, participant 43844). - populations.ts — classify staff (fee 32207) / child (32359, guest 32810 ignored) / leader-staff (participants fee 32348 or checkbox 89316); dedupe by member_no, priority staff > leader > child. - presence.ts — staff/child period model (union of selected periods minus excluded days; child 91058 is positive attend-days); leader-staff fixed 25 Jul–1 Aug. - diet.ts — allergens/preferences unified by label across both form schemas; section gates; consent (90424) gates diet only for leader-staff. - core.ts (server-only) + report.ts / export.ts (server-fn entry points). UI (reports.staff-dietary.tsx): - Deltagartyper (all selected by default), check-in, day range picker, and a staff-only "confirmed dates" (111761) filter; per-person matrix with sticky name column, member no, per-diet and per-day (Ja/blank) columns; summary tab with per-day headcount + diet counts reflecting filters. - CSV/XLSX export (node-xlsx), timestamped filename; download requires a sensitive-data acknowledgement dialog. - "Senast uppdaterad" + manual refresh button. Infra: - Scoutnet cache is now stale-while-revalidate (15-min TTL) with fetchedAt tracking; only the first cold request blocks. - Router-level pending component so slow report loaders show a spinner. - CI workflow to build/push the Docker image. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a19399c commit 46a5351

21 files changed

Lines changed: 1704 additions & 49 deletions

.github/workflows/build-image.yml

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
name: Build Docker Image
2+
3+
on:
4+
push:
5+
release:
6+
types: [published]
7+
8+
env:
9+
REGISTRY: ghcr.io
10+
IMAGE_NAME: ${{ github.repository }}
11+
12+
concurrency:
13+
group: build-image-${{ github.ref }}
14+
cancel-in-progress: true
15+
16+
jobs:
17+
docker:
18+
runs-on: ubuntu-latest
19+
permissions:
20+
contents: read
21+
packages: write
22+
attestations: write
23+
id-token: write
24+
steps:
25+
- name: Checkout
26+
uses: actions/checkout@v5
27+
28+
- name: Log in to the Container registry
29+
uses: docker/login-action@v3
30+
with:
31+
registry: ${{ env.REGISTRY }}
32+
username: ${{ github.actor }}
33+
password: ${{ secrets.GITHUB_TOKEN }}
34+
35+
- name: Set up QEMU
36+
uses: docker/setup-qemu-action@v3
37+
38+
- name: Set up Docker Buildx
39+
uses: docker/setup-buildx-action@v3
40+
41+
- name: Extract metadata (tags, labels) for Docker
42+
id: meta
43+
uses: docker/metadata-action@v5
44+
with:
45+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
46+
tags: |
47+
type=sha
48+
type=raw,value=latest,enable={{is_default_branch}}
49+
type=semver,pattern={{version}}
50+
51+
- name: Build and push
52+
id: push
53+
uses: docker/build-push-action@v6
54+
with:
55+
push: true
56+
platforms: linux/amd64
57+
tags: ${{ steps.meta.outputs.tags }}
58+
labels: ${{ steps.meta.outputs.labels }}
59+
cache-from: type=gha
60+
cache-to: type=gha,mode=max
61+
62+
- name: Generate artifact attestation
63+
uses: actions/attest-build-provenance@v3
64+
with:
65+
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
66+
subject-digest: ${{ steps.push.outputs.digest }}
67+
push-to-registry: true

CLAUDE.md

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -42,19 +42,31 @@ Project API — **owns no data, has no database.**
4242
MUI render, `useAppBarTitle(...)`).
4343
3. Link it from `src/routes/_authenticated/index.tsx`.
4444

45-
## Future work — the real "staff dietary info per day" report
46-
47-
Not built yet. Known complexity to respect when building it:
48-
- **Days are layered & population-specific.** Presence per day is a multi-select
49-
answer, but earlier form questions let people opt out of specific days, and the
50-
calculation differs for **staff vs participants vs child-of-staff** (child-of-
51-
staff live in the staff project under a different fee + form). Compute per
52-
population kind (`project.kind` + fee).
53-
- **Diet data is consent-gated & sensitive.** Reuse the question-ID map and
54-
consent gating from `j26-signupinfo/pyapp/app/scoutnet_forms.py`
55-
(`_j26_question_hack_individual`): consent flags `90424`/`90433`, allergies
56-
`88199`, preferences `89292`, extra-restricted `88206`, health section `21334`.
57-
Apply role-based redaction. `data.ts` should expose the raw `questions` map for
58-
the report to decode.
59-
- The current staff-roster report deliberately touches none of this — only
60-
`name` / `fee` / `checked_in`.
45+
## Reports
46+
47+
- **staff-roster** (`src/reports/staff-roster/`) — trivial proof-of-concept:
48+
merged roster across both projects, check-in filter. Touches only
49+
`name`/`fee`/`checked_in`.
50+
- **staff-dietary** (`src/reports/staff-dietary/`) — the real dietary-per-day
51+
report. Module layout:
52+
- `constants.ts` — all hardcoded question/choice tables, taken from the live
53+
form defs (staff 43853, child 43854, participant 43844). Regenerate via the
54+
discovery script if forms change.
55+
- `values.ts` — defensive readers (multiselect = **array** of option ids,
56+
boolean = `"0"`/`"1"`, text = string).
57+
- `populations.ts` — classify `staff`(fee 32207) / `child`(32359, ignore guest
58+
32810) from the staff project; `leader-staff` from participants (fee 32348
59+
**or** checkbox `89316`). Dedupe by `member_no`, priority staff>leader>child.
60+
- `presence.ts` — staff/child period model: `presentDays = ⋃(selected-period
61+
days) − ⋃(absence days of checked periods)`; **child `91058` is positive**
62+
(attend-days, not absence); leader-staff fixed **25 Jul–1 Aug**.
63+
- `diet.ts` — allergens/prefs unified by Swedish label across the two form
64+
schemas; section gates; consent `90424` gates diet **only for leader-staff**
65+
(staff/child forms have no consent question).
66+
- `core.ts` — server-only orchestration (`runStaffDietaryReport`): fetch →
67+
classify → dedupe → presence+diet → filter → aggregates. **Not** a server fn,
68+
so both `report.ts` and `export.ts` reuse it.
69+
- `report.ts` / `export.ts` — the `createServerFn` entry points (auth-gated).
70+
Keep server-only imports (data/cache/node:fs, node-xlsx) reachable **only**
71+
through a server-fn handler or `core.ts`; a plain exported fn that pulls them
72+
into a client-imported module breaks the browser build.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"dayjs": "^1.11.20",
3232
"jose": "^6.2.2",
3333
"nitro": "npm:nitro-nightly@latest",
34+
"node-xlsx": "^0.24.0",
3435
"react": "^19.2.0",
3536
"react-dom": "^19.2.0"
3637
},

pnpm-lock.yaml

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/pending.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Box, CircularProgress, Typography } from "@mui/material";
2+
3+
/**
4+
* Shown while a report loader is running. The first cold request fetches the
5+
* full participants project from Scoutnet (~15s); after that the cache serves
6+
* instantly, so this is mostly seen once per server lifetime.
7+
*/
8+
export function PendingReport() {
9+
return (
10+
<Box
11+
display="flex"
12+
flexDirection="column"
13+
alignItems="center"
14+
justifyContent="center"
15+
gap={2}
16+
minHeight="40vh"
17+
>
18+
<CircularProgress />
19+
<Typography variant="body2" color="text.secondary">
20+
Hämtar data från Scoutnet…
21+
</Typography>
22+
</Box>
23+
);
24+
}

0 commit comments

Comments
 (0)