Certifications
Track operational certifications (Hygieniapassi, First Aid, ride operator training, etc.) per employee, with full historical fidelity. The capability covers digitizing the records, surfacing them in the UI, ad-hoc reporting, and automated renewal reminders. Roster-time gating and PDF uploads remain out of scope and will land in later phases.
Roles
| Role | What they can do |
|---|---|
| Employee | (Phase 1) cannot view their own certifications. |
| Supervisor | Read certifications for employees in their assigned venues. |
| Manager / Admin | Read tenant-wide; create/edit types and records; run reports. |
Read access for Supervisors is location-scoped through the same UserScopeService used elsewhere — they can see records only for employees they share at least one venue assignment with.
Certification types
Set up certification types in Settings → Certifications (Manager+). A type has:
- Name (e.g., "Hygieniapassi", "First Aid")
- Category (
safety/food/first_aid/other) - Requires renewal — when checked, you can also set:
- Default validity (months) — pre-fills the expires-at field when adding a new record. Optional even when renewals are required (some types have a custom cadence).
Categories drive UI grouping and are displayed as badges. They have no business logic attached.
Adding a certification to an employee
- Open the employee detail page (
Employees → <employee>) - In the Certifications card, click Add certification
- Pick the type, set issued at and (optionally) expires at, fill in issued by and notes if relevant
- Save
If the type carries a default validity, the expires at field is pre-filled when you set the issue date.
Renewals
The system never modifies an existing record's issued_at or expires_at. A renewal is a new row. The historical record stays in place forever so that audit questions like "was X certified on date Y?" remain answerable.
You can correct the issued by and notes on an existing row via the inline edit affordance. Anything else requires a new record.
Currently-certified status
The system computes status at query time, not as a stored flag:
active → today between issued_at and expires_at (inclusive),
or expires_at is null
expiring_30 → expires_at is within the next 30 calendar days
expired → expires_at is in the past
All date comparisons evaluate against today's calendar date in Europe/Helsinki.
Reports
Manager+ can run an ad-hoc report at Reports → Certifications. Filters:
- Type — single certification type
- Status — active / expiring within 30 days / expired
- Location / Department — narrows by employee assignments
Each (employee × type) pair collapses to its latest record. Older renewal rows do not appear as separate report rows — they're available on the employee detail page if needed.
The Export CSV button downloads the current report. The CSV includes a UTF-8 BOM so Excel reads Finnish characters (ä / ö / å) correctly.
Dashboard widget
Manager+ sees an "Expiring in 30 days" widget on the dashboard that links into the full report. The heading and "View full report" link both deep-link to the report with status=expiring_30 pre-selected.
Renewal reminders
For renewal-required certification types, the system automatically dispatches notifications on two days:
- 30 days before expiry —
CertificationExpiringSoonnotification fires. - The day of expiry itself —
CertificationExpiredfollow-up notification fires.
Recipients are the affected employee plus every Manager and CompanyAdmin in the tenant. Supervisors are not included by default — Managers are typically the ones who book renewal training, so they get the action handle. The reminders are dispatched via push and email per the standard notification channels; clicking through opens the certifications report.
A few details worth knowing:
- Idempotent. Each
(employee × certification × reminder type)pair fires exactly once. Re-running the cron, restarting the worker, or running concurrent workers does not double-send. A small ledger (certification_reminders_sent) backs this guarantee at the database level. - Open-ended certs are skipped. A certification with
expires_atleft blank (the "valid indefinitely" case) never triggers reminders. - One-shot types are skipped. A certification type with
requires_renewal = false(e.g., onboarding training) never triggers reminders. - Renewals reset the loop. When you record a renewal as a new certification row, that new row gets its own ledger entries — no manual cancellation of the old reminders is needed.
- Cron schedule. The sweep runs daily at 09:00 Europe/Helsinki. If a worker is down on the qualifying day, that one reminder is missed (the dashboard widget and report still surface the cert in time).
Audit log
Every write — certification_type.created, certification_type.updated, employee_certification.created, employee_certification.updated — is recorded via the existing AuditService.
Tenant isolation
Both certification_types and employee_certifications enforce tenant isolation via PostgreSQL FORCE ROW LEVEL SECURITY. Queries from one tenant never return another tenant's rows, even if the application code forgets to filter.