""" ChillTrack API — room-temperature log PDF export. Recreates the paper form: one row per unit, one column per daily slot. Built entirely in memory with an io.BytesIO() buffer (see sqx-api's cohort_reports.build_pdf pattern) — nothing is ever written to disk. """ import io from reportlab.lib import colors from reportlab.lib.pagesizes import A4, landscape from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet from reportlab.lib.units import mm from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle from slots import SLOT_TIMES INK = colors.HexColor("#172033") MUTED = colors.HexColor("#64748B") BRAND = colors.HexColor("#1D4ED8") OUT_OF_TOLERANCE_BG = colors.HexColor("#FDE2E1") OUT_OF_TOLERANCE_TEXT = colors.HexColor("#B42318") HEADER_BG = colors.HexColor("#1A1A2E") def _styles(): base = getSampleStyleSheet() return { "title": ParagraphStyle("title", parent=base["Title"], fontName="Helvetica-Bold", fontSize=16, leading=19, textColor=INK), "subtitle": ParagraphStyle("subtitle", parent=base["Normal"], fontSize=9, leading=12, textColor=MUTED), "small": ParagraphStyle("small", parent=base["Normal"], fontSize=7.5, leading=10, textColor=MUTED), } def build_room_temperature_pdf(units: list, readings: list, shift_date: str, roster: dict | None = None) -> io.BytesIO: """Builds the room-temperature log PDF for one shift_date. units: list of ctk_units dicts (each needs id/name/setpoint_c/tolerance_c). readings: list of ctk_readings dicts for this shift_date (any units/slots). roster: optional {slot_label_or_range: [names]} for the footer signature block. Returns a BytesIO positioned at 0, ready for send_file(...). """ buffer = io.BytesIO() styles = _styles() doc = SimpleDocTemplate( buffer, pagesize=landscape(A4), leftMargin=10 * mm, rightMargin=10 * mm, topMargin=10 * mm, bottomMargin=10 * mm, title=f"Room Temperature Log — {shift_date}", ) # Index readings by (unit_id, slot_index) for O(1) cell lookup. by_cell = {} for r in readings: key = (r.get("unit_id"), r.get("slot_index")) by_cell[key] = r story = [ Paragraph("COLCOM — ROOM TEMPERATURE LOG", styles["title"]), Paragraph(f"Shift date: {shift_date} (08:00 to 08:00 next day)", styles["subtitle"]), Spacer(1, 3 * mm), ] header_row = ["Unit (setpoint)"] + SLOT_TIMES table_data = [header_row] cell_styles = [] # list of TableStyle commands for out-of-tolerance cells for row_idx, unit in enumerate(units, start=1): unit_id = unit.get("id") or unit.get("unit_id") label = f"{unit.get('name', '')} ({unit.get('setpoint_c')}°C ±{unit.get('tolerance_c')})" row = [label] for slot_index in range(len(SLOT_TIMES)): reading = by_cell.get((unit_id, slot_index)) if reading is None: row.append("") else: value = reading.get("value_c") row.append(f"{value}" if value is not None else "") if reading.get("within_tolerance") is False: cell_styles.append(("BACKGROUND", (slot_index + 1, row_idx), (slot_index + 1, row_idx), OUT_OF_TOLERANCE_BG)) cell_styles.append(("TEXTCOLOR", (slot_index + 1, row_idx), (slot_index + 1, row_idx), OUT_OF_TOLERANCE_TEXT)) cell_styles.append(("FONTNAME", (slot_index + 1, row_idx), (slot_index + 1, row_idx), "Helvetica-Bold")) table_data.append(row) col_widths = [55 * mm] + [18 * mm] * len(SLOT_TIMES) table = Table(table_data, colWidths=col_widths, repeatRows=1, hAlign="LEFT") base_style = [ ("FONTNAME", (0, 0), (-1, -1), "Helvetica"), ("FONTSIZE", (0, 0), (-1, -1), 7), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), ("ALIGN", (1, 0), (-1, -1), "CENTER"), ("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#CBD5E1")), ("BACKGROUND", (0, 0), (-1, 0), HEADER_BG), ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), ("FONTSIZE", (0, 0), (-1, 0), 7.5), ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#F8FAFC")]), ("LEFTPADDING", (0, 0), (-1, -1), 3), ("RIGHTPADDING", (0, 0), (-1, -1), 3), ("TOPPADDING", (0, 0), (-1, -1), 3), ("BOTTOMPADDING", (0, 0), (-1, -1), 3), ] + cell_styles table.setStyle(TableStyle(base_style)) story.append(table) if roster: story.append(Spacer(1, 5 * mm)) story.append(Paragraph("Shift roster", styles["subtitle"])) for label, names in roster.items(): names_str = ", ".join(names) if isinstance(names, list) else str(names) story.append(Paragraph(f"{label}: {names_str}", styles["small"])) doc.build(story) buffer.seek(0) return buffer