Task #120 — Make the Executive Meetings print page responsive (no clipped attendee names)

Problem:
artifacts/tx-os/src/pages/executive-meetings-print.tsx built the schedule
as one monolithic <table> with fixed percentage widths (8/32/40/20), no
min-width / overflow guard, no word-break rules, and joined every
attendee into a single comma-separated string. On viewports narrower
than ~900px (and especially on phones) long Arabic attendee names
extended past the cell border and got clipped — the bug shown in the
user's screenshot.

Approach (artifacts/tx-os/src/pages/executive-meetings-print.tsx only):

1. Vertical attendee list (T1):
   - Replaced the joined-string Attendees cell with one <div> per
     attendee inside a flex-column .em-print-attendees wrapper.
     Numbered "1- Name", "2- Name (Title)" etc. — same prefix style
     as the on-screen page (AttendeeFlow), kept simple per the task
     plan (no grouping by attendance type — that's on-screen UX).
   - Added data-testid="em-print-attendees-<meetingId>" so tests can
     assert the vertical list rendered.

2. Cell wrap protections (T1):
   - .em-print-table th, .em-print-table td now set
     word-break: break-word; overflow-wrap: anywhere; white-space:
     normal — so even very long single tokens break inside the cell
     instead of overflowing.

3. Screen scroll guard + responsive widths (T2):
   - Wrapped the <table> in a div.em-print-scroll with
     overflow-x:auto, -webkit-overflow-scrolling:touch.
   - Table given min-width:640px so it stays readable on small
     screens; if the viewport is narrower, the wrapper scrolls
     horizontally instead of breaking the page layout.
   - Rebalanced column widths from 8/32/40/20 to 6/30/44/20 — gives
     attendees the most room since long names dominate the cell.

4. Print-mode overrides (T2):
   - @media print resets .em-print-scroll overflow to visible,
     .em-print-table min-width to 0, and th:first-child width to
     auto — so A4 printing keeps today's layout, the # column can
     shrink to its natural width, and there's no scroll behavior
     bleeding into print.

5. Scope hygiene (T3):
   - Did NOT touch executive-meetings.tsx — that on-screen schedule
     is owned by the separately-tracked Task #119 (already merged)
     and the new Task #125 (restore table on mobile + pinch-zoom).
     The diff for this task is exactly one file plus this commit
     message.

Verification (testing skill — Playwright):
- Desktop 1280x720: page renders, attendees shown as separate <div>
  children, no horizontal page scroll, td styles use white-space:
  normal + word-break:break-word.
- Phone 360x800: no horizontal PAGE scroll; the .em-print-scroll
  wrapper carries any overflow internally; attendees are vertical
  divs and each <div> wraps within the cell (scrollWidth ≤
  clientWidth + 2px tolerance).
- Tablet 768x1024: no horizontal page scroll (viewport ≥ table
  min-width).
- Print emulation (tablet + phone): .em-print-scroll computed
  overflow-x === "visible", .em-print-table computed min-width ===
  "0px" — A4 layout preserved.
- Architect review: PASS, no blocking issues.

Out of scope (per task brief):
- Real PDF generation (Task #111).
- The on-screen schedule layout (Tasks #119 / #125).
- Data model, columns, theme, RBAC.
This commit is contained in:
riyadhafraa
2026-04-29 07:09:50 +00:00
parent be4ecb30bb
commit 3994ccdb0c
2 changed files with 91 additions and 44 deletions
@@ -116,13 +116,31 @@ export default function ExecutiveMeetingsPrintPage() {
.no-print { display: none !important; }
body { background: white; }
@page { size: A4; margin: 14mm; }
/* On A4 the table always fits the page; drop the screen-only
scroll guard so the layout stays identical to today and the
# column can shrink to its natural width. */
.em-print-scroll { overflow: visible !important; }
.em-print-table { min-width: 0 !important; }
.em-print-table th:first-child { width: auto !important; }
}
/* Screen-only horizontal scroll guard — prevents the table from
breaking the page layout on phones (<640px). */
.em-print-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.em-print-table {
border-collapse: collapse;
width: 100%;
min-width: 640px;
}
.em-print-table { border-collapse: collapse; width: 100%; }
.em-print-table th, .em-print-table td {
border: 1px solid #d1d5db;
padding: 8px 10px;
text-align: center;
vertical-align: middle;
/* Long Arabic names have few break opportunities; force wrap so
the cell never overflows its border. */
word-break: break-word;
overflow-wrap: anywhere;
white-space: normal;
}
.em-print-table th {
background: #0B1E3F;
@@ -130,6 +148,17 @@ export default function ExecutiveMeetingsPrintPage() {
font-weight: 600;
}
.em-print-table tr.hl td { background: #fecaca; }
.em-print-attendees {
display: flex;
flex-direction: column;
gap: 2px;
text-align: center;
line-height: 1.4;
}
.em-print-attendees .em-print-attendee-num {
color: #6b7280;
margin-inline-end: 4px;
}
`}</style>
<div className="no-print flex items-center justify-between px-6 py-3 border-b border-gray-200 bg-gray-50">
@@ -164,49 +193,67 @@ export default function ExecutiveMeetingsPrintPage() {
) : meetings.length === 0 ? (
<div className="text-gray-500 text-sm text-center">{t.none}</div>
) : (
<table className="em-print-table" dir={isRtl ? "rtl" : "ltr"}>
<thead>
<tr>
<th style={{ width: "8%" }}>{t.no}</th>
<th style={{ width: "32%" }}>{t.meeting}</th>
<th style={{ width: "40%" }}>{t.attendees}</th>
<th style={{ width: "20%" }}>{t.time}</th>
</tr>
</thead>
<tbody>
{meetings.map((m) => {
const title = isRtl
? m.titleAr
: m.titleEn || m.titleAr;
const attendees = (m.attendees ?? [])
.map((a) => (a.title ? `${a.name} (${a.title})` : a.name))
.join("، ");
const time =
m.startTime && m.endTime
? `${m.startTime.slice(0, 5)} ${m.endTime.slice(0, 5)}`
: m.startTime
? m.startTime.slice(0, 5)
: "—";
return (
<tr key={m.id} className={m.isHighlighted ? "hl" : ""}>
<td>{m.dailyNumber}</td>
<td>
<div style={{ fontWeight: 600 }}>{title}</div>
{m.location ? (
<div style={{ fontSize: 12, color: "#4b5563" }}>
{m.location}
</div>
) : null}
</td>
<td style={{ textAlign: "center" }}>
{attendees || "—"}
</td>
<td>{time}</td>
</tr>
);
})}
</tbody>
</table>
<div className="em-print-scroll" data-testid="em-print-scroll">
<table className="em-print-table" dir={isRtl ? "rtl" : "ltr"}>
<thead>
<tr>
{/* Slightly rebalanced from 8/32/40/20: give attendees the
most room since long Arabic names dominate the cell. */}
<th style={{ width: "6%" }}>{t.no}</th>
<th style={{ width: "30%" }}>{t.meeting}</th>
<th style={{ width: "44%" }}>{t.attendees}</th>
<th style={{ width: "20%" }}>{t.time}</th>
</tr>
</thead>
<tbody>
{meetings.map((m) => {
const title = isRtl
? m.titleAr
: m.titleEn || m.titleAr;
const attendees = m.attendees ?? [];
const time =
m.startTime && m.endTime
? `${m.startTime.slice(0, 5)} ${m.endTime.slice(0, 5)}`
: m.startTime
? m.startTime.slice(0, 5)
: "—";
return (
<tr key={m.id} className={m.isHighlighted ? "hl" : ""}>
<td>{m.dailyNumber}</td>
<td>
<div style={{ fontWeight: 600 }}>{title}</div>
{m.location ? (
<div style={{ fontSize: 12, color: "#4b5563" }}>
{m.location}
</div>
) : null}
</td>
<td>
{attendees.length === 0 ? (
"—"
) : (
<div
className="em-print-attendees"
data-testid={`em-print-attendees-${m.id}`}
>
{attendees.map((a, idx) => (
<div key={a.id ?? idx}>
<span className="em-print-attendee-num">
{idx + 1}-
</span>
{a.title ? `${a.name} (${a.title})` : a.name}
</div>
))}
</div>
)}
</td>
<td>{time}</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
</div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB