from pathlib import Path import json from reportlab.lib import colors from reportlab.lib.enums import TA_LEFT from reportlab.lib.pagesizes import landscape, legal from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet from reportlab.lib.units import inch from reportlab.platypus import ( BaseDocTemplate, Frame, LongTable, PageBreak, PageTemplate, Paragraph, Spacer, Table, TableStyle, ) DOCS_DIR = Path(__file__).resolve().parent INPUT_PATH = DOCS_DIR / "checked-sites-revision-audit-2026-06-04.json" OUTPUT_PATH = DOCS_DIR / "checked-sites-revision-audit-2026-06-04.pdf" BG = colors.HexColor("#F5F1E8") TEXT = colors.HexColor("#231D14") MUTED = colors.HexColor("#6B6255") ACCENT = colors.HexColor("#B8831F") ACCENT_SOFT = colors.HexColor("#F1E3C2") OK = colors.HexColor("#1F8A4C") OK_SOFT = colors.HexColor("#E5F4EB") WARN = colors.HexColor("#B45309") WARN_SOFT = colors.HexColor("#F7E7D7") REVIEW = colors.HexColor("#2563EB") REVIEW_SOFT = colors.HexColor("#E6EEFF") TODO = colors.HexColor("#7C3AED") TODO_SOFT = colors.HexColor("#F0E8FF") LINE = colors.HexColor("#D7CFC1") def page_template(canvas, doc): canvas.saveState() canvas.setFillColor(BG) canvas.rect(0, 0, doc.pagesize[0], doc.pagesize[1], fill=1, stroke=0) canvas.setFillColor(MUTED) canvas.setFont("Helvetica", 9) canvas.drawRightString(doc.pagesize[0] - 36, 20, f"Page {doc.page}") canvas.restoreState() def pill(text, fg, bg): return Paragraph( ( f'{escape(text)}' ), styles["pill"], ) def escape(value): return ( str(value or "") .replace("&", "&") .replace("<", "<") .replace(">", ">") ) def status_pill(value): value = value or "—" if value == "client_review": return pill(value, REVIEW, REVIEW_SOFT) if value == "client_approved": return pill(value, OK, OK_SOFT) if value == "not_started": return pill(value, TODO, TODO_SOFT) if value == "in_progress": return pill(value, ACCENT, ACCENT_SOFT) if value == "Yes": return pill(value, OK, OK_SOFT) if value == "No": return pill(value, WARN, WARN_SOFT) return pill(value, MUTED, colors.white) def text_para(value, style_name="cell"): return Paragraph(escape(value), styles[style_name]) def build_summary_cards(rows): existing_rows = [row for row in rows if row.get("exists", True) is not False] active_rows = [row for row in existing_rows if row.get("hasActiveRevision") == "Yes"] billed_rows = [row for row in existing_rows if row.get("r00Billed") != "No"] unbilled_rows = [row for row in existing_rows if row.get("r00Billed") == "No"] cards = [ ("Checked Sites", str(len(existing_rows)), "Sites found in the database from your checked list."), ("Active Revisions", str(len(active_rows)), "Checked sites that now have an active R01+."), ("R00 Billed", str(len(billed_rows)), "Checked sites where the new-book unit is already billed."), ("R00 Unbilled", str(len(unbilled_rows)), "Checked sites where R00 is done but not billed yet."), ] card_rows = [] for label, value, subtext in cards: card_rows.append( Table( [ [Paragraph(label.upper(), styles["card_label"])], [Paragraph(value, styles["card_value"])], [Paragraph(subtext, styles["card_sub"])], ], colWidths=[3.0 * inch], style=TableStyle( [ ("BACKGROUND", (0, 0), (-1, -1), colors.white), ("BOX", (0, 0), (-1, -1), 0.8, LINE), ("INNERPADDING", (0, 0), (-1, -1), 10), ("ROUNDEDCORNERS", [10, 10, 10, 10]), ] ), ) ) return Table([card_rows], colWidths=[3.1 * inch] * 4, hAlign="LEFT") def build_table(rows, title, intro): flow = [Paragraph(title, styles["section_title"]), Paragraph(intro, styles["small"]), Spacer(1, 10)] header = [ text_para("Site", "th"), text_para("DB Title", "th"), text_para("Internal #", "th"), text_para("R00 Done", "th"), text_para("R00 Billed", "th"), text_para("Active R#", "th"), text_para("Active Status", "th"), text_para("Completed Revisions", "th"), text_para("Billed Versions", "th"), text_para("Version Summary", "th"), ] data = [header] for row in rows: data.append( [ text_para(row.get("site", ""), "cell_mono"), text_para(row.get("dbTitle", "")), text_para(row.get("internalTaskNumber", ""), "cell_mono"), status_pill(row.get("r00Completed", "No")), status_pill(row.get("r00Billed", "No")), text_para(row.get("activeRevision", "—"), "cell_mono"), status_pill(row.get("activeStatus", "—")), text_para(row.get("completedRevisions", "—")), text_para(row.get("billedVersions", "—")), text_para(row.get("versionSummary", "—")), ] ) table = LongTable( data, colWidths=[ 0.78 * inch, 1.7 * inch, 0.72 * inch, 0.75 * inch, 0.85 * inch, 0.6 * inch, 1.0 * inch, 1.45 * inch, 1.2 * inch, 2.15 * inch, ], repeatRows=1, ) table.setStyle( TableStyle( [ ("BACKGROUND", (0, 0), (-1, 0), colors.white), ("BOX", (0, 0), (-1, -1), 0.8, LINE), ("INNERGRID", (0, 0), (-1, -1), 0.4, LINE), ("VALIGN", (0, 0), (-1, -1), "TOP"), ("TOPPADDING", (0, 0), (-1, -1), 6), ("BOTTOMPADDING", (0, 0), (-1, -1), 6), ("LEFTPADDING", (0, 0), (-1, -1), 6), ("RIGHTPADDING", (0, 0), (-1, -1), 6), ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#FBF8F2")]), ] ) ) flow.append(table) return flow styles = getSampleStyleSheet() styles.add( ParagraphStyle( name="title_main", fontName="Helvetica-Bold", fontSize=24, leading=28, textColor=TEXT, alignment=TA_LEFT, ) ) styles.add( ParagraphStyle( name="eyebrow", fontName="Helvetica-Bold", fontSize=9, leading=11, textColor=MUTED, ) ) styles.add( ParagraphStyle( name="subtitle", fontName="Helvetica", fontSize=11, leading=15, textColor=MUTED, ) ) styles.add( ParagraphStyle( name="section_title", fontName="Helvetica-Bold", fontSize=15, leading=18, textColor=TEXT, spaceAfter=2, ) ) styles.add( ParagraphStyle( name="small", fontName="Helvetica", fontSize=9, leading=12, textColor=MUTED, ) ) styles.add( ParagraphStyle( name="card_label", fontName="Helvetica-Bold", fontSize=8, leading=10, textColor=MUTED, ) ) styles.add( ParagraphStyle( name="card_value", fontName="Helvetica-Bold", fontSize=24, leading=26, textColor=TEXT, ) ) styles.add( ParagraphStyle( name="card_sub", fontName="Helvetica", fontSize=8.5, leading=11, textColor=MUTED, ) ) styles.add( ParagraphStyle( name="th", fontName="Helvetica-Bold", fontSize=8, leading=10, textColor=MUTED, ) ) styles.add( ParagraphStyle( name="cell", fontName="Helvetica", fontSize=8.2, leading=10.5, textColor=TEXT, ) ) styles.add( ParagraphStyle( name="cell_mono", fontName="Courier", fontSize=8.1, leading=10.3, textColor=TEXT, ) ) styles.add( ParagraphStyle( name="pill", fontName="Helvetica-Bold", fontSize=7.5, leading=9, alignment=1, ) ) def main(): rows = json.loads(INPUT_PATH.read_text()) existing_rows = [row for row in rows if row.get("exists", True) is not False] active_rows = [row for row in existing_rows if row.get("hasActiveRevision") == "Yes"] no_active_rows = [row for row in existing_rows if row.get("hasActiveRevision") != "Yes"] doc = BaseDocTemplate( str(OUTPUT_PATH), pagesize=landscape(legal), leftMargin=36, rightMargin=36, topMargin=28, bottomMargin=28, ) frame = Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height, id="main") doc.addPageTemplates([PageTemplate(id="default", frames=[frame], onPage=page_template)]) story = [ Paragraph("FOURGE PORTAL AUDIT", styles["eyebrow"]), Spacer(1, 4), Paragraph("Checked Sites Revision Audit", styles["title_main"]), Spacer(1, 6), Paragraph( "This report treats your checkmark as “R00 was completed,” then separates that from billing and from any newer active revisions. " "It is designed to make client-billing comparison easier at a glance.", styles["subtitle"], ), Spacer(1, 18), build_summary_cards(rows), Spacer(1, 18), ] story.extend( build_table( active_rows, "Checked Sites With Active Revisions", "These are the sites where R00 is done, but there is still a newer revision active or awaiting billing.", ) ) story.append(PageBreak()) story.extend( build_table( no_active_rows, "Checked Sites With No Active Revision", "These are the checked sites that currently do not have a newer active revision.", ) ) doc.build(story) print(OUTPUT_PATH) if __name__ == "__main__": main()