teqo-backup/src/lib/municipalityAdvisorMembership.ts
Francisco Ignácio Fontoura Solla 7c50e29aa7 B27: combobox + chips + auto-save no popover de Assessores da lista de municípios
Replaces the checkbox-list + "Salvar assessores" button in the
Assessors popover (/campanha/municipios) with removable chips + a
Command combobox, delta-based auto-save (one advisor add/remove per
request, never the whole array), and optimistic UI with request
sequencing to resolve out-of-order responses correctly.

- New POST /campanha/municipios/advisors JSON route (mirrors
  expected-votes/), backed by setMunicipalityAdvisorMembership(Record)
  in actions/municipality.ts (text advisory lock + transaction).
- nextAdvisorIdsAfterMembership moved to
  src/lib/municipalityAdvisorMembership.ts with unit tests.
- Shared isSameOriginRequest (src/utilities/sameOriginRequest.ts) and
  uniqueRelationshipIds (src/utilities/relationship.ts) helpers,
  de-duplicating the same-origin CSRF check and the
  Set+map(relationshipId)+filter pattern across advisor call sites.
- MunicipalityListAdvisorsControl.tsx rewritten: chips, Command
  combobox, per-item pending affordance, rollback on error, and a
  request-sequence guard so only the most recently sent delta's
  response is adopted when concurrent optimistic updates settle.
- e2e coverage for the combobox auto-save flow; setup prewarms the new
  POST-only route to avoid Playwright dev-server HMR aborting the
  first request.
- Plan and roadmap marked as delivered (B27).
2026-07-26 03:31:51 -03:00

27 lines
1 KiB
TypeScript

import { MAX_ADVISORS_PER_MUNICIPALITY } from '@/lib/schemas/municipality'
/**
* Returns the next advisor-id list after applying one membership change, or
* `null` when the municipality is already in the desired state (no write
* needed). Shared by the advisor-carteira mutations (`actions/advisor.ts`)
* and the município-side quick edit (`actions/municipality.ts`, B27) — the
* two sides of the same `municipality.advisors` relation.
*/
export const nextAdvisorIdsAfterMembership = (
currentAdvisorIDs: readonly number[],
advisorId: number,
assigned: boolean,
): number[] | null => {
const alreadyAssigned = currentAdvisorIDs.includes(advisorId)
if (assigned === alreadyAssigned) return null
if (assigned) {
if (currentAdvisorIDs.length >= MAX_ADVISORS_PER_MUNICIPALITY) {
throw new Error(
`Cada município aceita no máximo ${MAX_ADVISORS_PER_MUNICIPALITY} assessores.`,
)
}
return [...currentAdvisorIDs, advisorId]
}
return currentAdvisorIDs.filter((id) => id !== advisorId)
}