From 01d9b8d54f840774339ca255e9e3d765ba1149a0 Mon Sep 17 00:00:00 2001 From: harrisonk0 Date: Sun, 22 Mar 2026 21:33:59 +0000 Subject: [PATCH 1/2] Tighten PDF pagination and cover layout --- components/SessionReportModal.tsx | 15 +++++- components/reports/SessionReportDocument.tsx | 53 +++++++++++++++++--- 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/components/SessionReportModal.tsx b/components/SessionReportModal.tsx index 5665a96..c2787a9 100644 --- a/components/SessionReportModal.tsx +++ b/components/SessionReportModal.tsx @@ -13,6 +13,9 @@ interface SessionReportModalProps { onClose: () => void; } +const MEMBER_DETAIL_FIRST_PAGE_ROWS = 18; +const MEMBER_DETAIL_CONTINUED_ROWS = 26; + const formatDate = (value: string) => new Date(`${value}T00:00:00`).toLocaleDateString(undefined, { day: 'numeric', @@ -60,7 +63,17 @@ const SessionReportModal: React.FC = ({ const sectionLabel = activeSection === 'company' ? 'Company Section' : 'Junior Section'; const filename = `${activeSection}-session-report-${startDate || 'start'}-to-${endDate || 'end'}.pdf`; const estimatedPageCount = report - ? 1 + 1 + 1 + Math.max(1, Math.ceil(report.meetings.length / 18)) + 1 + Math.max(1, Math.ceil(report.members.length / (activeSection === 'junior' ? 14 : 16))) + report.members.reduce((sum, member) => sum + 1 + Math.ceil(Math.max(member.meetings.length - 14, 0) / 22), 0) + ? 1 + + 1 + + 1 + + Math.max(1, Math.ceil(report.meetings.length / 18)) + + 1 + + Math.max(1, Math.ceil(report.members.length / (activeSection === 'junior' ? 14 : 16))) + + report.members.reduce( + (sum, member) => + sum + 1 + Math.ceil(Math.max(member.meetings.length - MEMBER_DETAIL_FIRST_PAGE_ROWS, 0) / MEMBER_DETAIL_CONTINUED_ROWS), + 0, + ) : 0; return ( diff --git a/components/reports/SessionReportDocument.tsx b/components/reports/SessionReportDocument.tsx index a3baa53..c1f30a0 100644 --- a/components/reports/SessionReportDocument.tsx +++ b/components/reports/SessionReportDocument.tsx @@ -19,6 +19,9 @@ const BB_BACKGROUND_URL = bbBackground; const COMPANY_LOGO_URL = companyLogo; const JUNIOR_LOGO_URL = juniorLogo; +const MEMBER_DETAIL_FIRST_PAGE_ROWS = 18; +const MEMBER_DETAIL_CONTINUED_ROWS = 26; + const styles = StyleSheet.create({ page: { backgroundColor: '#f8fafc', @@ -53,15 +56,40 @@ const styles = StyleSheet.create({ }, coverPhotoCard: { width: '34%', + height: 228, backgroundColor: '#18233f', borderRadius: 18, overflow: 'hidden', border: '1 solid #314469', + position: 'relative', + justifyContent: 'center', + alignItems: 'center', }, coverPhoto: { + position: 'absolute', + top: 0, + left: 0, width: '100%', - height: 228, + height: '100%', objectFit: 'cover', + opacity: 0.32, + }, + coverPhotoInner: { + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 18, + }, + coverPhotoLogo: { + width: 96, + height: 96, + objectFit: 'contain', + marginBottom: 10, + }, + coverPhotoLabel: { + color: '#dbe7ff', + fontSize: 10, + textAlign: 'center', + lineHeight: 1.4, }, coverMainLogo: { width: 120, @@ -229,8 +257,8 @@ const styles = StyleSheet.create({ backgroundColor: '#ffffff', border: '1 solid #e2e8f0', borderRadius: 10, - padding: 14, - marginBottom: 14, + padding: 12, + marginBottom: 12, }, memberTableSection: { marginTop: 6, @@ -246,7 +274,7 @@ const styles = StyleSheet.create({ flexDirection: 'row', justifyContent: 'space-between', borderBottom: '1 solid #f1f5f9', - paddingVertical: 5, + paddingVertical: 4, }, keyLabel: { fontSize: 9, @@ -327,9 +355,9 @@ const styles = StyleSheet.create({ backgroundColor: '#f8fafc', }, tableCell: { - paddingVertical: 7, + paddingVertical: 5, paddingHorizontal: 8, - fontSize: 8.5, + fontSize: 8, color: '#0f172a', }, tableHeadCell: { @@ -587,6 +615,12 @@ const SessionReportDocument: React.FC = ({ report }) + + + + End-of-session attendance, marks, and member performance for the selected section. + + @@ -773,7 +807,10 @@ const SessionReportDocument: React.FC = ({ report }) ))} {report.members.flatMap((member) => { - const continuationChunks = chunk(member.meetings.slice(14), 22); + const continuationChunks = chunk( + member.meetings.slice(MEMBER_DETAIL_FIRST_PAGE_ROWS), + MEMBER_DETAIL_CONTINUED_ROWS, + ); return [ @@ -868,7 +905,7 @@ const SessionReportDocument: React.FC = ({ report }) Behaviour )} - {member.meetings.slice(0, 14).map((meeting, index) => ( + {member.meetings.slice(0, MEMBER_DETAIL_FIRST_PAGE_ROWS).map((meeting, index) => ( {formatDate(meeting.date)} {meeting.attended ? 'Present' : 'Absent'} From 84dd34c0c756c12327bc28163cb34c58dc0b6aa4 Mon Sep 17 00:00:00 2001 From: harrisonk0 Date: Sun, 22 Mar 2026 21:40:59 +0000 Subject: [PATCH 2/2] Address PDF review feedback --- ARCHITECTURE.md | 1 + components/SessionReportModal.tsx | 11 +++++++---- components/reports/SessionReportDocument.tsx | 7 ++++--- components/reports/reportConstants.ts | 2 ++ 4 files changed, 14 insertions(+), 7 deletions(-) create mode 100644 components/reports/reportConstants.ts diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ddf93a4..827c4a8 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -49,6 +49,7 @@ flowchart LR - `components/*` implements pages and shared UI. - `components/reports/*` contains the branded session PDF document used by the dashboard export flow. - Navigation is app-state driven rather than router-driven. +- `@react-pdf/renderer` documents are the styling exception: those files use `StyleSheet.create()` and PDF-specific inline layout values because the PDF renderer does not support the app's Tailwind/PostCSS runtime classes. ### 2. Hook Layer diff --git a/components/SessionReportModal.tsx b/components/SessionReportModal.tsx index c2787a9..f613561 100644 --- a/components/SessionReportModal.tsx +++ b/components/SessionReportModal.tsx @@ -3,6 +3,10 @@ import { PDFDownloadLink } from '@react-pdf/renderer'; import Modal from './Modal'; import SessionReportDocument from './reports/SessionReportDocument'; +import { + MEMBER_DETAIL_CONTINUED_ROWS, + MEMBER_DETAIL_FIRST_PAGE_ROWS, +} from './reports/reportConstants'; import { buildSessionReportData, getSectionDateRange } from '../services/reporting/sessionReport'; import type { Boy, Section } from '../types'; @@ -13,9 +17,6 @@ interface SessionReportModalProps { onClose: () => void; } -const MEMBER_DETAIL_FIRST_PAGE_ROWS = 18; -const MEMBER_DETAIL_CONTINUED_ROWS = 26; - const formatDate = (value: string) => new Date(`${value}T00:00:00`).toLocaleDateString(undefined, { day: 'numeric', @@ -68,7 +69,9 @@ const SessionReportModal: React.FC = ({ + 1 + Math.max(1, Math.ceil(report.meetings.length / 18)) + 1 - + Math.max(1, Math.ceil(report.members.length / (activeSection === 'junior' ? 14 : 16))) + + (report.members.length === 0 + ? 0 + : Math.max(1, Math.ceil(report.members.length / (activeSection === 'junior' ? 14 : 16)))) + report.members.reduce( (sum, member) => sum + 1 + Math.ceil(Math.max(member.meetings.length - MEMBER_DETAIL_FIRST_PAGE_ROWS, 0) / MEMBER_DETAIL_CONTINUED_ROWS), diff --git a/components/reports/SessionReportDocument.tsx b/components/reports/SessionReportDocument.tsx index c1f30a0..c05f694 100644 --- a/components/reports/SessionReportDocument.tsx +++ b/components/reports/SessionReportDocument.tsx @@ -13,15 +13,16 @@ import bbLogo from '../../assets/branding/bb-logo.png'; import bbBackground from '../../assets/branding/bb-background.jpg'; import companyLogo from '../../assets/branding/company-logo.png'; import juniorLogo from '../../assets/branding/junior-logo.png'; +import { + MEMBER_DETAIL_CONTINUED_ROWS, + MEMBER_DETAIL_FIRST_PAGE_ROWS, +} from './reportConstants'; const BB_LOGO_URL = bbLogo; const BB_BACKGROUND_URL = bbBackground; const COMPANY_LOGO_URL = companyLogo; const JUNIOR_LOGO_URL = juniorLogo; -const MEMBER_DETAIL_FIRST_PAGE_ROWS = 18; -const MEMBER_DETAIL_CONTINUED_ROWS = 26; - const styles = StyleSheet.create({ page: { backgroundColor: '#f8fafc', diff --git a/components/reports/reportConstants.ts b/components/reports/reportConstants.ts new file mode 100644 index 0000000..b1928ed --- /dev/null +++ b/components/reports/reportConstants.ts @@ -0,0 +1,2 @@ +export const MEMBER_DETAIL_FIRST_PAGE_ROWS = 18; +export const MEMBER_DETAIL_CONTINUED_ROWS = 26;