Companion to attendance: capture WHAT was done on site each day,
alongside WHO worked. Optional 1:1 with WorkLog. Mobile-first form
auto-redirected from /attendance/log/ on success (with a Skip link).
Why this design (vs. extending WorkLog or per-project templates):
- Hybrid schema. Stable + queryable fields are real columns
(`weather`, `temperature_min`, `temperature_max`, `notes`,
`created_by`, `created_at`, `updated_at`). The METRICS that change
per project / over time live in a single JSONField with shape
`{counts: {key: int}, checks: {key: bool}}` — driven by
`core/site_report_schema.py`. Adding a new metric is a one-line
edit to that file, NO migration required. Old reports without the
new key just render as 0 / unchecked.
- Two-step flow. Attendance form is unchanged; on successful POST
the supervisor lands on `/site-report/<work_log_id>/edit/` for the
most-recently-created log. They can fill in progress details
(~30 sec on a phone) or click "Skip" to home. WorkLogs without a
SiteReport are completely valid historic rows.
- Permission scope mirrors WorkLog access. Anyone who can see the
parent log (admin / log's supervisor / project's supervisors) can
see + edit its SiteReport. Wraps the existing pattern from
`work_history()` in a small helper `_can_access_site_report()`.
What ships:
Models:
- SiteReport (1:1 → WorkLog, weather choices, IntegerField temps,
JSONField metrics defaulting to {})
- Migration 0013_add_site_report (pure CreateModel, no schema
changes to existing tables)
Schema:
- core/site_report_schema.py (NEW) — single source of truth for
the metric list. Currently 7 counts + 4 checks per Konrad's
v1 spec. Helpers: get_count_keys, get_check_keys, label_for,
empty_metrics.
Form:
- SiteReportForm (in core/forms.py) — ModelForm with the four
stable fields PLUS dynamic IntegerField/BooleanField per
metric in __init__. save() serializes both halves into the
JSON blob. clean() validates min ≤ max temperature.
Views:
- site_report_edit — create-or-update; stamps created_by on
first save; preserves it on subsequent admin edits
- site_report_detail — read-only display; 404 when no report
- attendance_log redirect updated to two-step flow
- _can_access_site_report — shared permission helper
URLs:
- /site-report/<work_log_id>/edit/ (name: site_report_edit)
- /site-report/<work_log_id>/ (name: site_report_detail)
Templates:
- site_report_edit.html — mobile-first stack of inputs, weather
as a chunky icon-button row (☀️ ☁️ 🌧️ ⛈️ 🥵 🥶 💨), counts in a
2-col grid, checks as toggle switches, Notes textarea, Skip
+ Save buttons. Iterates pre-built (metric, bound_field)
pairs from the view to avoid needing a new template filter.
- site_report_detail.html — counts as accent-coloured value
cards, checks as a check-list, weather + temp + notes + edit
link.
- work_history.html — added a small clipboard icon next to
each row's date: filled (linked to detail) when a report
exists, muted outline (linked to edit) when not. Click is
event.stopPropagation()-ed so the row's payroll-modal
handler doesn't also fire.
Performance:
- work_history queryset adds .select_related('site_report') so
the new template indicator doesn't introduce an N+1.
Admin:
- SiteReport registered with raw_id_fields on work_log +
created_by, list filters on weather + project + date.
Tests (16 new, full suite 85/85):
- SiteReportModelTests — defaults, 1:1 reverse accessor,
arbitrary-key JSON round-trip
- SiteReportFormTests — dynamic field generation, save
serialisation, temp validation, instance pre-fill
- SiteReportEditViewTests — admin GET/POST, project
supervisor allowed, outsider supervisor 403, created_by
preserved on subsequent admin edits
- SiteReportDetailViewTests — 404 when absent, displays data
when present
- AttendanceLogRedirectsToSiteReportTests — confirms the
two-step flow
CLAUDE.md updates:
- SiteReport added to "Key Models" with shape + reverse-accessor note
- New "SiteReport metric schema" section near "UI-vs-DB
naming drift" — explains the JSON-column-with-Python-source
pattern, when it's safe, what NOT to do (rename a key with
data), and where the keys appear across the codebase
- URL Routes table gets the two new endpoints
What's NOT in this commit (deferred per the brainstorm plan):
- JournalEntry model + manual web-entry UI (Phase A.2 — depends
on Konrad's Q7 answer about Vi/recipient field)
- Letterly inbound webhook (Phase B — integrations branch only,
depends on Q5 sample payload)
- Photos on site reports (Q9, defaulted to "future")
- Per-project metric templates (Q4, defaulted to "same set for all v1")
Reference plan: ~/.claude/plans/prancy-painting-brook.md (local).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
132 lines
6.8 KiB
Python
132 lines
6.8 KiB
Python
# === URL ROUTING ===
|
|
# Maps URLs to view functions. Each path() connects a web address to
|
|
# the Python function that handles it.
|
|
|
|
from django.urls import path
|
|
from . import views
|
|
|
|
urlpatterns = [
|
|
# Dashboard — the home page after login
|
|
path('', views.index, name='home'),
|
|
|
|
# Attendance logging — where supervisors log daily work
|
|
path('attendance/log/', views.attendance_log, name='attendance_log'),
|
|
|
|
# Work history — table of all work logs with filters
|
|
path('history/', views.work_history, name='work_history'),
|
|
|
|
# CSV export — downloads filtered work logs as a spreadsheet
|
|
path('history/export/', views.export_work_log_csv, name='export_work_log_csv'),
|
|
|
|
# === WORK LOG PAYROLL CROSS-LINK (admin-only) ===
|
|
# Click a historic work log -> see who got paid and who didn't.
|
|
# AJAX endpoint returns JSON (the modal builds its own DOM safely);
|
|
# detail view renders the same data as a shareable full page.
|
|
path('history/<int:log_id>/', views.work_log_payroll_detail, name='work_log_payroll_detail'),
|
|
path('history/<int:log_id>/payroll/ajax/', views.work_log_payroll_ajax, name='work_log_payroll_ajax'),
|
|
|
|
# === SITE REPORT (DAILY PROGRESS) ===
|
|
# Companion to attendance: log WHAT was done on site (counts, checks,
|
|
# weather, free-form notes). Edit page is the redirect target after
|
|
# /attendance/log/ submission. Detail page is read-only.
|
|
path('site-report/<int:work_log_id>/edit/', views.site_report_edit, name='site_report_edit'),
|
|
path('site-report/<int:work_log_id>/', views.site_report_detail, name='site_report_detail'),
|
|
|
|
# CSV export — downloads all worker data (admin only)
|
|
path('workers/export/', views.export_workers_csv, name='export_workers_csv'),
|
|
|
|
# AJAX toggle — activates/deactivates workers, projects, teams from dashboard
|
|
path('toggle/<str:model_name>/<int:item_id>/', views.toggle_active, name='toggle_active'),
|
|
|
|
# === PAYROLL ===
|
|
# Main payroll dashboard — shows pending payments, history, loans, and charts
|
|
path('payroll/', views.payroll_dashboard, name='payroll_dashboard'),
|
|
|
|
# Process payment — pays a worker and links their unpaid logs + adjustments
|
|
path('payroll/pay/<int:worker_id>/', views.process_payment, name='process_payment'),
|
|
|
|
# Batch pay — preview which workers would be paid, then process all at once
|
|
path('payroll/batch-pay/preview/', views.batch_pay_preview, name='batch_pay_preview'),
|
|
path('payroll/batch-pay/', views.batch_pay, name='batch_pay'),
|
|
|
|
# Price overtime — creates Overtime adjustments from unpriced OT entries
|
|
path('payroll/price-overtime/', views.price_overtime, name='price_overtime'),
|
|
|
|
# Add a new payroll adjustment (bonus, deduction, loan, etc.)
|
|
path('payroll/adjustment/add/', views.add_adjustment, name='add_adjustment'),
|
|
|
|
# Edit an existing unpaid adjustment
|
|
path('payroll/adjustment/<int:adj_id>/edit/', views.edit_adjustment, name='edit_adjustment'),
|
|
|
|
# Delete an unpaid adjustment
|
|
path('payroll/adjustment/<int:adj_id>/delete/', views.delete_adjustment, name='delete_adjustment'),
|
|
|
|
# Bulk-delete multiple unpaid adjustments at once (Adjustments tab)
|
|
path('payroll/adjustments/bulk-delete/', views.bulk_delete_adjustments, name='bulk_delete_adjustments'),
|
|
|
|
# Preview a worker's payslip (AJAX — returns JSON)
|
|
path('payroll/preview/<int:worker_id>/', views.preview_payslip, name='preview_payslip'),
|
|
|
|
# Worker lookup — AJAX report card for a single worker (returns JSON)
|
|
path('payroll/worker-lookup/<int:worker_id>/', views.worker_lookup_ajax, name='worker_lookup_ajax'),
|
|
|
|
# Add a repayment from the payslip preview modal (AJAX — returns JSON)
|
|
path('payroll/repayment/<int:worker_id>/', views.add_repayment_ajax, name='add_repayment_ajax'),
|
|
|
|
# View a completed payslip (print-friendly page)
|
|
path('payroll/payslip/<int:pk>/', views.payslip_detail, name='payslip_detail'),
|
|
|
|
# === REPORTS ===
|
|
# Generate payroll reports filtered by date range, project, or team
|
|
path('report/', views.generate_report, name='generate_report'),
|
|
path('report/pdf/', views.generate_report_pdf, name='generate_report_pdf'),
|
|
|
|
# === 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'),
|
|
|
|
# === TEAMS ===
|
|
# Admin-friendly team management UI (alternative to /admin/core/team/)
|
|
path('teams/', views.team_list, name='team_list'),
|
|
path('teams/new/', views.team_edit, name='team_new'),
|
|
path('teams/report/', views.team_batch_report, name='team_batch_report'),
|
|
path('teams/report/csv/', views.team_batch_report_csv, name='team_batch_report_csv'),
|
|
path('teams/<int:team_id>/', views.team_detail, name='team_detail'),
|
|
path('teams/<int:team_id>/edit/', views.team_edit, name='team_edit'),
|
|
|
|
# === PROJECTS ===
|
|
# Admin-friendly project management UI (alternative to /admin/core/project/)
|
|
path('projects/', views.project_list, name='project_list'),
|
|
path('projects/new/', views.project_edit, name='project_new'),
|
|
path('projects/report/', views.project_batch_report, name='project_batch_report'),
|
|
path('projects/report/csv/', views.project_batch_report_csv, name='project_batch_report_csv'),
|
|
path('projects/<int:project_id>/', views.project_detail, name='project_detail'),
|
|
path('projects/<int:project_id>/edit/', views.project_edit, name='project_edit'),
|
|
|
|
# === EXPENSE RECEIPTS ===
|
|
# Create a new expense receipt — emails HTML + PDF to Spark Receipt
|
|
path('receipts/create/', views.create_receipt, name='create_receipt'),
|
|
|
|
# === TEMPORARY: Import production data from browser ===
|
|
# Visit /import-data/ once to populate the database. Remove after use.
|
|
path('import-data/', views.import_data, name='import_data'),
|
|
|
|
# === TEMPORARY: Run migrations from browser ===
|
|
# Visit /run-migrate/ to apply pending database migrations on production.
|
|
path('run-migrate/', views.run_migrate, name='run_migrate'),
|
|
|
|
# === BACKUP / RESTORE (admin-only, browser-accessible) ===
|
|
# Flatlogic has no SSH/shell — admins use these to snapshot and
|
|
# restore all app data via the browser. See CLAUDE.md "Backup &
|
|
# Restore" section for the full procedure.
|
|
path('backup-data/', views.backup_data, name='backup_data'),
|
|
path('restore-data/', views.restore_data, name='restore_data'),
|
|
]
|