From 3593d84b30a6021615f6e092e7a83ab67439b927 Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Wed, 12 Aug 2026 12:52:29 -0300 Subject: [PATCH] =?UTF-8?q?refactor(C129):=20simplify=20=E2=80=94=20Campai?= =?UTF-8?q?gnNameSubline=20compartilhado=20(a11y=20sr-label),=20guard=20le?= =?UTF-8?q?genda=3D=3Dnome,=20sem=20plumb=20morto,=20spec=20mais=20forte?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../campanha/(app)/dobradinhas/formActions.ts | 5 +++-- .../campanha/(app)/dobradinhas/page.tsx | 11 +++++++---- .../campanha/(app)/pessoas/page.tsx | 5 ++--- .../campaign/shared/CampaignNameSubline.tsx | 19 +++++++++++++++++++ .../stateDeputy/StateDeputyContactSection.tsx | 5 +++-- src/utilities/people/peopleData.ts | 9 +++++++-- src/utilities/people/personDetail.ts | 2 +- src/utilities/stateDeputyData.ts | 7 +++---- .../campaignStateDeputyBallotName.int.spec.ts | 18 ++++++++++++++++++ tests/unit/peopleMerge.unit.spec.ts | 9 +++++++++ 10 files changed, 72 insertions(+), 18 deletions(-) create mode 100644 src/components/campaign/shared/CampaignNameSubline.tsx diff --git a/src/app/(campaign)/campanha/(app)/dobradinhas/formActions.ts b/src/app/(campaign)/campanha/(app)/dobradinhas/formActions.ts index f0b32122..aeafd5ac 100644 --- a/src/app/(campaign)/campanha/(app)/dobradinhas/formActions.ts +++ b/src/app/(campaign)/campanha/(app)/dobradinhas/formActions.ts @@ -43,7 +43,8 @@ const stateDeputyContactSafeMessages = [ const stateDeputyPartySafeMessages = [STATE_DEPUTY_STAFF_MESSAGE] as const -const stateDeputyBallotNameSafeMessages = [STATE_DEPUTY_STAFF_MESSAGE] as const +/** The party and ballotName inline cells share the same refusal vocabulary. */ +const stateDeputyInlineFieldSafeMessages = stateDeputyPartySafeMessages /** Per-field Contact edit for B163 (lista + ficha). */ export const updateStateDeputyContactFormAction = async ( @@ -119,7 +120,7 @@ export const updateStateDeputyBallotNameFormAction = async ( }) return { message: 'Salvo.' } }, - safeMessages: stateDeputyBallotNameSafeMessages, + safeMessages: stateDeputyInlineFieldSafeMessages, genericMessage: 'Não foi possível salvar o nome de legenda. Verifique os dados e tente novamente.', }) diff --git a/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx b/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx index 4d13b525..4e48d4bb 100644 --- a/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx +++ b/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx @@ -13,6 +13,7 @@ import { CampaignTransitionAnchor, } from '@/components/campaign/shared/CampaignListPending' import { CampaignListSheetProvider } from '@/components/campaign/shared/CampaignListSheetHost' +import { CampaignNameSubline } from '@/components/campaign/shared/CampaignNameSubline' import { CampaignTable, CampaignTableHead, @@ -136,10 +137,12 @@ const stateDeputyColumns = ( /> {/* C129 — the ballot name sits OUTSIDE the name cell's click-to-edit region (sibling, not child), so clicking it never opens the name - editor. */} - {row.ballotName ? ( - {row.ballotName} - ) : null} + editor. A legenda identical to the real name is skipped (same + policy as `peopleNameSubline`). */} + ), }, diff --git a/src/app/(campaign)/campanha/(app)/pessoas/page.tsx b/src/app/(campaign)/campanha/(app)/pessoas/page.tsx index 712ae9fa..acf718fa 100644 --- a/src/app/(campaign)/campanha/(app)/pessoas/page.tsx +++ b/src/app/(campaign)/campanha/(app)/pessoas/page.tsx @@ -21,6 +21,7 @@ import { CampaignTransitionAnchor, } from '@/components/campaign/shared/CampaignListPending' import { CampaignListSheetProvider } from '@/components/campaign/shared/CampaignListSheetHost' +import { CampaignNameSubline } from '@/components/campaign/shared/CampaignNameSubline' import { CampaignTable, CampaignTableHead, @@ -193,9 +194,7 @@ const peopleColumns = ({ ({row.party}) ) : null} - {subline ? ( - {subline} - ) : null} + ) }, diff --git a/src/components/campaign/shared/CampaignNameSubline.tsx b/src/components/campaign/shared/CampaignNameSubline.tsx new file mode 100644 index 00000000..85d692e0 --- /dev/null +++ b/src/components/campaign/shared/CampaignNameSubline.tsx @@ -0,0 +1,19 @@ +/** + * The "discreto sob o nome" secondary line (C129): the same muted small text + * under a name in every surface — people table, dobradinhas list and ficha. + * The position is the visual label; an optional `srLabel` names the line for + * screen readers (the muted/small styling does not exist in a linear read). + */ +export const CampaignNameSubline = ({ + value, + srLabel, +}: { + value: string | null | undefined + srLabel?: string +}) => + value ? ( + + {srLabel ? {srLabel} : null} + {value} + + ) : null diff --git a/src/components/campaign/stateDeputy/StateDeputyContactSection.tsx b/src/components/campaign/stateDeputy/StateDeputyContactSection.tsx index a625d9af..8cc17c87 100644 --- a/src/components/campaign/stateDeputy/StateDeputyContactSection.tsx +++ b/src/components/campaign/stateDeputy/StateDeputyContactSection.tsx @@ -2,6 +2,7 @@ import { updateStateDeputyContactFormAction } from '@/app/(campaign)/campanha/(app)/dobradinhas/formActions' import { CampaignInlineEditableCell } from '@/components/campaign/shared/CampaignInlineEditableCell' +import { CampaignNameSubline } from '@/components/campaign/shared/CampaignNameSubline' import { PhonesFieldEditor } from '@/components/campaign/shared/PhonesFieldEditor' type StateDeputyContactSectionProps = { @@ -39,8 +40,8 @@ export const StateDeputyContactSection = ({ formAction={updateStateDeputyContactFormAction} href={`/campanha/dobradinhas/${stateDeputyId}`} /> - {ballotName ? ( - {ballotName} + {ballotName !== name ? ( + ) : null} diff --git a/src/utilities/people/peopleData.ts b/src/utilities/people/peopleData.ts index e26eb87f..a401a77b 100644 --- a/src/utilities/people/peopleData.ts +++ b/src/utilities/people/peopleData.ts @@ -78,6 +78,9 @@ export type PeopleRowViewModel = { /** * C129 — the Name-cell subline of `/campanha/pessoas`: the dobradinha's * "nome de legenda" (ballot name), discreet under the real name, when present. + * A ballot name identical to the real name is redundant noise — it reads as a + * duplicated line, so it is skipped (a mesa registering the full name as urn + * name is plausible data entry). * * C130 extends this with the base fallback — the FINAL shared rule for the * second line is `ballotName ?? city` (legenda overrides base), the two plans @@ -86,8 +89,10 @@ export type PeopleRowViewModel = { * here (and its unit spec), never markup. Until C130 lands, this branch keeps * the accept "sem nome de legenda, nada muda na linha" — null when absent. */ -export const peopleNameSubline = (person: MergedPerson | PeopleRowViewModel): string | null => - person.ballotName +export const peopleNameSubline = ( + person: Pick, +): string | null => + person.ballotName && person.ballotName !== person.name ? person.ballotName : null /** The merge intermediate carries every field the list/detail row needs. */ export type MergedPerson = Omit & { diff --git a/src/utilities/people/personDetail.ts b/src/utilities/people/personDetail.ts index 74ca19a0..aac9f225 100644 --- a/src/utilities/people/personDetail.ts +++ b/src/utilities/people/personDetail.ts @@ -91,7 +91,7 @@ export const loadPersonDetail = async ( depth: 1, limit: 0, pagination: false, - select: { contact: true, slug: true, party: true, advisors: true }, + select: { contact: true, slug: true, party: true, ballotName: true, advisors: true }, user, overrideAccess: false, }), diff --git a/src/utilities/stateDeputyData.ts b/src/utilities/stateDeputyData.ts index 13548b8c..697d5810 100644 --- a/src/utilities/stateDeputyData.ts +++ b/src/utilities/stateDeputyData.ts @@ -239,13 +239,13 @@ export type StateDeputySummary = { name: string slug: string party: string | null - /** C129 — the "nome de legenda" (ballot name), shown discreet under the name. */ - ballotName: string | null } export type StateDeputyDetailViewModel = StateDeputySummary & { email: string | null phone: string | null + /** C129 — the "nome de legenda" (ballot name), shown discreet under the name. */ + ballotName: string | null /** Every number of the ficha, order = priority (C112) — primary first. */ phones: string[] notes: string | null @@ -345,7 +345,7 @@ export const loadStateDeputySummaries = async ( limit: 0, pagination: false, sort: 'contact.name', - select: { contact: true, slug: true, party: true, ballotName: true }, + select: { contact: true, slug: true, party: true }, // Intentional admin bypass: id lookups for the home-search card, no // actor-scoping possible or needed (B52 precedent). overrideAccess: true, @@ -359,7 +359,6 @@ export const loadStateDeputySummaries = async ( name: stateDeputyContactSummary(doc.contact).name, slug: doc.slug, party: doc.party ?? null, - ballotName: doc.ballotName ?? null, }, ]), ) diff --git a/tests/int/campaignStateDeputyBallotName.int.spec.ts b/tests/int/campaignStateDeputyBallotName.int.spec.ts index 016925e8..5544b0dd 100644 --- a/tests/int/campaignStateDeputyBallotName.int.spec.ts +++ b/tests/int/campaignStateDeputyBallotName.int.spec.ts @@ -55,6 +55,15 @@ describe('updateStateDeputyBallotNameRecord (C129)', () => { }) expect(updated.ballotName).toBeNull() + + const reloaded = await payload.findByID({ + collection: 'stateDeputy', + id: stateDeputy.id, + depth: 0, + select: { ballotName: true }, + overrideAccess: true, + }) + expect(reloaded.ballotName).toBeNull() }) it('lets an advisor edit the ballot name of a dobradinha (staff scope, no carteira gate)', async () => { @@ -68,6 +77,15 @@ describe('updateStateDeputyBallotNameRecord (C129)', () => { }) expect(updated.ballotName).toBe('Apelido da Mesa') + + const reloaded = await payload.findByID({ + collection: 'stateDeputy', + id: stateDeputy.id, + depth: 0, + select: { ballotName: true }, + overrideAccess: true, + }) + expect(reloaded.ballotName).toBe('Apelido da Mesa') }) it('refuses a leader (staff gate)', async () => { diff --git a/tests/unit/peopleMerge.unit.spec.ts b/tests/unit/peopleMerge.unit.spec.ts index ca7fd7dc..f3c6b684 100644 --- a/tests/unit/peopleMerge.unit.spec.ts +++ b/tests/unit/peopleMerge.unit.spec.ts @@ -341,6 +341,15 @@ describe('peopleNameSubline (C129)', () => { const maria = byContact(mergePeopleSources(source), 100) expect(peopleNameSubline(maria)).toBeNull() }) + + it('skips a ballot name identical to the real name (redundant duplicate line)', () => { + const withIdenticalBallotName = mergePeopleSources({ + ...source, + deputies: [{ ...source.deputies[0]!, ballotName: 'Ana Lima' }], + }) + const ana = byContact(withIdenticalBallotName, 101) + expect(peopleNameSubline(ana)).toBeNull() + }) }) describe('peopleFilterFacetsFromRows', () => {