# Worker Management Expansion — Design & Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. REQUIRED SUB-SKILL: Use superpowers:test-driven-development where tests are called for.

**Goal:** Extend the Worker model with certifications + disciplinary records, add a friendly in-app worker-edit form (replacing the need to use Django admin for common edits), and build a batch worker report showing projects, teams, days worked, and first/last payslip dates.

**Architecture:**
- Two new models (`WorkerCertificate`, `WorkerWarning`) with ForeignKey to existing `Worker`.
- A worker list page + worker edit page, both admin-only, replacing most everyday admin needs.
- A batch-report page (HTML + CSV + PDF) summarising each worker's full work history.
- All new work lives inside the existing `core/` app — no new apps. Reuses existing patterns: `is_admin()` gating, `@login_required`, Bootstrap modals, `render_to_pdf()`, inline formsets.

**Tech Stack:** Django 5.2.7, Python 3.13, SQLite (local) / MySQL (prod), Bootstrap 5, WeasyPrint for PDFs, existing `format_tags.money` filter.

---

## Context

**Why this is being built:**
The owner (Konrad) currently manages workers via `/admin/core/worker/` — which is functional but lacks:
- Certification tracking (Skills, PDP, First Aid, Medical, Work at Height) with expiry dates and document uploads
- A disciplinary/warnings record per worker
- A friendlier edit UI than Django admin's default form
- A consolidated worker-history report (projects worked, days, payslips) for review or regulatory/auditor purposes

This plan closes all four gaps in one coherent feature.

**Outcomes when complete:**
1. Per worker, the admin can see and maintain: all 5 cert types, all warnings, in one page.
2. The admin has a worker list and worker edit page that are easier than Django admin.
3. The admin can generate a batch report of all workers' project/team/day/payslip history — viewable, CSV-exportable, PDF-exportable.

---

## Scope (explicit)

### In scope
- New models: `WorkerCertificate`, `WorkerWarning`
- Django admin registrations for both (inline on Worker)
- Worker list page (admin-only): `/workers/`
- Worker edit page (admin-only): `/workers/<id>/edit/` and `/workers/new/`
- Worker detail page (admin-only, read-only view with history): `/workers/<id>/`
- Batch worker report: `/workers/report/` (HTML), `/workers/report/csv/`, `/workers/report/pdf/`
- Nav links added to base.html
- Migrations for the two new models
- Updates to `CLAUDE.md` documenting the new pieces

### Out of scope (not in this plan — can be follow-ups)
- Cert expiry email alerts
- Worker self-service portal (only admins use this)
- Photo cropping / file optimisation
- Bulk cert upload (edit one worker at a time)

---

## Files to Create / Modify

| File | Action | Purpose |
|------|--------|---------|
| `core/models.py` | Modify | Add `WorkerCertificate` and `WorkerWarning` model classes |
| `core/migrations/0009_worker_certificates_warnings.py` | Create (auto) | Schema changes |
| `core/admin.py` | Modify | Register new models; add inlines to WorkerAdmin |
| `core/forms.py` | Modify | `WorkerForm`, `WorkerCertificateFormSet`, `WorkerWarningFormSet` |
| `core/views.py` | Modify | 6 new views: worker_list, worker_detail, worker_edit, worker_batch_report, worker_batch_report_csv, worker_batch_report_pdf |
| `core/urls.py` | Modify | 6 new routes |
| `core/templates/core/workers/list.html` | Create | Worker list with search + filters + "Add Worker" button |
| `core/templates/core/workers/edit.html` | Create | Section-based edit form with inline certs + warnings |
| `core/templates/core/workers/detail.html` | Create | Read-only worker profile with history tabs |
| `core/templates/core/workers/batch_report.html` | Create | HTML batch report (extends base.html) |
| `core/templates/core/pdf/workers_report_pdf.html` | Create | PDF version of batch report (uses WeasyPrint) |
| `core/templates/base.html` | Modify | Add "Workers" nav link (admin only) |
| `CLAUDE.md` | Modify | Document new models, URLs, admin patterns |

---

## Data Model

### `WorkerCertificate`

```python
class WorkerCertificate(models.Model):
    """A certification held by a worker (Skills, PDP, First Aid, etc.).

    One row per (worker, cert_type) — existence of the row means the
    worker currently holds this certification. Delete the row to record
    that they no longer hold it. Use `valid_until` to track expiry.
    """
    CERT_TYPES = [
        ('skills', 'Skills Certificate'),
        ('pdp', 'PDP (Professional Driving Permit)'),
        ('first_aid', 'First Aid'),
        ('medical', 'Medical'),
        ('work_at_height', 'Work at Height'),
    ]

    worker = models.ForeignKey(
        Worker, related_name='certificates', on_delete=models.CASCADE,
    )
    cert_type = models.CharField(max_length=30, choices=CERT_TYPES)
    document = models.FileField(
        upload_to='workers/certificates/', blank=True, null=True,
        help_text='Scan or photo of the certificate',
    )
    issued_date = models.DateField(blank=True, null=True)
    valid_until = models.DateField(
        blank=True, null=True,
        help_text='Expiry date — leave blank if the cert does not expire',
    )
    notes = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        unique_together = [('worker', 'cert_type')]
        ordering = ['worker', 'cert_type']

    def __str__(self):
        return f'{self.worker.name} — {self.get_cert_type_display()}'

    @property
    def is_expired(self):
        if not self.valid_until:
            return False
        return self.valid_until < timezone.now().date()

    @property
    def expires_soon(self):
        """True if the cert expires within the next 30 days."""
        if not self.valid_until:
            return False
        today = timezone.now().date()
        return today <= self.valid_until <= today + datetime.timedelta(days=30)
```

### `WorkerWarning`

```python
class WorkerWarning(models.Model):
    """A disciplinary warning issued to a worker."""
    SEVERITY_CHOICES = [
        ('verbal', 'Verbal Warning'),
        ('written', 'Written Warning'),
        ('final', 'Final Warning'),
    ]

    worker = models.ForeignKey(
        Worker, related_name='warnings', on_delete=models.CASCADE,
    )
    date = models.DateField(default=timezone.now)
    severity = models.CharField(max_length=20, choices=SEVERITY_CHOICES)
    reason = models.CharField(
        max_length=200,
        help_text='Short summary — e.g. "Repeated lateness"',
    )
    description = models.TextField(
        blank=True,
        help_text='Full context of what happened',
    )
    issued_by = models.ForeignKey(
        User, on_delete=models.SET_NULL, null=True, blank=True,
        related_name='warnings_issued',
    )
    document = models.FileField(
        upload_to='workers/warnings/', blank=True, null=True,
        help_text='Signed warning form (optional)',
    )
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['-date']

    def __str__(self):
        return f'{self.worker.name} — {self.get_severity_display()} ({self.date})'
```

### Migration

`python manage.py makemigrations core` → produces `0009_worker_certificates_warnings.py`.
Safe migration: two new tables, no changes to existing ones. Reversible.

---

## URL Routes (new)

Add to `core/urls.py` before the `# === EXPENSE RECEIPTS ===` section:

```python
# === WORKERS ===
# Admin-friendly worker management UI (alternative to /admin/core/worker/)
path('workers/', views.worker_list, name='worker_list'),
path('workers/new/', views.worker_edit, name='worker_new'),
path('workers/<int:worker_id>/', views.worker_detail, name='worker_detail'),
path('workers/<int:worker_id>/edit/', views.worker_edit, name='worker_edit'),
# Batch report (table of all workers with aggregated history)
path('workers/report/', views.worker_batch_report, name='worker_batch_report'),
path('workers/report/csv/', views.worker_batch_report_csv, name='worker_batch_report_csv'),
path('workers/report/pdf/', views.worker_batch_report_pdf, name='worker_batch_report_pdf'),
```

---

## Views (new)

All admin-gated via `@login_required` + `is_admin(request.user)` → 403 if not admin.

### `worker_list(request)`
- Fetches all Workers (not just active).
- Filters: `?q=search_term`, `?status=active|inactive|all`.
- Template: `core/workers/list.html`.
- Shows table: name, ID, phone, salary, days worked, active toggle.

### `worker_detail(request, worker_id)`
- Worker profile (read-only).
- Shows: personal info, PPE, license, certs (with expiry highlights), warnings, and a "History" tab with projects/teams/days/payslips.
- Template: `core/workers/detail.html`.

### `worker_edit(request, worker_id=None)`
- GET: renders form pre-filled (or blank if `worker_id is None`).
- POST: validates + saves Worker + inline formsets for certs + warnings.
- Redirect on success: → `worker_detail`.
- Template: `core/workers/edit.html`.

### `worker_batch_report(request)`
- Builds per-worker aggregates using a shared `_build_worker_report_context()` helper (parallel to `_build_report_context`).
- Filters: `?status=`, `?project=`, `?team=`.
- Template: `core/workers/batch_report.html`.

### `worker_batch_report_csv(request)`
- Same context builder; streams a CSV with all columns.

### `worker_batch_report_pdf(request)`
- Same context builder; uses `render_to_pdf('core/pdf/workers_report_pdf.html', context)`.

### Shared helper `_build_worker_report_context(status=None, project_id=None, team_id=None)`
Returns a list of dicts, one per worker:
```python
{
    'worker': worker_obj,
    'projects': ['Solar Farm Alpha', 'Solar Farm Beta'],  # distinct project names
    'teams': ['Team Alpha'],                               # distinct team names
    'days_worked': 47,                                     # distinct WorkLog dates
    'first_payslip_date': date(2025, 3, 14) or None,
    'last_payslip_date': date(2026, 4, 5) or None,
    'total_paid_lifetime': Decimal('127450.00'),
    'payslip_count': 12,
    'active_certs': 3,
    'expiring_certs': 1,           # expires within 30 days
    'expired_certs': 0,
    'active_warnings_count': 1,    # warnings issued in last 12 months
}
```

Aggregation approach (efficient — one query per aggregate, not per worker):
```python
qs = Worker.objects.annotate(
    days_worked=Count('work_logs__date', distinct=True),
    first_payslip_date=Min('payroll_records__date'),
    last_payslip_date=Max('payroll_records__date'),
    total_paid_lifetime=Sum('payroll_records__amount_paid'),
    payslip_count=Count('payroll_records', distinct=True),
)
# then a separate prefetch for projects/teams
```

---

## Forms (new)

```python
# core/forms.py

class WorkerForm(forms.ModelForm):
    """Main worker edit form — covers the flat fields on Worker."""
    class Meta:
        model = Worker
        fields = [
            'name', 'id_number', 'phone_number', 'monthly_salary',
            'employment_date', 'active', 'notes',
            'shoe_size', 'overall_top_size', 'pants_size', 'tshirt_size',
            'photo', 'id_document',
            'has_drivers_license', 'drivers_license',
        ]
        widgets = {
            'employment_date': forms.DateInput(attrs={'type': 'date'}),
            'notes': forms.Textarea(attrs={'rows': 3}),
        }


WorkerCertificateFormSet = inlineformset_factory(
    Worker, WorkerCertificate,
    fields=['cert_type', 'document', 'issued_date', 'valid_until', 'notes'],
    extra=0,              # no blank rows by default
    can_delete=True,
    widgets={
        'issued_date': forms.DateInput(attrs={'type': 'date'}),
        'valid_until': forms.DateInput(attrs={'type': 'date'}),
        'notes': forms.Textarea(attrs={'rows': 2}),
    },
)

WorkerWarningFormSet = inlineformset_factory(
    Worker, WorkerWarning,
    fields=['date', 'severity', 'reason', 'description', 'document'],
    extra=0,
    can_delete=True,
    widgets={
        'date': forms.DateInput(attrs={'type': 'date'}),
        'description': forms.Textarea(attrs={'rows': 3}),
    },
)
```

In the edit view, `request.POST`/`request.FILES` flow through all three (form + two formsets); all must be valid before saving.

---

## Templates (new)

### `core/workers/list.html`
- Search box (name/ID) + status filter dropdown
- Table columns: Name, ID, Phone, Salary, Days Worked, Active, Actions (View / Edit / Toggle)
- Buttons: "Add Worker", "Batch Report", "Export CSV"
- Styled with existing stat-card / resource-row patterns from index.html

### `core/workers/edit.html`
- Section-based layout (no tabs — long-form scroll for easier visual review):
  1. **Personal & Pay** — name, id_number, phone, salary, employment_date, active, notes
  2. **PPE Sizing** — shoe, overall top, pants, t-shirt
  3. **Documents** — photo, id_document
  4. **Driver's License** — has_drivers_license, drivers_license file
  5. **Certifications** (formset with + add button, × delete)
  6. **Warnings & Disciplinary** (formset with + add button, × delete)
- Client-side JS: "Add Certification" / "Add Warning" buttons clone a hidden blank formset row and bump the TOTAL_FORMS counter (standard Django formset pattern)
- Submit button at the bottom; Cancel goes back to `worker_detail`

### `core/workers/detail.html`
- Header: worker photo, name, ID, active badge
- Tabs:
  1. **Profile** — personal, PPE, license info
  2. **Certifications** — list with colored badges: green (valid > 30 days), amber (expires within 30), red (expired)
  3. **Warnings** — chronological list
  4. **History** — projects worked, teams, days, last 10 payslips
- "Edit" button links to `worker_edit`

### `core/workers/batch_report.html`
- Report header + filter bar (status / project / team)
- Table with columns:
  - Name | ID | Salary | Active | Days Worked | Projects | Teams | First Payslip | Last Payslip | Total Paid | Certs (n/m) | Warnings
- "Export CSV" + "Download PDF" buttons at top-right
- Row click → `worker_detail`

### `core/pdf/workers_report_pdf.html`
- Print-optimized A4 layout using WeasyPrint
- Header: "FoxFitt Construction — Worker Roster Report"
- Filter summary subhead
- Table (narrower columns, landscape orientation may be needed for many fields)
- Uses the same amber accent and typography as `report_pdf.html`

---

## Navigation

`base.html` desktop topbar: add a "Workers" link after "Receipts" and before "Admin" (admin-only):

```html
{% if user.is_staff %}
<a href="{% url 'worker_list' %}" class="topbar-nav__link {% if 'worker' in request.resolver_match.url_name %}active{% endif %}">
    <i class="fas fa-hard-hat"></i><span>Workers</span>
</a>
{% endif %}
```

Also add matching entries to the mobile menu (the `.mobile-menu__nav` block) and the bottom tab bar (if room).

---

## Django Admin Enhancements

Register the new models:

```python
# core/admin.py

class WorkerCertificateInline(admin.TabularInline):
    model = WorkerCertificate
    extra = 0

class WorkerWarningInline(admin.TabularInline):
    model = WorkerWarning
    extra = 0
    readonly_fields = ['created_at']

@admin.register(WorkerCertificate)
class WorkerCertificateAdmin(admin.ModelAdmin):
    list_display = ('worker', 'cert_type', 'valid_until', 'is_expired')
    list_filter = ('cert_type',)
    search_fields = ('worker__name',)

@admin.register(WorkerWarning)
class WorkerWarningAdmin(admin.ModelAdmin):
    list_display = ('worker', 'date', 'severity', 'reason')
    list_filter = ('severity',)
    search_fields = ('worker__name', 'reason')
```

Then update `WorkerAdmin` to include the inlines:

```python
class WorkerAdmin(admin.ModelAdmin):
    # ...existing config...
    inlines = [WorkerCertificateInline, WorkerWarningInline]
```

This means the Django admin ALSO gets the new sections — the in-app edit page is a better UX, but admin remains fully functional.

---

## Task-by-Task Execution Plan

Each task is 5–15 minutes. Execute in order.

### Task 1: Add the two new models + migration

**Files:**
- Modify: `core/models.py` (append `WorkerCertificate`, `WorkerWarning` classes at end of file)
- Create: `core/migrations/0009_worker_certificates_warnings.py` (auto-generated)

**Steps:**
1. Add the two model classes (code above)
2. `USE_SQLITE=true python manage.py makemigrations core`
3. `USE_SQLITE=true python manage.py migrate`
4. Verify: `python manage.py check` → no issues
5. Commit

### Task 2: Register models in Django admin

**Files:**
- Modify: `core/admin.py`

**Steps:**
1. Add `WorkerCertificateInline`, `WorkerWarningInline`, `WorkerCertificateAdmin`, `WorkerWarningAdmin` classes
2. Add `inlines = [...]` to `WorkerAdmin`
3. Verify in browser: `/admin/core/worker/<id>/change/` shows certs + warnings sections
4. Commit

### Task 3: Add WorkerForm and formsets

**Files:**
- Modify: `core/forms.py`

**Steps:**
1. Add `WorkerForm`, `WorkerCertificateFormSet`, `WorkerWarningFormSet`
2. Verify: `python manage.py shell` → import forms → no errors
3. Commit

### Task 4: Add worker_list view and template

**Files:**
- Modify: `core/views.py` (+ `import` updates)
- Modify: `core/urls.py`
- Create: `core/templates/core/workers/list.html`

**Steps:**
1. Add URL route `workers/` → `views.worker_list`
2. Add view `worker_list(request)` with search + status filter
3. Create template with search bar, table, action buttons
4. Verify: visit `/workers/` as admin → list shows workers
5. Commit

### Task 5: Add worker_edit view + template (the big one)

**Files:**
- Modify: `core/views.py`
- Modify: `core/urls.py`
- Create: `core/templates/core/workers/edit.html`

**Steps:**
1. Add two URL routes: `workers/new/`, `workers/<id>/edit/`
2. Add view `worker_edit(request, worker_id=None)` handling both create and update
3. Create section-based template with formsets for certs + warnings
4. Add JS for "+ Add Certification" / "+ Add Warning" buttons (formset clone pattern)
5. Verify: add a worker, edit a worker, add cert, add warning → all persist
6. Commit

### Task 6: Add worker_detail view + template

**Files:**
- Modify: `core/views.py`
- Modify: `core/urls.py`
- Create: `core/templates/core/workers/detail.html`

**Steps:**
1. Add URL `workers/<id>/`
2. Add view with tab-context (profile, certs, warnings, history)
3. Template with Bootstrap tabs; cert badges styled by expiry
4. Link "Edit" button to edit view
5. Verify
6. Commit

### Task 7: Add batch report (HTML, CSV, PDF)

**Files:**
- Modify: `core/views.py` (add `_build_worker_report_context`, 3 views)
- Modify: `core/urls.py`
- Create: `core/templates/core/workers/batch_report.html`
- Create: `core/templates/core/pdf/workers_report_pdf.html`

**Steps:**
1. Write `_build_worker_report_context()` helper with the annotate/prefetch pattern
2. Add 3 URLs + 3 views
3. Create HTML template with filter bar + table
4. Create PDF template (derive from existing `report_pdf.html` structure — cover, section headings, ledger-style table)
5. Verify HTML, CSV, PDF all render correctly
6. Commit

### Task 8: Add nav links

**Files:**
- Modify: `core/templates/base.html`

**Steps:**
1. Add desktop nav link (admin-only)
2. Add mobile menu link
3. Verify on desktop and mobile layouts
4. Commit

### Task 9: Update CLAUDE.md

**Files:**
- Modify: `CLAUDE.md`

**Steps:**
1. Add `WorkerCertificate` and `WorkerWarning` to Key Models section
2. Add 7 new URL routes to URL Routes table
3. Add "Worker Management" subsection under Development Workflow
4. Commit

### Task 10: Verification pass

- Visit every new page; click every button; upload a test file to each upload field
- Try edge cases: blank forms, duplicate cert_type for same worker (should fail unique constraint), expired certs
- Regenerate PDF; open to verify layout
- Run `python manage.py check`
- Smoke-test existing features (payroll report, payment, receipt) still work

---

## Open Questions for User

The following decisions need your input before I start executing. Defaults in brackets are what I'll use if you don't answer.

### Q1: Certification types list
I'm proposing these 5 types:
- Skills Certificate
- PDP (Professional Driving Permit)
- First Aid
- Medical
- Work at Height

**Any additions, removals, or renames?** [Default: use exactly these 5]

### Q2: Warning severity levels
I'm proposing: Verbal → Written → Final. **Accept, or add a fourth (e.g. "Informal"), or use different names?** [Default: Verbal/Written/Final]

### Q3: Navigation placement
**Where does the "Workers" link go?**
- (a) Top desktop nav, admin-only, after Receipts — prominent, 1-click access
- (b) Only accessible from a button on the Dashboard — less cluttered nav
- (c) Inside the Admin dropdown / submenu

[Default: (a) — matches how Payroll is currently linked]

### Q4: Worker list — default sort?
- (a) Alphabetical by name
- (b) By employment date (newest first)
- (c) By active status, then name

[Default: (a) alphabetical]

### Q5: Coexist with Django admin?
The new worker list/edit pages would be more user-friendly, but Django admin at `/admin/core/worker/` remains fully functional.
**Keep Django admin working as a fallback for power-user edits?** [Default: yes — keep both]

### Q6: Cert expiry alerts on dashboard
Would you like the Dashboard to show a stat card like "3 certs expiring in 30 days"?
- (a) Yes, show it — helpful operationally
- (b) No, keep dashboard unchanged for now
- (c) Show, but only if count > 0 (hide the card when everything's fine)

[Default: (c) conditional display]

### Q7: File upload size limit
Certificates and warnings support file uploads. Currently no limit. Should I add a max size to prevent someone accidentally uploading a 50MB scan?
- (a) No limit
- (b) 5 MB max
- (c) 10 MB max

[Default: (b) 5 MB]

### Q8: Batch report columns
I'm proposing this column set:
Name | ID | Salary | Active | Days Worked | Projects | Teams | First Payslip | Last Payslip | Total Paid | Certs (active/total) | Warnings

**Anything to add or remove?** Common additions could be: employment_date, phone, has_drivers_license, last-active-date.

[Default: use the list above]

### Q9: Scope bailout
If I discover during execution that any task is significantly more complex than the estimate (e.g. Task 5 turns out to need 2 hours instead of 30 minutes), should I:
- (a) Keep going, add a new task, push complete
- (b) Stop, flag the issue, let you decide
- (c) Deliver a smaller version (e.g. certs-only, no warnings) and flag warnings as follow-up

[Default: (b) stop and flag]

---

## Verification (end-to-end)

Run after all tasks complete:

```
USE_SQLITE=true python manage.py check
USE_SQLITE=true python manage.py migrate --plan  # confirm no pending migrations
```

Browser smoke tests (as admin):
1. Visit `/workers/` — list renders, search works
2. Click "Add Worker" — blank form loads
3. Fill name/ID/salary, click Save → redirected to detail view
4. Click "Edit" on detail — form pre-filled
5. Click "+ Add Certification" → blank cert row appears
6. Fill cert (Medical, valid_until=next month), upload a test PDF, Save
7. Verify cert appears on detail page with green "valid" badge
8. Click "+ Add Warning", fill, Save — verify it appears on Warnings tab
9. Visit `/workers/report/` — table shows all workers with aggregates
10. Click "Export CSV" — downloads, opens in Excel/LibreOffice cleanly
11. Click "Download PDF" — renders with correct layout
12. Visit `/admin/core/worker/<id>/change/` — Django admin still works, inlines show
13. As a non-admin user, visit `/workers/` — should get 403

## Rollback

The change touches two new models with ForeignKey to Worker. If we need to undo:
1. `git reset --hard` to the last commit before this plan started
2. `python manage.py migrate core 0008` (the pre-existing migration) — drops the two new tables
3. Branches stay isolated until merged; worst case the whole thing sits on a local branch forever

---

## Estimated Timeline

- Task 1 (models + migration): 10 min
- Task 2 (admin): 5 min
- Task 3 (forms): 10 min
- Task 4 (list view): 15 min
- Task 5 (edit view + template): 40 min — biggest task; has formset JS
- Task 6 (detail view): 20 min
- Task 7 (batch report): 30 min
- Task 8 (nav): 5 min
- Task 9 (CLAUDE.md): 5 min
- Task 10 (verification): 15 min

**Total: ~2.5 hours of supervised execution**, or ~90 minutes if I can auto-execute with good test coverage.
