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