Report PDF: mirror the executive redesign (hero band + 4 chapters)

PDF template updated to match the new HTML structure: cover block
with static filter labels, hero KPI band (4 stacked 2x2), Chapter I
lifetime (Projects + Teams full-width, Projects now with Start /
Working Days / Avg-R-per-Working-Day columns), Chapter II selected
period (existing Total Paid Out hero + Loans/Advances pairs +
Labour Cost + Payments/Adjustments), Chapter III worker breakdown
(heading renamed), Chapter IV team x project pivot (new).

THIS YEAR section dropped per design doc section 3 (redundant with
All Time + Selected Period).

Same _build_report_context helper so HTML and PDF cannot drift in
data. All numbers identical. WeasyPrint-friendly: absolute units,
single-column body, no Font Awesome.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Konrad du Plessis 2026-04-23 00:12:56 +02:00
parent fe85c9d7fd
commit a27da90c58

View File

@ -5,17 +5,14 @@
<style>
/* ==========================================================
PAGE SETUP
Standard A4 portrait with generous margins. WeasyPrint
understands @page rules for headers/footers; we use a
simple bottom-margin footer block instead of a running
element so the footer can reference `now` from context.
========================================================== */
@page {
size: a4 portrait;
margin: 2cm 1.8cm 1.6cm 1.8cm;
@frame footer_frame {
-pdf-frame-content: footerContent;
bottom: 0.6cm;
margin-left: 1.8cm;
margin-right: 1.8cm;
height: 0.8cm;
}
}
/* ==========================================================
@ -30,12 +27,14 @@
p { margin: 3pt 0; }
/* ==========================================================
COVER
COVER — brand eyebrow + orange title band + filter pills
The accent line colour was updated to the brand amber
(#e8851a) so the PDF matches the web app's orange accent.
========================================================== */
.brand-eyebrow {
font-size: 7.5pt;
font-weight: bold;
color: #10b981;
color: #e8851a;
letter-spacing: 3pt;
margin-bottom: 4pt;
}
@ -45,8 +44,8 @@
margin: 0;
}
table.cover-band td {
border-top: 1pt solid #10b981;
border-bottom: 1pt solid #10b981;
border-top: 1pt solid #e8851a;
border-bottom: 1pt solid #e8851a;
padding: 9pt 0;
vertical-align: middle;
}
@ -64,39 +63,53 @@
white-space: nowrap;
}
.cover-filters {
font-size: 10pt;
color: #64748b;
letter-spacing: 0.3pt;
margin: 4pt 0 14pt 0;
font-size: 9.5pt;
color: #475569;
letter-spacing: 0.2pt;
margin: 6pt 0 14pt 0;
}
/* ==========================================================
SECTION STRUCTURE
CHAPTER HEADINGS
Matches the HTML's "chapter-num I" style: a small filled
square-ish marker with the Roman numeral next to the
chapter title. No Font Awesome in PDFs — just text.
========================================================== */
.section {
margin-top: 16pt;
}
h2.section-title {
.chapter-heading {
margin: 16pt 0 8pt 0;
padding-bottom: 4pt;
border-bottom: 0.8pt solid #e8851a;
page-break-after: avoid;
}
.break-before {
page-break-before: always;
}
.eyebrow {
font-size: 7pt;
.chapter-num {
display: inline-block;
background-color: #e8851a;
color: #ffffff;
font-size: 9pt;
font-weight: bold;
color: #10b981;
letter-spacing: 2.5pt;
margin-bottom: 3pt;
padding: 2pt 7pt;
margin-right: 8pt;
letter-spacing: 0.5pt;
}
h2.section-title {
font-size: 13pt;
.chapter-title {
font-size: 14pt;
font-weight: bold;
color: #0f172a;
margin: 0 0 10pt 0;
padding-bottom: 4pt;
border-bottom: 0.5pt solid #cbd5e1;
letter-spacing: 0.2pt;
}
/* ==========================================================
SECTION STRUCTURE — smaller sub-headings within a chapter
========================================================== */
.section { margin-top: 10pt; }
h2.section-title {
page-break-after: avoid;
font-size: 11pt;
font-weight: bold;
color: #0f172a;
margin: 10pt 0 6pt 0;
padding-bottom: 3pt;
border-bottom: 0.4pt solid #cbd5e1;
}
h3.sub-title {
font-size: 9pt;
@ -105,35 +118,121 @@
letter-spacing: 1pt;
margin: 8pt 0 3pt 0;
}
.break-before { page-break-before: always; }
.eyebrow {
font-size: 7pt;
font-weight: bold;
color: #e8851a;
letter-spacing: 2.5pt;
margin-bottom: 3pt;
}
/* ==========================================================
HERO CARD — 50% SMALLER
Halved the overall visual weight per feedback:
• padding dropped from 9pt → 4pt top/bottom
• hero-value dropped from 22pt → 14pt
• label/caption scaled down in proportion
Result: card is roughly half the height it was before.
HERO KPI BAND — 4 cards laid out 2x2 so they fit on the
portrait page without shrinking the big numbers. Each cell
is a mini stat card: small label, bold value, subline.
========================================================== */
table.kpi-band {
width: 100%;
border-collapse: separate;
border-spacing: 6pt;
margin: 2pt 0 10pt 0;
}
table.kpi-band td.kpi {
width: 50%;
background-color: #f8fafc;
border-left: 3pt solid #e8851a;
padding: 8pt 10pt;
vertical-align: top;
}
/* Colour-vary left border to echo the HTML hero tiles */
table.kpi-band td.kpi-danger { border-left-color: #dc2626; }
table.kpi-band td.kpi-warning { border-left-color: #d97706; }
table.kpi-band td.kpi-info { border-left-color: #0ea5e9; }
.kpi-label {
font-size: 7pt;
font-weight: bold;
color: #64748b;
letter-spacing: 1.8pt;
line-height: 1;
margin: 0;
text-transform: uppercase;
}
.kpi-value {
font-size: 16pt;
font-weight: bold;
color: #0f172a;
line-height: 1;
margin: 4pt 0 2pt 0;
}
.kpi-subline {
font-size: 7.5pt;
color: #64748b;
line-height: 1.2;
margin: 0;
}
/* ==========================================================
SMALLER STAT CARD GRID — 6 stats for Selected Period
Rendered as a 3x2 grid so the labels + numbers all fit on
the portrait page. Same border-colour trick as the hero.
========================================================== */
table.stat-grid {
width: 100%;
border-collapse: separate;
border-spacing: 5pt;
margin: 2pt 0 8pt 0;
}
table.stat-grid td.stat {
width: 33.33%;
background-color: #f8fafc;
border-left: 2.5pt solid #e8851a;
padding: 6pt 8pt;
vertical-align: top;
}
table.stat-grid td.stat-danger { border-left-color: #dc2626; }
table.stat-grid td.stat-warning { border-left-color: #d97706; }
table.stat-grid td.stat-success { border-left-color: #059669; }
table.stat-grid td.stat-info { border-left-color: #0ea5e9; }
.stat-label {
font-size: 6.5pt;
font-weight: bold;
color: #64748b;
letter-spacing: 1.2pt;
line-height: 1;
margin: 0;
text-transform: uppercase;
}
.stat-value {
font-size: 11pt;
font-weight: bold;
color: #0f172a;
line-height: 1;
margin: 3pt 0 0 0;
}
/* ==========================================================
HERO — legacy single-card block used by the small
"Total Paid Out" hero inside Chapter II (kept for the
SELECTED PERIOD section's existing design).
========================================================== */
table.hero {
width: 100%;
border-collapse: collapse;
margin: 4pt 0 14pt 0;
margin: 4pt 0 10pt 0;
}
table.hero td {
background-color: #f8fafc;
vertical-align: top;
}
table.hero td.hero-accent {
background-color: #10b981;
background-color: #e8851a;
width: 3pt;
padding: 0;
}
table.hero td.hero-body {
padding: 4pt 14pt;
}
/* Hero spacing is dominated by line-height, not margin.
line-height: 1 collapses the phantom "leading" above/below
the value glyphs → ~50% tighter gaps around "R 64 939.00". */
.hero-label {
font-size: 7pt;
font-weight: bold;
@ -157,10 +256,10 @@
}
/* ==========================================================
LEDGER LINES — with R-symbol aligned in its own column
Splitting the value cell into two cells (rsym + rnum) means
every "R" in a column appears at the same x-position, while
the numbers right-align neatly on their own edge.
LEDGER LINES — used in Chapter II for compact key/value
tables (Labour Cost, Payments by Date, Adjustments). The
split "rsym"/"rnum" trick keeps every R aligned in its
own column so the numbers right-align cleanly.
========================================================== */
table.ledger {
width: 100%;
@ -191,9 +290,6 @@
padding-right: 10pt;
width: 55pt;
}
/* The two cells that together form a money value.
rsym: left-aligned "R" anchored at a fixed x-position
rnum: right-aligned number, bold black */
table.ledger td.rsym {
text-align: left;
color: #0f172a;
@ -213,57 +309,56 @@
}
/* ==========================================================
TWO-COLUMN LAYOUT
LIFETIME PROJECTS — wider table with Start / Working
Days / Avg per Working Day columns. Not a ledger because
we need column headers.
========================================================== */
table.cols {
table.lifetime {
width: 100%;
border-collapse: collapse;
margin: 0;
margin-top: 4pt;
font-size: 8.5pt;
}
table.cols td {
vertical-align: top;
padding: 0;
table.lifetime th {
text-align: left;
font-size: 7pt;
font-weight: bold;
color: #64748b;
letter-spacing: 0.8pt;
padding: 4pt 5pt 5pt 5pt;
border-bottom: 1pt solid #0f172a;
white-space: nowrap;
text-transform: uppercase;
}
table.cols td.colL { width: 45%; }
table.cols td.gap { width: 10%; }
table.cols td.colR { width: 45%; }
/* Extra breathing room between the two rows of the Period
Breakdown section (Labour Cost row ⇢ Payments/Adjustments row) */
table.cols-spaced {
margin-top: 18pt;
table.lifetime th.r { text-align: right; }
table.lifetime td {
padding: 5pt;
border-bottom: 0.4pt solid #e2e8f0;
color: #334155;
vertical-align: middle;
}
table.lifetime td.name {
font-weight: bold;
color: #0f172a;
}
table.lifetime td.r {
text-align: right;
white-space: nowrap;
}
table.lifetime td.total {
font-weight: bold;
color: #0f172a;
text-align: right;
white-space: nowrap;
}
table.lifetime td.dim {
color: #cbd5e1;
text-align: right;
}
/* ==========================================================
PERIOD DETAIL — 15% smaller text in this section only
Scoped via the .period-detail wrapper so other sections keep
their normal size.
========================================================== */
.period-detail h3.sub-title {
font-size: 8pt; /* was 9pt */
}
.period-detail table.ledger td.lbl {
font-size: 8pt; /* was 9.5pt */
}
.period-detail table.ledger td.meta {
font-size: 7.5pt; /* was 8.5pt */
}
.period-detail table.ledger td.rsym,
.period-detail table.ledger td.rnum {
font-size: 8.5pt; /* was 10pt */
}
/* Use split padding-top/bottom (NOT the shorthand) so horizontal
padding defined on .meta and .rsym is preserved — otherwise the
shorthand clobbers it and you get "130 daysR" with no gap. */
.period-detail table.ledger td {
padding-top: 3pt;
padding-bottom: 3pt;
}
/* ==========================================================
WORKER BREAKDOWN TABLE
Money values inside use a nested mini-table so R and number
live in their own columns (same alignment trick as ledger).
WORKER BREAKDOWN TABLE — unchanged layout, now used for
Chapter III only.
========================================================== */
table.worker {
width: 100%;
@ -296,19 +391,88 @@
text-align: right;
white-space: nowrap;
}
/* Total Paid column: bolder, darker for emphasis */
table.worker td.total {
font-weight: bold;
color: #0f172a;
text-align: right;
white-space: nowrap;
}
/* Empty-value variant (em-dash) */
table.worker td.dim {
color: #cbd5e1;
text-align: right;
}
/* ==========================================================
PIVOT TABLE — Chapter IV Team x Project activity
========================================================== */
table.pivot {
width: 100%;
border-collapse: collapse;
margin-top: 4pt;
font-size: 8.5pt;
}
table.pivot th {
text-align: left;
font-size: 7pt;
font-weight: bold;
color: #64748b;
letter-spacing: 0.8pt;
padding: 4pt 5pt 5pt 5pt;
border-bottom: 1pt solid #0f172a;
white-space: nowrap;
text-transform: uppercase;
}
table.pivot th.r { text-align: right; }
table.pivot td {
padding: 4pt 5pt;
border-bottom: 0.4pt solid #e2e8f0;
color: #334155;
}
table.pivot td.name {
font-weight: bold;
color: #0f172a;
}
table.pivot td.r {
text-align: right;
white-space: nowrap;
}
table.pivot td.dim {
color: #cbd5e1;
text-align: right;
}
table.pivot td.total {
font-weight: bold;
color: #0f172a;
text-align: right;
white-space: nowrap;
}
table.pivot tr.total-row td {
border-top: 0.8pt solid #0f172a;
border-bottom: none;
font-weight: bold;
color: #0f172a;
background-color: #f8fafc;
}
/* ==========================================================
TWO-COLUMN LAYOUT (kept for the Chapter II sub-blocks)
========================================================== */
table.cols {
width: 100%;
border-collapse: collapse;
margin: 0;
}
table.cols td {
vertical-align: top;
padding: 0;
}
table.cols td.colL { width: 48%; }
table.cols td.gap { width: 4%; }
table.cols td.colR { width: 48%; }
table.cols-spaced {
margin-top: 12pt;
}
/* ==========================================================
MISC
========================================================== */
@ -317,13 +481,14 @@
font-size: 9pt;
padding: 5pt 0;
}
#footerContent {
.footer {
margin-top: 20pt;
padding-top: 6pt;
border-top: 0.3pt solid #e2e8f0;
font-size: 7pt;
color: #94a3b8;
text-align: center;
letter-spacing: 0.5pt;
border-top: 0.3pt solid #e2e8f0;
padding-top: 4pt;
}
</style>
</head>
@ -331,6 +496,11 @@
<!-- ==============================================================
COVER
Brand eyebrow + title band + static filter-pill line. The filter
line uses plain text (no x-buttons) because this is a static PDF.
project_name and team_name already arrive comma-joined from the
report-context helper — "All Projects" / "All Teams" when unset,
or "Wilkot Boerdery, Solar Farm Alpha" style for multi-select.
============================================================== -->
<div class="brand-eyebrow">FOXFITT CONSTRUCTION</div>
<table class="cover-band">
@ -339,168 +509,141 @@
<td class="cover-date">{{ start_date|date:"d F Y" }} &ndash; {{ end_date|date:"d F Y" }}</td>
</tr>
</table>
<div class="cover-filters">{{ project_name }} &nbsp;&bull;&nbsp; {{ team_name }}</div>
<!-- ==============================================================
ALL TIME
(All-time and year-to-date sections now appear FIRST, before the
Selected Period block — the big-picture lifetime view sets context
before we zoom in to the selected date range.)
============================================================== -->
<div class="section">
<div class="eyebrow">LIFETIME PERFORMANCE</div>
<h2 class="section-title">All Time &mdash; Labour Cost</h2>
<table class="cols">
<tr>
<td class="colL">
<h3 class="sub-title">BY PROJECT</h3>
{% if alltime_projects %}
<table class="ledger">
{% for item in alltime_projects %}
<tr>
<td class="rank">{{ forloop.counter }}</td>
<td class="lbl">{{ item.project }}</td>
<td class="rsym">R</td>
<td class="rnum">{{ item.total|money }}</td>
</tr>
{% endfor %}
</table>
{% else %}<p class="empty">No project data yet.</p>{% endif %}
</td>
<td class="gap"></td>
<td class="colR">
<h3 class="sub-title">BY TEAM</h3>
{% if alltime_teams %}
<table class="ledger">
{% for item in alltime_teams %}
<tr>
<td class="rank">{{ forloop.counter }}</td>
<td class="lbl">{{ item.team }}</td>
<td class="rsym">R</td>
<td class="rnum">{{ item.total|money }}</td>
</tr>
{% endfor %}
</table>
{% else %}<p class="empty">No team data yet.</p>{% endif %}
</td>
</tr>
</table>
<div class="cover-filters">
{{ start_date|date:"d M Y" }} &ndash; {{ end_date|date:"d M Y" }}
&nbsp;&middot;&nbsp; {{ project_name }}
&nbsp;&middot;&nbsp; {{ team_name }}
</div>
<!-- ==============================================================
THIS YEAR
HERO KPI BAND — 4 cards in a 2x2 grid
Shows the big-picture numbers up front:
1. Paid This Period (total_paid_out for the selected range)
2. Outstanding Now (what's currently unpaid across FoxFitt)
3. FoxFitt Avg / Day (lifetime company average)
4. FoxFitt Avg / Month (lifetime × ~30.44)
============================================================== -->
<div class="section">
<div class="eyebrow">YEAR-TO-DATE</div>
<h2 class="section-title">{{ current_year }} &mdash; Labour Cost</h2>
<table class="cols">
<table class="kpi-band">
<tr>
<td class="colL">
<h3 class="sub-title">BY PROJECT</h3>
{% if year_projects %}
<table class="ledger">
{% for item in year_projects %}
<tr>
<td class="rank">{{ forloop.counter }}</td>
<td class="lbl">{{ item.project }}</td>
<td class="rsym">R</td>
<td class="rnum">{{ item.total|money }}</td>
</tr>
{% endfor %}
</table>
{% else %}<p class="empty">No project data for {{ current_year }}.</p>{% endif %}
</td>
<td class="gap"></td>
<td class="colR">
<h3 class="sub-title">BY TEAM</h3>
{% if year_teams %}
<table class="ledger">
{% for item in year_teams %}
<tr>
<td class="rank">{{ forloop.counter }}</td>
<td class="lbl">{{ item.team }}</td>
<td class="rsym">R</td>
<td class="rnum">{{ item.total|money }}</td>
</tr>
{% endfor %}
</table>
{% else %}<p class="empty">No team data for {{ current_year }}.</p>{% endif %}
</td>
<td class="kpi kpi-danger">
<div class="kpi-label">Paid This Period</div>
<div class="kpi-value">R {{ total_paid_out|money }}</div>
<div class="kpi-subline">{{ start_date|date:"d M Y" }} &ndash; {{ end_date|date:"d M Y" }}</div>
</td>
<td class="kpi kpi-warning">
<div class="kpi-label">Outstanding Now</div>
<div class="kpi-value">R {{ current_outstanding.total|money }}</div>
<div class="kpi-subline">as of {{ current_as_of|date:"d M Y H:i" }}</div>
</td>
</tr>
</table>
</div>
<tr>
<td class="kpi kpi-info">
<div class="kpi-label">FoxFitt Avg / Day</div>
<div class="kpi-value">R {{ company_avg_daily|money }}</div>
<div class="kpi-subline">lifetime avg &middot; {{ company_working_days }} working days</div>
</td>
<td class="kpi kpi-info">
<div class="kpi-label">FoxFitt Avg / Month</div>
<div class="kpi-value">R {{ company_avg_monthly|money }}</div>
<div class="kpi-subline">lifetime avg &middot; ~30.44 days/month</div>
</td>
</tr>
</table>
<!-- ==============================================================
SELECTED PERIOD (new page, compact text via .period-detail)
All summary figures for the chosen date range live here:
- Hero: Total Paid Out (headline KPI)
- Loans pair (issued + outstanding)
- Advances pair (issued + outstanding)
- Labour Cost by project / team
- Payments by date / Adjustments
- Worker breakdown (next section, flows naturally)
The .period-detail wrapper shrinks ledger text 15%; the hero
card uses its own classes so its headline stays prominent.
CHAPTER I — Lifetime Context
All-time totals by project (with start date / working days /
avg per working day) and by team (simpler: name + total).
============================================================== -->
<div class="section break-before period-detail">
<div class="eyebrow">SELECTED PERIOD</div>
<h2 class="section-title">Period Breakdown</h2>
<div class="chapter-heading">
<span class="chapter-num">I</span><span class="chapter-title">Lifetime Context</span>
</div>
<!-- Hero: the headline KPI for this period -->
<table class="hero">
<h2 class="section-title">All Time &mdash; Projects</h2>
{% if alltime_projects %}
<table class="lifetime">
<tr>
<th>Project</th>
<th>Start</th>
<th class="r">Working Days</th>
<th class="r">Total Cost</th>
<th class="r">Avg R / Working Day</th>
</tr>
{% for item in alltime_projects %}
<tr>
<td class="name">{{ item.project }}</td>
<td>{% if item.start_date %}{{ item.start_date|date:"d M Y" }}{% else %}<span style="color:#cbd5e1;">&mdash;</span>{% endif %}</td>
<td class="r">{% if item.working_days %}{{ item.working_days }}{% else %}<span style="color:#cbd5e1;">&mdash;</span>{% endif %}</td>
<td class="total">R&nbsp;{{ item.total|money }}</td>
<td class="r">{% if item.working_days %}R&nbsp;{{ item.avg_per_working_day|money }}{% else %}<span style="color:#cbd5e1;">&mdash;</span>{% endif %}</td>
</tr>
{% endfor %}
</table>
{% else %}<p class="empty">No lifetime project data.</p>{% endif %}
<h2 class="section-title">All Time &mdash; Teams</h2>
{% if alltime_teams %}
<table class="lifetime">
<tr>
<th>Team</th>
<th class="r">Total Cost</th>
</tr>
{% for item in alltime_teams %}
<tr>
<td class="name">{{ item.team }}</td>
<td class="total">R&nbsp;{{ item.total|money }}</td>
</tr>
{% endfor %}
</table>
{% else %}<p class="empty">No lifetime team data.</p>{% endif %}
<!-- ==============================================================
CHAPTER II — Selected Period
The detailed breakdown for the chosen date range. Starts on a
fresh page so lifetime context (Chapter I) doesn't crowd it.
Structure:
- 6 stat cards (Paid · Worker-Days · Loans×2 · Advances×2)
- Labour Cost by Project / by Team (side-by-side)
- Payments by Date / Adjustments (side-by-side)
============================================================== -->
<div class="break-before">
<div class="chapter-heading">
<span class="chapter-num">II</span><span class="chapter-title">Selected Period: {{ start_date|date:"d M Y" }} &ndash; {{ end_date|date:"d M Y" }}</span>
</div>
<!-- 6 stat cards in a 3x2 grid -->
<table class="stat-grid">
<tr>
<td class="hero-accent"></td>
<td class="hero-body">
<div class="hero-label">TOTAL PAID OUT</div>
<div class="hero-value">R {{ total_paid_out|money }}</div>
<div class="hero-caption">across {{ total_worker_days }} worker-days in this period</div>
<td class="stat stat-danger">
<div class="stat-label">Total Paid Out</div>
<div class="stat-value">R {{ total_paid_out|money }}</div>
</td>
<td class="stat stat-info">
<div class="stat-label">Worker-Days</div>
<div class="stat-value">{{ total_worker_days }}</div>
</td>
<td class="stat stat-success">
<div class="stat-label">Loans Issued</div>
<div class="stat-value">R {{ loans_issued|money }}</div>
</td>
</tr>
<tr>
<td class="stat stat-warning">
<div class="stat-label">Loans Outstanding</div>
<div class="stat-value">R {{ loans_outstanding|money }}</div>
</td>
<td class="stat stat-success">
<div class="stat-label">Advances Issued</div>
<div class="stat-value">R {{ advances_issued|money }}</div>
</td>
<td class="stat stat-warning">
<div class="stat-label">Advances Outstanding</div>
<div class="stat-value">R {{ advances_outstanding|money }}</div>
</td>
</tr>
</table>
<!-- Loans pair (left) + Advances pair (right) -->
<!-- Each column shows issued first, then outstanding — grouped
by instrument type for easier scanning. -->
<table class="cols">
<tr>
<td class="colL">
<h3 class="sub-title">LOANS</h3>
<table class="ledger">
<tr>
<td class="lbl">Loans issued</td>
<td class="rsym">R</td>
<td class="rnum">{{ loans_issued|money }}</td>
</tr>
<tr>
<td class="lbl">Loans outstanding</td>
<td class="rsym">R</td>
<td class="rnum">{{ loans_outstanding|money }}</td>
</tr>
</table>
</td>
<td class="gap"></td>
<td class="colR">
<h3 class="sub-title">ADVANCES</h3>
<table class="ledger">
<tr>
<td class="lbl">Advances issued</td>
<td class="rsym">R</td>
<td class="rnum">{{ advances_issued|money }}</td>
</tr>
<tr>
<td class="lbl">Advances outstanding</td>
<td class="rsym">R</td>
<td class="rnum">{{ advances_outstanding|money }}</td>
</tr>
</table>
</td>
</tr>
</table>
<!-- First row: Labour Cost by project / team -->
<!-- Labour Cost by Project / by Team (side-by-side) -->
<table class="cols">
<tr>
<td class="colL">
@ -537,7 +680,7 @@
</tr>
</table>
<!-- Second row: extra top margin creates clear visual gap -->
<!-- Payments by Date / Adjustments (side-by-side) -->
<table class="cols cols-spaced">
<tr>
<td class="colL">
@ -574,13 +717,14 @@
</div>
<!-- ==============================================================
WORKER BREAKDOWN
Uses nested mini-tables inside each money cell so the R and the
number line up column-wise across every row.
CHAPTER III — Worker Breakdown
Per-worker detail: days worked, total paid, plus one column
per active adjustment type. Same table as before.
============================================================== -->
<div class="section">
<div class="eyebrow">PER-WORKER DETAIL</div>
<h2 class="section-title">Worker Breakdown</h2>
<div class="chapter-heading">
<span class="chapter-num">III</span><span class="chapter-title">Worker Breakdown</span>
</div>
{% if worker_breakdown %}
<table class="worker">
@ -612,10 +756,59 @@
{% endif %}
</div>
<!-- ==============================================================
CHAPTER IV — Team × Project Activity
Pivot table: rows = teams, columns = projects, cells = distinct
work days. The `dictlookup` filter (from core/templatetags/
format_tags.py) lets the template fetch cells by project id
from the `row.cells_by_project_id` dict.
============================================================== -->
<div class="section">
<div class="chapter-heading">
<span class="chapter-num">IV</span><span class="chapter-title">Team &times; Project Activity</span>
</div>
{% if team_project_activity.rows %}
<table class="pivot">
<tr>
<th>Team</th>
{% for col in team_project_activity.columns %}
<th class="r">{{ col.name }}</th>
{% endfor %}
<th class="r">Total</th>
</tr>
{% for row in team_project_activity.rows %}
<tr>
<td class="name">{{ row.team_name }}</td>
{% for col in team_project_activity.columns %}
{% with days=row.cells_by_project_id|dictlookup:col.id %}
{% if days %}
<td class="r">{{ days }}</td>
{% else %}
<td class="dim">&mdash;</td>
{% endif %}
{% endwith %}
{% endfor %}
<td class="total">{{ row.row_total }}</td>
</tr>
{% endfor %}
<tr class="total-row">
<td>Total</td>
{% for col in team_project_activity.columns %}
<td class="r">{{ team_project_activity.col_totals|dictlookup:col.id }}</td>
{% endfor %}
<td class="r">{{ team_project_activity.grand_total }}</td>
</tr>
</table>
{% else %}
<p class="empty">No team &times; project activity in this period.</p>
{% endif %}
</div>
<!-- ==============================================================
FOOTER
============================================================== -->
<div id="footerContent">
<div class="footer">
GENERATED {{ now|date:"d M Y H:i" }} &nbsp;&bull;&nbsp; FOXFITT CONSTRUCTION &nbsp;&bull;&nbsp; CONFIDENTIAL
</div>