From 5c8508171a56fe571e040fd84ef61d2fb309c02f Mon Sep 17 00:00:00 2001 From: Konrad du Plessis Date: Wed, 22 Apr 2026 02:30:25 +0200 Subject: [PATCH] Add Teams & Projects management pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the app with friendly form-based management for Teams and Projects — an alternative to using Django admin for routine maintenance. New URLs (admin-only, all return 403 for non-admins): - /teams/ · /teams/new/ · /teams// · /teams//edit/ - /teams/report/ · /teams/report/csv/ - /projects/ + same 5 variants Forms (core/forms.py): - TeamForm — ModelForm with pay-schedule validation (both or neither field) - ProjectForm — ModelForm with end_date >= start_date validation - _supervisor_user_queryset() — admins + Work Logger group members Views (core/views.py): - 10 new views (5 per model: list, detail, edit, batch_report, batch_csv) - _build_team_report_context() / _build_project_report_context() shared helpers - All views gate on is_admin(user) - Reuses existing get_pay_period() for Team detail Pay Schedule tab Templates (core/templates/core/teams/ and projects/): - list.html — filterable table with search - detail.html — tabbed profile / workers / history / schedule - edit.html — serves both /new/ and /edit/ - batch_report.html — lifetime aggregates per row, CSV download UI integration: - Resources dropdown added to top nav (admin-only, Teams + Projects) - Manage All buttons added to Dashboard Manage Resources tabs (Teams, Projects) No model changes, no migrations — purely additive. CLAUDE.md updated with new routes and section describing the pattern. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 35 ++ core/forms.py | 139 ++++++ core/templates/base.html | 24 + core/templates/core/index.html | 10 + .../templates/core/projects/batch_report.html | 105 +++++ core/templates/core/projects/detail.html | 319 +++++++++++++ core/templates/core/projects/edit.html | 150 ++++++ core/templates/core/projects/list.html | 141 ++++++ core/templates/core/teams/batch_report.html | 115 +++++ core/templates/core/teams/detail.html | 296 ++++++++++++ core/templates/core/teams/edit.html | 152 ++++++ core/templates/core/teams/list.html | 131 ++++++ core/urls.py | 19 + core/views.py | 433 +++++++++++++++++- 14 files changed, 2068 insertions(+), 1 deletion(-) create mode 100644 core/templates/core/projects/batch_report.html create mode 100644 core/templates/core/projects/detail.html create mode 100644 core/templates/core/projects/edit.html create mode 100644 core/templates/core/projects/list.html create mode 100644 core/templates/core/teams/batch_report.html create mode 100644 core/templates/core/teams/detail.html create mode 100644 core/templates/core/teams/edit.html create mode 100644 core/templates/core/teams/list.html diff --git a/CLAUDE.md b/CLAUDE.md index a4ba0f3..0419504 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -134,6 +134,41 @@ python manage.py check # System check | `/payroll/batch-pay/preview/` | `batch_pay_preview` | Admin: AJAX JSON batch pay preview (`?mode=schedule\|all`) | | `/payroll/batch-pay/` | `batch_pay` | Admin: POST process batch payments for multiple workers | | `/run-migrate/` | `run_migrate` | Setup: run pending DB migrations from browser | +| `/teams/` | `team_list` | Admin: list teams with filters & search | +| `/teams/new/` | `team_edit` | Admin: create a new team | +| `/teams//` | `team_detail` | Admin: team detail (Profile · Pay Schedule · Workers · History) | +| `/teams//edit/` | `team_edit` | Admin: edit team (shared view with `team_new`) | +| `/teams/report/` | `team_batch_report` | Admin: batch report across all teams (HTML) | +| `/teams/report/csv/` | `team_batch_report_csv` | Admin: batch report CSV download | +| `/projects/` | `project_list` | Admin: list projects with filters & search | +| `/projects/new/` | `project_edit` | Admin: create a new project | +| `/projects//` | `project_detail` | Admin: project detail (Profile · Supervisors · Teams · Workers · History) | +| `/projects//edit/` | `project_edit` | Admin: edit project (shared view with `project_new`) | +| `/projects/report/` | `project_batch_report` | Admin: batch report across all projects (HTML) | +| `/projects/report/csv/` | `project_batch_report_csv` | Admin: batch report CSV download | + +## Team & Project Management Pages +Added as a friendly alternative to Django admin for managing Teams and Projects +outside of `/admin/`. Pattern mirrors (or anticipates) the Workers management UI. + +- **Access**: Admins only — every view checks `is_admin(user)` and returns 403 for others +- **Entry points**: + - `Resources` dropdown in the top nav (admin-only) — links to Teams and Projects + - `Manage All Teams` / `Manage All Projects` buttons on the Dashboard's Manage Resources card tabs +- **Forms**: `TeamForm` and `ProjectForm` in `core/forms.py` — plain `ModelForm` classes, + no inline formsets. `_supervisor_user_queryset()` returns admins + Work Logger group members + so both forms pick supervisors from the same pool +- **Helpers in views.py**: + - `_build_team_report_context(request)` — shared between HTML and CSV batch-report views + - `_build_project_report_context(request)` — same pattern for projects + - Reuses existing `get_pay_period(team)` for the Team detail "Pay Schedule" tab +- **Templates**: `core/templates/core/teams/{list,detail,edit,batch_report}.html` + and `core/templates/core/projects/{list,detail,edit,batch_report}.html` +- **No model changes / no migrations** — these pages are purely additive +- **PDF export deferred** — HTML + CSV only for now; PDF can be added with a new + view + template per model if needed later +- **Django admin still works** for all of these models — the new pages are an + alternative UI, not a replacement ## Frontend Design Conventions - **CSS variables** in `static/css/custom.css` `:root` — always use `var(--name)`: diff --git a/core/forms.py b/core/forms.py index 14702d3..a5f9016 100644 --- a/core/forms.py +++ b/core/forms.py @@ -6,9 +6,21 @@ from django import forms from django.forms import inlineformset_factory +from django.contrib.auth.models import User +from django.db.models import Q from .models import WorkLog, Project, Team, Worker, PayrollAdjustment, ExpenseReceipt, ExpenseLineItem +# === HELPER: who can be a supervisor? === +# The app's business rule: a "supervisor" is either an admin (is_staff) +# or a member of the "Work Logger" group. We reuse this queryset in both +# TeamForm (single supervisor) and ProjectForm (multiple supervisors). +def _supervisor_user_queryset(): + return User.objects.filter( + Q(is_staff=True) | Q(groups__name='Work Logger') + ).distinct().order_by('username') + + class AttendanceLogForm(forms.ModelForm): """ Form for logging daily worker attendance. @@ -213,3 +225,130 @@ ExpenseLineItemFormSet = inlineformset_factory( }), } ) + + +# ============================================================================= +# === TEAM FORM === +# Used on /teams/new/ and /teams//edit/ to create or edit a Team. +# Mirrors the Django admin experience outside of /admin/ so the owner +# doesn't need to go into Django admin for routine team maintenance. +# ============================================================================= + +class TeamForm(forms.ModelForm): + """ + Form for creating/editing a Team. + + Fields: + - name: team name + - supervisor: a single User (filtered to admins + Work Logger group) + - active: whether the team is currently in use + - pay_frequency: optional — weekly / fortnightly / monthly + - pay_start_date: anchor date for the first pay period + - workers: checkbox list of ALL workers (active + inactive) — + inactive ones are flagged with a badge in the template + """ + + class Meta: + model = Team + fields = ['name', 'supervisor', 'active', 'pay_frequency', + 'pay_start_date', 'workers'] + widgets = { + 'name': forms.TextInput(attrs={ + 'class': 'form-control', + 'placeholder': 'Team name (e.g. "Footings Crew")' + }), + 'supervisor': forms.Select(attrs={'class': 'form-select'}), + 'active': forms.CheckboxInput(attrs={'class': 'form-check-input'}), + 'pay_frequency': forms.Select(attrs={'class': 'form-select'}), + 'pay_start_date': forms.DateInput(attrs={ + 'type': 'date', 'class': 'form-control' + }), + # Multi-select for workers — rendered as a checkbox grid in the template + 'workers': forms.CheckboxSelectMultiple(attrs={'class': 'form-check-input'}), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Supervisor dropdown = admins + Work Logger group, alphabetical + self.fields['supervisor'].queryset = _supervisor_user_queryset() + self.fields['supervisor'].required = False + # Workers picker: ALL workers — inactive marked in template + self.fields['workers'].queryset = Worker.objects.all().order_by('name') + self.fields['workers'].required = False + # Schedule fields are optional + self.fields['pay_frequency'].required = False + self.fields['pay_start_date'].required = False + + def clean(self): + """If pay_frequency is set, pay_start_date must also be set (and vice versa).""" + cleaned = super().clean() + freq = cleaned.get('pay_frequency') + start = cleaned.get('pay_start_date') + if freq and not start: + self.add_error('pay_start_date', + 'A start date is required when pay frequency is set.') + if start and not freq: + self.add_error('pay_frequency', + 'Choose a pay frequency when setting a start date.') + return cleaned + + +# ============================================================================= +# === PROJECT FORM === +# Used on /projects/new/ and /projects//edit/ to create or edit a Project. +# ============================================================================= + +class ProjectForm(forms.ModelForm): + """ + Form for creating/editing a Project. + + Fields: + - name: project name (e.g. "Solar Farm — Phase 2") + - description: free-text notes + - active: whether the project is currently running + - start_date / end_date: optional timeline + - supervisors: M2M to User — any number of supervisors may be assigned + """ + + class Meta: + model = Project + fields = ['name', 'description', 'active', 'start_date', 'end_date', + 'supervisors'] + widgets = { + 'name': forms.TextInput(attrs={ + 'class': 'form-control', + 'placeholder': 'Project name' + }), + 'description': forms.Textarea(attrs={ + 'class': 'form-control', + 'rows': 3, + 'placeholder': 'What this project covers...' + }), + 'active': forms.CheckboxInput(attrs={'class': 'form-check-input'}), + 'start_date': forms.DateInput(attrs={ + 'type': 'date', 'class': 'form-control' + }), + 'end_date': forms.DateInput(attrs={ + 'type': 'date', 'class': 'form-control' + }), + # Multi-select checkboxes for supervisors + 'supervisors': forms.CheckboxSelectMultiple(attrs={'class': 'form-check-input'}), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Supervisor dropdown = admins + Work Logger group members + self.fields['supervisors'].queryset = _supervisor_user_queryset() + self.fields['supervisors'].required = False + self.fields['start_date'].required = False + self.fields['end_date'].required = False + self.fields['description'].required = False + + def clean(self): + """If both dates are set, end_date must not be before start_date.""" + cleaned = super().clean() + start = cleaned.get('start_date') + end = cleaned.get('end_date') + if start and end and end < start: + self.add_error('end_date', 'End date cannot be before start date.') + return cleaned diff --git a/core/templates/base.html b/core/templates/base.html index 304e71c..0009da1 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -67,7 +67,31 @@ Receipts + {# === RESOURCES DROPDOWN (admin only) === + Friendly management pages for Teams and Projects — an + alternative to Django admin. Workers will be added here + when the Workers management UI ships in a later release. #} {% if user.is_staff %} +