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 %} +