Task #207: custom subheadings inside executive-meeting attendee cells

Adds a new `kind` column (`varchar(16) NOT NULL DEFAULT 'person'`) on
`executive_meeting_attendees` so users can interleave free-text section
labels with person rows in a meeting's attendee list. Subheadings are
excluded from the running attendee number and from the per-meeting
attendee count surface, but reorder and delete identically to person
rows.

DB
- New `kind` column in `lib/db/src/schema/executive-meetings.ts`,
  defaulting to `"person"`. Applied via direct SQL because
  `drizzle-kit push` trips on a pre-existing duplicate-row issue in
  `app_permissions` (already documented in replit.md).

API (artifacts/api-server/src/routes/executive-meetings.ts)
- `attendeeSchema` accepts `kind: z.enum(["person","subheading"])` with
  default `"person"`.
- All 4 insert paths round-trip `kind`: POST create, PATCH meeting
  update (attendees replacement), PUT `/attendees`, and duplicate.
- `pdf-renderer` mapper forwards `kind`. `PdfMeetingAttendee.kind`
  typed as `string | null` to match the DB column shape.

Frontend (artifacts/tx-os/src/pages/executive-meetings.tsx)
- `AttendeeFlow` renders subheadings on a separate full-width row
  (`basis-full`, semibold, centered) and increments the running
  person index only for `kind === "person"`. Pending ghost row branches
  on `pendingKind`.
- Inline "+ subheading" chip via `onStartAdd(type, "subheading")`.
- Manage dialog: addSubheading button, subheading rows hide the title
  field, show a kind badge, and reorder/delete identically. Manage
  list summary count filters to person rows only (architect fix).
- Print page renders subheadings as `.em-print-subheading` and skips
  them in the running counter.
- New locale keys under `executiveMeetings.schedule` and
  `executiveMeetings.manage.attendees` in both `ar.json` and `en.json`.

PDF
- Subheadings print as `— label —` and never advance `personIdx`.

Tests
- New spec `executive-meetings-attendee-subheadings.spec.mjs` seeds
  mixed person+subheading rows and asserts (a) the subheading row
  renders, (b) numbering stays `1-`, `2-`, `3-` with a subheading
  wedged between persons, (c) zero-subheading meetings keep legacy
  numbering. Runs in both AR and EN. All 4 cases pass.

Code review
- Architect found one regression (Manage list summary count included
  subheadings) — fixed.
This commit is contained in:
riyadhafraa
2026-04-30 11:45:59 +00:00
parent 986bb8e7b2
commit 89df2385a8
9 changed files with 808 additions and 168 deletions
+8
View File
@@ -74,12 +74,20 @@ export const executiveMeetingAttendeesTable = pgTable(
.references(() => executiveMeetingsTable.id, { onDelete: "cascade" }),
// attendee name holds sanitized rich-text HTML so it needs unbounded
// length. Plain-text rows still render correctly (no tags = no parse).
// For `kind = "subheading"` rows, this column stores the user-written
// section label (free text, single value rendered as-is in AR and EN).
name: text("name").notNull(),
title: varchar("title", { length: 200 }),
attendanceType: varchar("attendance_type", { length: 32 })
.notNull()
.default("internal"),
sortOrder: integer("sort_order").notNull().default(0),
// "person" — a real attendee row (default; matches all pre-existing
// data after backfill).
// "subheading" — a user-defined section label inserted between attendees
// inside the same attendance-type group. Numbering, count,
// and virtual-platform extraction MUST ignore these rows.
kind: varchar("kind", { length: 16 }).notNull().default("person"),
},
(t) => ({
meetingIdx: index("executive_meeting_attendees_meeting_idx").on(t.meetingId),