From 9b76f19b85d00d09136f03363550f48dc5550fb2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 1 Aug 2026 05:11:31 +0000 Subject: [PATCH 1/2] =?UTF-8?q?B88=20=E2=80=94=20A=C3=A7=C3=B5es=20r=C3=A1?= =?UTF-8?q?pidas=20em=20Organiza=C3=A7=C3=B5es=20(lista=20+=20detalhe)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Registra /campanha/organizacoes no registry B79 com launcher new-organization na lista; detalhe vazio até existir filtro de lideranças por org ou município óbvio único. Co-authored-by: Francisco Solla --- src/lib/campaignQuickActionPaths.ts | 32 ++++++++++-- src/lib/campaignQuickActionRegistry.ts | 7 +++ src/lib/organizationQuickActions.ts | 35 +++++++++++++ tests/unit/campaignQuickAction.unit.spec.ts | 56 +++++++++++++++++++++ 4 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 src/lib/organizationQuickActions.ts diff --git a/src/lib/campaignQuickActionPaths.ts b/src/lib/campaignQuickActionPaths.ts index fa6a26fa..d08c83f9 100644 --- a/src/lib/campaignQuickActionPaths.ts +++ b/src/lib/campaignQuickActionPaths.ts @@ -1,4 +1,4 @@ -/** Client-safe `/campanha/atividades` path helpers for the B84 quick-action registry. */ +/** Client-safe quick-action path helpers for the B84+ vertical registries. */ export const ACTIVITY_LIST_PATH = '/campanha/atividades' as const @@ -6,20 +6,35 @@ export const ACTIVITY_NEW_PATH = '/campanha/atividades/nova' as const export const ACTIVITY_TOUR_COMPOSER_PATH = '/campanha/atividades/giros' as const +export const ORGANIZATIONS_LIST_PATH = '/campanha/organizacoes' as const + +export const ORGANIZATION_NEW_PATH = '/campanha/organizacoes/nova' as const + export type ActivityQuickActionSurface = { kind: 'list' } | { kind: 'detail'; activitySlug: string } +export type OrganizationQuickActionSurface = + | { kind: 'list' } + | { kind: 'detail'; organizationSlug: string } + const normalizePathname = (pathname: string): string => pathname.length > 1 && pathname.endsWith('/') ? pathname.slice(0, -1) : pathname -const activityDetailSlug = (pathname: string): string | null => { - const prefix = `${ACTIVITY_LIST_PATH}/` +const listDetailSlug = ( + pathname: string, + listPath: string, + excludedSegments: readonly string[], +): string | null => { + const prefix = `${listPath}/` if (!pathname.startsWith(prefix)) return null const rest = pathname.slice(prefix.length) if (!rest || rest.includes('/')) return null - if (rest === 'nova' || rest === 'giros') return null + if (excludedSegments.includes(rest)) return null return rest } +const activityDetailSlug = (pathname: string): string | null => + listDetailSlug(pathname, ACTIVITY_LIST_PATH, ['nova', 'giros']) + export const parseActivityQuickActionSurface = ( pathname: string, ): ActivityQuickActionSurface | null => { @@ -29,5 +44,14 @@ export const parseActivityQuickActionSurface = ( return slug ? { kind: 'detail', activitySlug: slug } : null } +export const parseOrganizationQuickActionSurface = ( + pathname: string, +): OrganizationQuickActionSurface | null => { + const normalized = normalizePathname(pathname) + if (normalized === ORGANIZATIONS_LIST_PATH) return { kind: 'list' } + const slug = listDetailSlug(normalized, ORGANIZATIONS_LIST_PATH, ['nova']) + return slug ? { kind: 'detail', organizationSlug: slug } : null +} + export const isActivityTourComposerPath = (pathname: string): boolean => normalizePathname(pathname) === ACTIVITY_TOUR_COMPOSER_PATH diff --git a/src/lib/campaignQuickActionRegistry.ts b/src/lib/campaignQuickActionRegistry.ts index 3412a497..740ed65c 100644 --- a/src/lib/campaignQuickActionRegistry.ts +++ b/src/lib/campaignQuickActionRegistry.ts @@ -1,6 +1,8 @@ import { resolveActivityQuickActions } from '@/lib/activityQuickActions' import { resolveStaffHomeQuickActions } from '@/lib/campaignHomeActions' import { resolveMunicipalityQuickActionsForPath } from '@/lib/campaignMunicipalityQuickActions' +import { parseOrganizationQuickActionSurface } from '@/lib/campaignQuickActionPaths' +import { resolveOrganizationQuickActions } from '@/lib/organizationQuickActions' import { CAMPAIGN_TERRITORIES_HOME } from '@/lib/campaignPaths' import type { CampaignQuickActionContext } from '@/lib/campaignQuickActionContext' import { parseActivityQuickActionSurface } from '@/lib/campaignQuickActionPaths' @@ -25,6 +27,11 @@ export const resolveQuickActionsForPath = ( return resolveActivityQuickActions(activitySurface, role, context) } + const organizationSurface = parseOrganizationQuickActionSurface(pathname) + if (organizationSurface) { + return resolveOrganizationQuickActions(organizationSurface, role, context) + } + const municipalityActions = resolveMunicipalityQuickActionsForPath(pathname, role, context) if (municipalityActions.length > 0) return municipalityActions diff --git a/src/lib/organizationQuickActions.ts b/src/lib/organizationQuickActions.ts new file mode 100644 index 00000000..03ac7ef5 --- /dev/null +++ b/src/lib/organizationQuickActions.ts @@ -0,0 +1,35 @@ +import { Plus } from 'lucide-react' + +import type { CampaignQuickActionContext } from '@/lib/campaignQuickActionContext' +import { + ORGANIZATION_NEW_PATH, + type OrganizationQuickActionSurface, +} from '@/lib/campaignQuickActionPaths' +import type { CampaignQuickAction } from '@/lib/campaignQuickActionTypes' +import { isStaffCampaignRole, type CampaignRole } from '@/lib/campaignRoles' + +const listQuickActions = (): readonly CampaignQuickAction[] => [ + { + id: 'new-organization', + label: 'Nova organização', + icon: Plus, + description: 'Cadastrar sindicato, associação ou movimento de apoio', + href: ORGANIZATION_NEW_PATH, + }, +] + +export const resolveOrganizationQuickActions = ( + surface: OrganizationQuickActionSurface, + role: CampaignRole, + _context: CampaignQuickActionContext, +): readonly CampaignQuickAction[] => { + if (!isStaffCampaignRole(role)) return [] + + if (surface.kind === 'list') { + return listQuickActions() + } + + // Detail: leadership list has no organization filter URL yet (B88 as-built); + // orgs rarely have a single obvious municipality for A1–A5 prefills. + return [] +} diff --git a/tests/unit/campaignQuickAction.unit.spec.ts b/tests/unit/campaignQuickAction.unit.spec.ts index 8838f23a..84302ae0 100644 --- a/tests/unit/campaignQuickAction.unit.spec.ts +++ b/tests/unit/campaignQuickAction.unit.spec.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest' import { resolveActivityQuickActions } from '@/lib/activityQuickActions' +import { resolveOrganizationQuickActions } from '@/lib/organizationQuickActions' import { CAMPAIGN_WIZARD_ACTION_SLUGS } from '@/lib/campaignActionRoutes' import { UNCOVERED_MUNICIPALITIES_LIST_HREF, homeActionsForRole } from '@/lib/campaignHomeActions' import { @@ -13,8 +14,11 @@ import { ACTIVITY_LIST_PATH, ACTIVITY_NEW_PATH, ACTIVITY_TOUR_COMPOSER_PATH, + ORGANIZATION_NEW_PATH, + ORGANIZATIONS_LIST_PATH, isActivityTourComposerPath, parseActivityQuickActionSurface, + parseOrganizationQuickActionSurface, } from '@/lib/campaignQuickActionPaths' import { resolveQuickActionsForPath } from '@/lib/campaignQuickActionRegistry' import { @@ -77,6 +81,44 @@ describe('campaignQuickActionPaths (activities)', () => { }) }) +describe('organizationQuickActions (B88)', () => { + it('parses list and detail surfaces', () => { + expect(parseOrganizationQuickActionSurface(ORGANIZATIONS_LIST_PATH)).toEqual({ kind: 'list' }) + expect(parseOrganizationQuickActionSurface(`${ORGANIZATIONS_LIST_PATH}/`)).toEqual({ + kind: 'list', + }) + expect(parseOrganizationQuickActionSurface('/campanha/organizacoes/sindmed')).toEqual({ + kind: 'detail', + organizationSlug: 'sindmed', + }) + }) + + it('ignores nova and nested routes', () => { + expect(parseOrganizationQuickActionSurface(ORGANIZATION_NEW_PATH)).toBeNull() + expect(parseOrganizationQuickActionSurface('/campanha/organizacoes/foo/editar')).toBeNull() + }) + + it('lists new-organization on the catalog page', () => { + const actions = resolveOrganizationQuickActions({ kind: 'list' }, 'coordinator', {}) + expect(actions.map((action) => action.id)).toEqual(['new-organization']) + expect(actions[0]?.href).toBe(ORGANIZATION_NEW_PATH) + }) + + it('returns empty detail catalog without leadership filter URL', () => { + expect( + resolveOrganizationQuickActions( + { kind: 'detail', organizationSlug: 'sindmed' }, + 'coordinator', + { organizationSlug: 'sindmed' }, + ), + ).toEqual([]) + }) + + it('returns empty catalog for leader lockdown', () => { + expect(resolveOrganizationQuickActions({ kind: 'list' }, 'leader', {})).toEqual([]) + }) +}) + describe('activityQuickActions (B84)', () => { it('lists vertical verbs on the activity list', () => { const actions = resolveActivityQuickActions({ kind: 'list' }, 'coordinator', {}) @@ -125,6 +167,20 @@ describe('activityQuickActions (B84)', () => { describe('campaignQuickActionRegistry', () => { const staffActionIds = homeActionsForRole('coordinator').map((action) => action.id) + it('delegates organization routes to the B88 catalog', () => { + const actions = resolveQuickActionsForPath(ORGANIZATIONS_LIST_PATH, 'coordinator', {}) + expect(actions.map((action) => action.id)).toEqual(['new-organization']) + expect(actions[0]?.href).toBe(ORGANIZATION_NEW_PATH) + }) + + it('returns empty organization detail catalog in the registry', () => { + expect( + resolveQuickActionsForPath('/campanha/organizacoes/sindmed', 'advisor', { + organizationSlug: 'sindmed', + }), + ).toEqual([]) + }) + it('returns empty catalog for unregistered paths', () => { expect(resolveQuickActionsForPath('/campanha/apoiadores', 'coordinator', {})).toEqual([]) }) From a7ecdbc8445efac02fe427f3d59098701fdceae2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 1 Aug 2026 05:12:11 +0000 Subject: [PATCH 2/2] chore: format B88 quick-action files --- src/lib/campaignQuickActionRegistry.ts | 8 +++++--- tests/unit/campaignQuickAction.unit.spec.ts | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/lib/campaignQuickActionRegistry.ts b/src/lib/campaignQuickActionRegistry.ts index 740ed65c..14c06481 100644 --- a/src/lib/campaignQuickActionRegistry.ts +++ b/src/lib/campaignQuickActionRegistry.ts @@ -1,13 +1,15 @@ import { resolveActivityQuickActions } from '@/lib/activityQuickActions' import { resolveStaffHomeQuickActions } from '@/lib/campaignHomeActions' import { resolveMunicipalityQuickActionsForPath } from '@/lib/campaignMunicipalityQuickActions' -import { parseOrganizationQuickActionSurface } from '@/lib/campaignQuickActionPaths' -import { resolveOrganizationQuickActions } from '@/lib/organizationQuickActions' import { CAMPAIGN_TERRITORIES_HOME } from '@/lib/campaignPaths' import type { CampaignQuickActionContext } from '@/lib/campaignQuickActionContext' -import { parseActivityQuickActionSurface } from '@/lib/campaignQuickActionPaths' +import { + parseActivityQuickActionSurface, + parseOrganizationQuickActionSurface, +} from '@/lib/campaignQuickActionPaths' import type { CampaignQuickAction } from '@/lib/campaignQuickActionTypes' import type { CampaignRole } from '@/lib/campaignRoles' +import { resolveOrganizationQuickActions } from '@/lib/organizationQuickActions' const isTerritoriesListPath = (pathname: string): boolean => pathname === CAMPAIGN_TERRITORIES_HOME || pathname.startsWith(`${CAMPAIGN_TERRITORIES_HOME}/`) diff --git a/tests/unit/campaignQuickAction.unit.spec.ts b/tests/unit/campaignQuickAction.unit.spec.ts index 84302ae0..13efbec3 100644 --- a/tests/unit/campaignQuickAction.unit.spec.ts +++ b/tests/unit/campaignQuickAction.unit.spec.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from 'vitest' import { resolveActivityQuickActions } from '@/lib/activityQuickActions' -import { resolveOrganizationQuickActions } from '@/lib/organizationQuickActions' import { CAMPAIGN_WIZARD_ACTION_SLUGS } from '@/lib/campaignActionRoutes' import { UNCOVERED_MUNICIPALITIES_LIST_HREF, homeActionsForRole } from '@/lib/campaignHomeActions' import { @@ -14,8 +13,8 @@ import { ACTIVITY_LIST_PATH, ACTIVITY_NEW_PATH, ACTIVITY_TOUR_COMPOSER_PATH, - ORGANIZATION_NEW_PATH, ORGANIZATIONS_LIST_PATH, + ORGANIZATION_NEW_PATH, isActivityTourComposerPath, parseActivityQuickActionSurface, parseOrganizationQuickActionSurface, @@ -26,6 +25,7 @@ import { QUICK_ACTIONS_SNAP_EXPANDED, quickActionsSnapIsExpanded, } from '@/lib/campaignQuickActionSnap' +import { resolveOrganizationQuickActions } from '@/lib/organizationQuickActions' describe('campaignQuickActionMount', () => { it('treats Início as exact match only', () => {