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> <style>
/* ========================================================== /* ==========================================================
PAGE SETUP 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 { @page {
size: a4 portrait; size: a4 portrait;
margin: 2cm 1.8cm 1.6cm 1.8cm; 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; } 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 { .brand-eyebrow {
font-size: 7.5pt; font-size: 7.5pt;
font-weight: bold; font-weight: bold;
color: #10b981; color: #e8851a;
letter-spacing: 3pt; letter-spacing: 3pt;
margin-bottom: 4pt; margin-bottom: 4pt;
} }
@ -45,8 +44,8 @@
margin: 0; margin: 0;
} }
table.cover-band td { table.cover-band td {
border-top: 1pt solid #10b981; border-top: 1pt solid #e8851a;
border-bottom: 1pt solid #10b981; border-bottom: 1pt solid #e8851a;
padding: 9pt 0; padding: 9pt 0;
vertical-align: middle; vertical-align: middle;
} }
@ -64,39 +63,53 @@
white-space: nowrap; white-space: nowrap;
} }
.cover-filters { .cover-filters {
font-size: 10pt; font-size: 9.5pt;
color: #64748b; color: #475569;
letter-spacing: 0.3pt; letter-spacing: 0.2pt;
margin: 4pt 0 14pt 0; 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 { .chapter-heading {
margin-top: 16pt; margin: 16pt 0 8pt 0;
} padding-bottom: 4pt;
h2.section-title { border-bottom: 0.8pt solid #e8851a;
page-break-after: avoid; page-break-after: avoid;
} }
.break-before { .chapter-num {
page-break-before: always; display: inline-block;
} background-color: #e8851a;
color: #ffffff;
.eyebrow { font-size: 9pt;
font-size: 7pt;
font-weight: bold; font-weight: bold;
color: #10b981; padding: 2pt 7pt;
letter-spacing: 2.5pt; margin-right: 8pt;
margin-bottom: 3pt; letter-spacing: 0.5pt;
} }
h2.section-title { .chapter-title {
font-size: 13pt; font-size: 14pt;
font-weight: bold; font-weight: bold;
color: #0f172a; color: #0f172a;
margin: 0 0 10pt 0; letter-spacing: 0.2pt;
padding-bottom: 4pt; }
border-bottom: 0.5pt solid #cbd5e1;
/* ==========================================================
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 { h3.sub-title {
font-size: 9pt; font-size: 9pt;
@ -105,35 +118,121 @@
letter-spacing: 1pt; letter-spacing: 1pt;
margin: 8pt 0 3pt 0; 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 HERO KPI BAND — 4 cards laid out 2x2 so they fit on the
Halved the overall visual weight per feedback: portrait page without shrinking the big numbers. Each cell
• padding dropped from 9pt → 4pt top/bottom is a mini stat card: small label, bold value, subline.
• hero-value dropped from 22pt → 14pt ========================================================== */
• label/caption scaled down in proportion table.kpi-band {
Result: card is roughly half the height it was before. 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 { table.hero {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
margin: 4pt 0 14pt 0; margin: 4pt 0 10pt 0;
} }
table.hero td { table.hero td {
background-color: #f8fafc; background-color: #f8fafc;
vertical-align: top; vertical-align: top;
} }
table.hero td.hero-accent { table.hero td.hero-accent {
background-color: #10b981; background-color: #e8851a;
width: 3pt; width: 3pt;
padding: 0; padding: 0;
} }
table.hero td.hero-body { table.hero td.hero-body {
padding: 4pt 14pt; 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 { .hero-label {
font-size: 7pt; font-size: 7pt;
font-weight: bold; font-weight: bold;
@ -157,10 +256,10 @@
} }
/* ========================================================== /* ==========================================================
LEDGER LINES — with R-symbol aligned in its own column LEDGER LINES — used in Chapter II for compact key/value
Splitting the value cell into two cells (rsym + rnum) means tables (Labour Cost, Payments by Date, Adjustments). The
every "R" in a column appears at the same x-position, while split "rsym"/"rnum" trick keeps every R aligned in its
the numbers right-align neatly on their own edge. own column so the numbers right-align cleanly.
========================================================== */ ========================================================== */
table.ledger { table.ledger {
width: 100%; width: 100%;
@ -191,9 +290,6 @@
padding-right: 10pt; padding-right: 10pt;
width: 55pt; 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 { table.ledger td.rsym {
text-align: left; text-align: left;
color: #0f172a; 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%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
margin: 0; margin-top: 4pt;
font-size: 8.5pt;
} }
table.cols td { table.lifetime th {
vertical-align: top; text-align: left;
padding: 0; 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.lifetime th.r { text-align: right; }
table.cols td.gap { width: 10%; } table.lifetime td {
table.cols td.colR { width: 45%; } padding: 5pt;
border-bottom: 0.4pt solid #e2e8f0;
/* Extra breathing room between the two rows of the Period color: #334155;
Breakdown section (Labour Cost row ⇢ Payments/Adjustments row) */ vertical-align: middle;
table.cols-spaced { }
margin-top: 18pt; 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 WORKER BREAKDOWN TABLE — unchanged layout, now used for
Scoped via the .period-detail wrapper so other sections keep Chapter III only.
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).
========================================================== */ ========================================================== */
table.worker { table.worker {
width: 100%; width: 100%;
@ -296,19 +391,88 @@
text-align: right; text-align: right;
white-space: nowrap; white-space: nowrap;
} }
/* Total Paid column: bolder, darker for emphasis */
table.worker td.total { table.worker td.total {
font-weight: bold; font-weight: bold;
color: #0f172a; color: #0f172a;
text-align: right; text-align: right;
white-space: nowrap; white-space: nowrap;
} }
/* Empty-value variant (em-dash) */
table.worker td.dim { table.worker td.dim {
color: #cbd5e1; color: #cbd5e1;
text-align: right; 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 MISC
========================================================== */ ========================================================== */
@ -317,13 +481,14 @@
font-size: 9pt; font-size: 9pt;
padding: 5pt 0; padding: 5pt 0;
} }
#footerContent { .footer {
margin-top: 20pt;
padding-top: 6pt;
border-top: 0.3pt solid #e2e8f0;
font-size: 7pt; font-size: 7pt;
color: #94a3b8; color: #94a3b8;
text-align: center; text-align: center;
letter-spacing: 0.5pt; letter-spacing: 0.5pt;
border-top: 0.3pt solid #e2e8f0;
padding-top: 4pt;
} }
</style> </style>
</head> </head>
@ -331,6 +496,11 @@
<!-- ============================================================== <!-- ==============================================================
COVER 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> <div class="brand-eyebrow">FOXFITT CONSTRUCTION</div>
<table class="cover-band"> <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> <td class="cover-date">{{ start_date|date:"d F Y" }} &ndash; {{ end_date|date:"d F Y" }}</td>
</tr> </tr>
</table> </table>
<div class="cover-filters">{{ project_name }} &nbsp;&bull;&nbsp; {{ team_name }}</div> <div class="cover-filters">
{{ start_date|date:"d M Y" }} &ndash; {{ end_date|date:"d M Y" }}
<!-- ============================================================== &nbsp;&middot;&nbsp; {{ project_name }}
ALL TIME &nbsp;&middot;&nbsp; {{ team_name }}
(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> </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"> <table class="kpi-band">
<div class="eyebrow">YEAR-TO-DATE</div>
<h2 class="section-title">{{ current_year }} &mdash; Labour Cost</h2>
<table class="cols">
<tr> <tr>
<td class="colL"> <td class="kpi kpi-danger">
<h3 class="sub-title">BY PROJECT</h3> <div class="kpi-label">Paid This Period</div>
{% if year_projects %} <div class="kpi-value">R {{ total_paid_out|money }}</div>
<table class="ledger"> <div class="kpi-subline">{{ start_date|date:"d M Y" }} &ndash; {{ end_date|date:"d M Y" }}</div>
{% for item in year_projects %} </td>
<tr> <td class="kpi kpi-warning">
<td class="rank">{{ forloop.counter }}</td> <div class="kpi-label">Outstanding Now</div>
<td class="lbl">{{ item.project }}</td> <div class="kpi-value">R {{ current_outstanding.total|money }}</div>
<td class="rsym">R</td> <div class="kpi-subline">as of {{ current_as_of|date:"d M Y H:i" }}</div>
<td class="rnum">{{ item.total|money }}</td> </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>
</tr> </tr>
</table> <tr>
</div> <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) CHAPTER I — Lifetime Context
All summary figures for the chosen date range live here: All-time totals by project (with start date / working days /
- Hero: Total Paid Out (headline KPI) avg per working day) and by team (simpler: name + total).
- 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.
============================================================== --> ============================================================== -->
<div class="section break-before period-detail"> <div class="chapter-heading">
<div class="eyebrow">SELECTED PERIOD</div> <span class="chapter-num">I</span><span class="chapter-title">Lifetime Context</span>
<h2 class="section-title">Period Breakdown</h2> </div>
<!-- Hero: the headline KPI for this period --> <h2 class="section-title">All Time &mdash; Projects</h2>
<table class="hero"> {% 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> <tr>
<td class="hero-accent"></td> <td class="stat stat-danger">
<td class="hero-body"> <div class="stat-label">Total Paid Out</div>
<div class="hero-label">TOTAL PAID OUT</div> <div class="stat-value">R {{ total_paid_out|money }}</div>
<div class="hero-value">R {{ total_paid_out|money }}</div> </td>
<div class="hero-caption">across {{ total_worker_days }} worker-days in this period</div> <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> </td>
</tr> </tr>
</table> </table>
<!-- Loans pair (left) + Advances pair (right) --> <!-- Labour Cost by Project / by Team (side-by-side) -->
<!-- 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 -->
<table class="cols"> <table class="cols">
<tr> <tr>
<td class="colL"> <td class="colL">
@ -537,7 +680,7 @@
</tr> </tr>
</table> </table>
<!-- Second row: extra top margin creates clear visual gap --> <!-- Payments by Date / Adjustments (side-by-side) -->
<table class="cols cols-spaced"> <table class="cols cols-spaced">
<tr> <tr>
<td class="colL"> <td class="colL">
@ -574,13 +717,14 @@
</div> </div>
<!-- ============================================================== <!-- ==============================================================
WORKER BREAKDOWN CHAPTER III — Worker Breakdown
Uses nested mini-tables inside each money cell so the R and the Per-worker detail: days worked, total paid, plus one column
number line up column-wise across every row. per active adjustment type. Same table as before.
============================================================== --> ============================================================== -->
<div class="section"> <div class="section">
<div class="eyebrow">PER-WORKER DETAIL</div> <div class="chapter-heading">
<h2 class="section-title">Worker Breakdown</h2> <span class="chapter-num">III</span><span class="chapter-title">Worker Breakdown</span>
</div>
{% if worker_breakdown %} {% if worker_breakdown %}
<table class="worker"> <table class="worker">
@ -612,10 +756,59 @@
{% endif %} {% endif %}
</div> </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 FOOTER
============================================================== --> ============================================================== -->
<div id="footerContent"> <div class="footer">
GENERATED {{ now|date:"d M Y H:i" }} &nbsp;&bull;&nbsp; FOXFITT CONSTRUCTION &nbsp;&bull;&nbsp; CONFIDENTIAL GENERATED {{ now|date:"d M Y H:i" }} &nbsp;&bull;&nbsp; FOXFITT CONSTRUCTION &nbsp;&bull;&nbsp; CONFIDENTIAL
</div> </div>