# M12 — People (HR-lite) & System Admin

Phase 5. Staff records supporting the fee-scale/seniority lookups used since
M07, plus the operational admin surface (backups, audit review, imports)
needed to actually run the system day to day.

## Dependencies
M01 (users, roles), M07 (appearance fee scales reference `seniority`).

## Part A — People (HR-lite)

Deliberately minimal — this is a legal practice management system, not a full
HRIS. Scope stays tight; flag anything larger behind `feature('payroll')`.

### `staff` (extends `users`, one-to-one)
```php
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('staff_number')->unique();   // via NumberingService
$table->string('seniority')->nullable();    // Partner/Senior Counsel/Counsel/Paralegal — feeds M07 fee scales
$table->foreignId('office_id')->nullable()->constrained()->nullOnDelete();
$table->date('date_joined')->nullable();
$table->string('phone')->nullable();
$table->timestamps();
```

### `leave_requests`
```php
$table->id();
$table->foreignId('staff_id')->constrained()->cascadeOnDelete();
$table->date('start_date'); $table->date('end_date');
$table->string('type')->default('annual');  // annual/sick/other
$table->text('reason')->nullable();
$table->string('status')->default('pending'); // pending/approved/rejected
$table->foreignId('approved_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
```
Simple approval flow (request → approve/reject by the responsible partner or
Firm Admin); shows on the roster (M04) as an "unavailable" flag so hearing
assignment surfaces a warning if assigning someone on approved leave.

### Payroll — feature-flagged, off by default (`feature('payroll')`)
If enabled: `salary_structures`, `payroll_runs`, `payslips` — scoped small
(gross/deductions/net, monthly run, PDF payslip) since most firms this size run
payroll externally; build only if confirmed needed, per
`docs/01-PRODUCT-OVERVIEW.md` out-of-scope note re: full HRIS.

## Part B — System Administration

### User & role management UI
Firm Admin screens over M01's RBAC: invite user (email invite, set password on
first login), assign roles, suspend/reactivate, force-logout (invalidate
sessions), 2FA reset (with a logged reason — a sensitive action).

### Audit review
`GET /admin/audit` — searchable view over `activity_log` + the M04 security
events log (login attempts, permission denials, document confidentiality
changes, portal access grants/revocations, data exports). Filters: user, model
type, date range, event category.

### Backups
UI wrapper over `spatie/laravel-backup`: last backup status/size, manual
"run backup now," download list (admin-only, itself audit-logged), monthly
automated **restore-integrity check** result display (per `docs/04-SECURITY.md`
§9). Alert banner if the last successful backup is older than 48 hours.

### Data import wizards
Excel import (via `maatwebsite/excel` import classes + the existing Importable
convention) for: legacy matters, legacy clients, legacy hearing history —
critical for onboarding a firm's existing caseload. Each wizard: download
template → upload → validation preview (row-by-row errors shown before commit)
→ commit inside a transaction → import summary (created/skipped/failed counts).

### System health
`/up` health endpoint (Laravel default) wired to uptime monitoring; an
in-app `/admin/system` page surfacing: queue health (Horizon link/embedded
metrics), failed jobs count with retry action, disk usage, PHP/Laravel version,
scheduled command last-run timestamps (numbering sequences, deadline reminders,
digests, backups) — so a Firm Admin can see at a glance if the automated
plumbing (M04 reminders, M09 digests) is actually running.

## Routes

```
RESTful /staff
RESTful /leave-requests  + PATCH /leave-requests/{id}/status
RESTful /admin/users     (invite/suspend/force-logout/reset-2fa actions)
GET  /admin/audit
GET  /admin/backups  · POST /admin/backups/run
RESTful /admin/import/{type}   (matters/clients/hearings)
GET  /admin/system
```
(Payroll routes only registered when `feature('payroll')` is true.)

## Acceptance criteria

- [ ] Staff record links 1:1 to a user; `seniority` correctly feeds M07's fee
      scale lookups
- [ ] Leave approval flow works; approved leave surfaces as an "unavailable"
      warning on the M04 roster assignment screen
- [ ] User invite/suspend/force-logout/2FA-reset all work and are audit-logged
- [ ] Audit review page filters correctly across activity_log + security
      events
- [ ] Backup status page reflects real backup state; manual run works; stale
      backup alert banner appears past 48 hours
- [ ] Import wizard: validation preview catches bad rows before commit; commit
      is transactional (partial failure leaves no orphaned rows)
- [ ] System health page accurately reflects queue/job/scheduled-command state
- [ ] Payroll module completely absent (routes 404, no menu entry) when
      `feature('payroll')` is false

## Claude Code kickoff prompt

> Read CLAUDE.md and this file, plus M04's roster assignment code (for the
> leave-conflict warning) and M07's appearance fee scale lookup (for
> `seniority`). Implement Part A (staff, leave) → Part B (user admin, audit
> review, backups wrapper, import wizards, system health) in order. Leave
> payroll unbuilt behind the feature flag unless explicitly requested — confirm
> scope before adding it.
