Spaces:
Sleeping
Sleeping
File size: 5,117 Bytes
432e043 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | """
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
|