Task #357: Show attendee groups correctly in the upcoming meeting alert
The upcoming meeting alert's details panel was grouping attendees by the `attendanceType` field, but in practice every attendee is stored as type=internal regardless of their actual group. Users define groups via subheading rows in the schedule editor (e.g. "الحضور الخارجي", "الحضور الداخلي"), but the alert stripped subheadings and lumped everyone under "داخلي". Fix: Rewrote the grouping logic in DetailsPanel to walk through the attendees array in order, using subheading rows as natural group separators. Each subheading becomes a bold, clearly visible group header (text-xs font-bold) instead of the old tiny 10px/60% opacity labels. Person rows are listed under their preceding subheading. Edge cases handled: - Persons before any subheading: shown without a group header - No subheadings at all: flat list without misleading "داخلي" label - Empty subheading groups (heading with no persons): skipped - Person count still counts only persons, not subheadings Changed file: - artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx (DetailsPanel component, lines ~1744-1885)
This commit is contained in:
@@ -1741,27 +1741,32 @@ function DetailsPanel({
|
|||||||
dividerColor,
|
dividerColor,
|
||||||
sectionBg,
|
sectionBg,
|
||||||
}: DetailsPanelProps) {
|
}: DetailsPanelProps) {
|
||||||
// Group attendees by attendanceType (internal/external/virtual) and
|
// Walk through attendees in order. Subheading rows act as group
|
||||||
// skip subheading rows — those are user-defined section labels in the
|
// headers (e.g. "الحضور الخارجي", "الحضور الداخلي"); person rows
|
||||||
// schedule editor, not actual people, so they shouldn't appear in a
|
// following a subheading belong to that group. If persons appear
|
||||||
// "who is attending" details panel.
|
// before any subheading they land in a header-less group.
|
||||||
const persons = (meeting.attendees ?? []).filter(
|
type AttendeeGroup = { heading: string | null; members: Attendee[] };
|
||||||
(a) => a.kind !== "subheading" && a.name && a.name.trim().length > 0,
|
const attendeeGroups: AttendeeGroup[] = [];
|
||||||
);
|
let currentGroup: AttendeeGroup = { heading: null, members: [] };
|
||||||
const groups: Record<string, Attendee[]> = {
|
for (const a of meeting.attendees ?? []) {
|
||||||
internal: [],
|
const cleanName = (a.name ?? "").replace(/<[^>]+>/g, "").trim();
|
||||||
external: [],
|
if (!cleanName) continue;
|
||||||
virtual: [],
|
if (a.kind === "subheading") {
|
||||||
};
|
if (currentGroup.heading !== null || currentGroup.members.length > 0) {
|
||||||
for (const p of persons) {
|
attendeeGroups.push(currentGroup);
|
||||||
const key =
|
}
|
||||||
p.attendanceType === "internal" ||
|
currentGroup = { heading: cleanName, members: [] };
|
||||||
p.attendanceType === "external" ||
|
} else {
|
||||||
p.attendanceType === "virtual"
|
currentGroup.members.push(a);
|
||||||
? p.attendanceType
|
}
|
||||||
: "internal";
|
|
||||||
groups[key].push(p);
|
|
||||||
}
|
}
|
||||||
|
if (currentGroup.heading !== null || currentGroup.members.length > 0) {
|
||||||
|
attendeeGroups.push(currentGroup);
|
||||||
|
}
|
||||||
|
const personCount = attendeeGroups.reduce(
|
||||||
|
(sum, g) => sum + g.members.length,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
const safeUrl = (() => {
|
const safeUrl = (() => {
|
||||||
const raw = (meeting.meetingUrl ?? "").trim();
|
const raw = (meeting.meetingUrl ?? "").trim();
|
||||||
@@ -1831,16 +1836,14 @@ function DetailsPanel({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attendees row is always shown (even when empty) so the user can
|
|
||||||
// distinguish "no one yet" from "field not loaded".
|
|
||||||
rows.push({
|
rows.push({
|
||||||
key: "attendees",
|
key: "attendees",
|
||||||
icon: <Users className="h-3.5 w-3.5 opacity-70" aria-hidden="true" />,
|
icon: <Users className="h-3.5 w-3.5 opacity-70" aria-hidden="true" />,
|
||||||
label: t("executiveMeetings.alert.detailsAttendees", {
|
label: t("executiveMeetings.alert.detailsAttendees", {
|
||||||
n: persons.length,
|
n: personCount,
|
||||||
}),
|
}),
|
||||||
value:
|
value:
|
||||||
persons.length === 0 ? (
|
personCount === 0 ? (
|
||||||
<div className="opacity-80" data-testid="alert-details-no-attendees">
|
<div className="opacity-80" data-testid="alert-details-no-attendees">
|
||||||
{t("executiveMeetings.alert.detailsNoAttendees")}
|
{t("executiveMeetings.alert.detailsNoAttendees")}
|
||||||
</div>
|
</div>
|
||||||
@@ -1849,27 +1852,23 @@ function DetailsPanel({
|
|||||||
className="max-h-40 overflow-y-auto"
|
className="max-h-40 overflow-y-auto"
|
||||||
data-testid="alert-details-attendees"
|
data-testid="alert-details-attendees"
|
||||||
>
|
>
|
||||||
{(["internal", "external", "virtual"] as const).map((g) => {
|
{attendeeGroups.map((group, gIdx) => {
|
||||||
const list = groups[g];
|
if (group.members.length === 0) return null;
|
||||||
if (list.length === 0) return null;
|
|
||||||
return (
|
return (
|
||||||
<div key={g} className="mb-1.5 last:mb-0">
|
<div key={`group-${gIdx}`} className="mb-2 last:mb-0">
|
||||||
<div className="text-[10px] font-semibold uppercase tracking-wide opacity-60">
|
{group.heading ? (
|
||||||
{t(`executiveMeetings.alert.detailsGroup.${g}`)}
|
<div className="mb-0.5 text-xs font-bold">
|
||||||
</div>
|
{group.heading}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<ul className="list-disc ps-4 leading-snug">
|
<ul className="list-disc ps-4 leading-snug">
|
||||||
{list.map((p, idx) => {
|
{group.members.map((p, idx) => {
|
||||||
// Attendee names are stored as sanitized rich-text
|
|
||||||
// HTML; strip every tag for the popup. If the input
|
|
||||||
// is tag-only (e.g. "<p></p>") we render nothing
|
|
||||||
// rather than fall back to the raw value, so HTML
|
|
||||||
// can never appear here.
|
|
||||||
const cleanName = p.name
|
const cleanName = p.name
|
||||||
.replace(/<[^>]+>/g, "")
|
.replace(/<[^>]+>/g, "")
|
||||||
.trim();
|
.trim();
|
||||||
if (!cleanName) return null;
|
if (!cleanName) return null;
|
||||||
return (
|
return (
|
||||||
<li key={`${g}-${p.id ?? idx}-${p.name}`}>
|
<li key={`${gIdx}-${p.id ?? idx}-${p.name}`}>
|
||||||
{cleanName}
|
{cleanName}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user