diff --git a/packages/ui/src/bodies/EventHoursBlock.tsx b/packages/ui/src/bodies/EventHoursBlock.tsx index f2cfa3a1..57a16f90 100644 --- a/packages/ui/src/bodies/EventHoursBlock.tsx +++ b/packages/ui/src/bodies/EventHoursBlock.tsx @@ -11,7 +11,7 @@ import { formatHoursDisplay } from "./formatHours" import { hoursReceiptState, type HoursReceiptState } from "./eventLifecycle" import { slotWindow } from "./eventSlotsModel" import { LogHoursEditor, type LoggedHoursEntry } from "./LogHoursEditor" -import type { HoursCleanup } from "./hoursEntries" +import { hoursRowSubject, type HoursCleanup } from "./hoursEntries" export interface EventHoursBlockProps { cleanupId: string @@ -195,12 +195,15 @@ function HoursSummaryCard({ {rosterSettled ? ( {entries.map((entry) => { - const attendee = byId.get(entry.userId) + const subject = hoursRowSubject(entry, byId) + const attendee = subject.kind === "attendee" ? subject.attendee : undefined const name = attendee ? user && attendee.id === user.id ? t("going.you") : attendee.name - : t("log_hours.row_unknown") + : subject.kind === "official" + ? t("log_hours.row_official") + : t("log_hours.row_unknown") return ( { expect(code(hoursBlock)).toContain("log_hours.row_unknown") }) + it("labels an operator-credited non-member as credited by CivFix, not as a former attendee", () => { + const source = code(hoursBlock) + expect(source).toContain("hoursRowSubject(entry, byId)") + expect(source).toContain("log_hours.row_official") + }) + it("uses the WCAG-safe coral ink token and never the fill token for text", () => { expect(code(hoursBlock)).not.toMatch(/theme\.colors\.accent\b(?!Text)/) }) diff --git a/packages/ui/src/bodies/__tests__/hoursEntries.test.ts b/packages/ui/src/bodies/__tests__/hoursEntries.test.ts index 56ef7c6f..ff0a7c60 100644 --- a/packages/ui/src/bodies/__tests__/hoursEntries.test.ts +++ b/packages/ui/src/bodies/__tests__/hoursEntries.test.ts @@ -4,6 +4,7 @@ import type { EventSlotDTO } from "@civfix/shared" import { parseHoursDraft, hoursDraftValid, + hoursRowSubject, buildHoursEntries, plannedEventHours, seedHoursDrafts, @@ -269,3 +270,27 @@ describe("suggestedHoursFor", () => { expect(hoursDraftValid(formatHours(suggestion?.hours ?? 0), MAX)).toBe(true) }) }) + +describe("hoursRowSubject", () => { + const roster = new Map([["a", { id: "a", name: "Ada" }]]) + + it("names a roster member even when an operator credited their row", () => { + expect(hoursRowSubject({ userId: "a" }, roster)).toEqual({ + kind: "attendee", + attendee: { id: "a", name: "Ada" }, + }) + expect(hoursRowSubject({ userId: "a", creditedByOfficial: true }, roster).kind).toBe("attendee") + }) + + it("attributes an operator-credited non-member to CivFix rather than calling them a former attendee", () => { + expect(hoursRowSubject({ userId: "z", creditedByOfficial: true }, roster)).toEqual({ kind: "official" }) + }) + + it("keeps the former-attendee label for a host-credited row whose user left the roster", () => { + expect(hoursRowSubject({ userId: "z", creditedByOfficial: false }, roster)).toEqual({ kind: "former" }) + }) + + it("treats an absent flag from an older server as a host credit", () => { + expect(hoursRowSubject({ userId: "z" }, roster)).toEqual({ kind: "former" }) + }) +}) diff --git a/packages/ui/src/bodies/hoursEntries.ts b/packages/ui/src/bodies/hoursEntries.ts index b4d95e2c..97838be5 100644 --- a/packages/ui/src/bodies/hoursEntries.ts +++ b/packages/ui/src/bodies/hoursEntries.ts @@ -72,3 +72,19 @@ export function seedHoursDrafts( for (const entry of entries) drafts[entry.userId] = formatHours(entry.hours) return drafts } + +export type HoursRowSubject = + | { kind: "attendee"; attendee: A } + | { kind: "official" } + | { kind: "former" } + +export function hoursRowSubject( + entry: { userId: string; creditedByOfficial?: boolean }, + rosterById: ReadonlyMap, +): HoursRowSubject { + const attendee = rosterById.get(entry.userId) + if (attendee !== undefined) return { kind: "attendee", attendee } + // An operator can credit someone who never joined the roster; calling them a "former attendee" + // would tell the host something false about who was at their event. + return entry.creditedByOfficial === true ? { kind: "official" } : { kind: "former" } +} diff --git a/packages/ui/src/i18n/locales/de/event-detail.json b/packages/ui/src/i18n/locales/de/event-detail.json index f3eb1901..b0ae084b 100644 --- a/packages/ui/src/i18n/locales/de/event-detail.json +++ b/packages/ui/src/i18n/locales/de/event-detail.json @@ -107,6 +107,7 @@ "summary_edit_a11y": "Erfasste Freiwilligenstunden bearbeiten", "row_logged": "erfasst {{ago}}", "row_unknown": "Ehemalige teilnehmende Person", + "row_official": "Von CivFix gutgeschrieben", "blank_hint": "Lass eine Zeile leer, wenn jemand nicht da war.", "blank_hint_edit": "Gib eine neue Zahl ein, um bereits gutgeschriebene Stunden zu korrigieren. Eine leere Zeile wird übersprungen: sie zu leeren entfernt bereits gutgeschriebene Stunden nicht.", "self_note": "Deine eigenen Stunden stehen hier nicht. Eine andere gastgebende Person muss sie dir gutschreiben.", diff --git a/packages/ui/src/i18n/locales/en/event-detail.json b/packages/ui/src/i18n/locales/en/event-detail.json index e0728488..087f96c7 100644 --- a/packages/ui/src/i18n/locales/en/event-detail.json +++ b/packages/ui/src/i18n/locales/en/event-detail.json @@ -107,6 +107,7 @@ "summary_edit_a11y": "Edit the volunteer hours you logged", "row_logged": "logged {{ago}}", "row_unknown": "Former attendee", + "row_official": "Credited by CivFix", "blank_hint": "Leave a row blank for anyone who did not attend.", "blank_hint_edit": "Type a new number to correct hours already credited. A blank row is skipped: clearing it does not remove hours someone already has.", "self_note": "Your own hours aren't listed here. Another host has to credit you.", diff --git a/packages/ui/src/i18n/locales/es/event-detail.json b/packages/ui/src/i18n/locales/es/event-detail.json index 8c764165..e9e88423 100644 --- a/packages/ui/src/i18n/locales/es/event-detail.json +++ b/packages/ui/src/i18n/locales/es/event-detail.json @@ -107,6 +107,7 @@ "summary_edit_a11y": "Editar las horas de voluntariado registradas", "row_logged": "registrado {{ago}}", "row_unknown": "Antiguo asistente", + "row_official": "Acreditado por CivFix", "blank_hint": "Deja una fila en blanco para quien no haya asistido.", "blank_hint_edit": "Escribe un número nuevo para corregir las horas ya acreditadas. Una fila en blanco se omite: borrarla no elimina las horas que ya se acreditaron.", "self_note": "Tus propias horas no aparecen aquí. Otra persona anfitriona tiene que acreditártelas.", diff --git a/packages/ui/src/i18n/locales/ko/event-detail.json b/packages/ui/src/i18n/locales/ko/event-detail.json index 457d490c..5473aba7 100644 --- a/packages/ui/src/i18n/locales/ko/event-detail.json +++ b/packages/ui/src/i18n/locales/ko/event-detail.json @@ -107,6 +107,7 @@ "summary_edit_a11y": "기록한 봉사 시간 수정", "row_logged": "{{ago}} 기록", "row_unknown": "이전 참석자", + "row_official": "CivFix에서 인정", "blank_hint": "참석하지 않은 사람은 비워 두세요.", "blank_hint_edit": "이미 인정된 시간을 수정하려면 새 숫자를 입력하세요. 빈 칸은 건너뜁니다. 칸을 비워도 이미 인정된 시간은 사라지지 않습니다.", "self_note": "본인의 시간은 여기에 표시되지 않습니다. 다른 주최자가 인정해 주어야 합니다.",