Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions packages/ui/src/bodies/EventHoursBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -195,12 +195,15 @@ function HoursSummaryCard({
{rosterSettled ? (
<View style={styles.people}>
{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 (
<View key={entry.userId} style={styles.personRow}>
<Avatar
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,12 @@ describe("EventHoursBlock renders inside its host's scroller", () => {
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)/)
})
Expand Down
25 changes: 25 additions & 0 deletions packages/ui/src/bodies/__tests__/hoursEntries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { EventSlotDTO } from "@civfix/shared"
import {
parseHoursDraft,
hoursDraftValid,
hoursRowSubject,
buildHoursEntries,
plannedEventHours,
seedHoursDrafts,
Expand Down Expand Up @@ -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" })
})
})
16 changes: 16 additions & 0 deletions packages/ui/src/bodies/hoursEntries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,19 @@ export function seedHoursDrafts(
for (const entry of entries) drafts[entry.userId] = formatHours(entry.hours)
return drafts
}

export type HoursRowSubject<A> =
| { kind: "attendee"; attendee: A }
| { kind: "official" }
| { kind: "former" }

export function hoursRowSubject<A>(
entry: { userId: string; creditedByOfficial?: boolean },
rosterById: ReadonlyMap<string, A>,
): HoursRowSubject<A> {
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" }
}
1 change: 1 addition & 0 deletions packages/ui/src/i18n/locales/de/event-detail.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/i18n/locales/en/event-detail.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/i18n/locales/es/event-detail.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/i18n/locales/ko/event-detail.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
"summary_edit_a11y": "기록한 봉사 시간 수정",
"row_logged": "{{ago}} 기록",
"row_unknown": "이전 참석자",
"row_official": "CivFix에서 인정",
"blank_hint": "참석하지 않은 사람은 비워 두세요.",
"blank_hint_edit": "이미 인정된 시간을 수정하려면 새 숫자를 입력하세요. 빈 칸은 건너뜁니다. 칸을 비워도 이미 인정된 시간은 사라지지 않습니다.",
"self_note": "본인의 시간은 여기에 표시되지 않습니다. 다른 주최자가 인정해 주어야 합니다.",
Expand Down