teqo-backup/src/lib/leadershipMunicipalityMembership.ts
Francisco Ignácio Fontoura Solla 812e3a0bee B34: editable município chips on the leaderships list.
The "Municípios" column was dead text; it is now chips that navigate to the
município, with a territory chip when a leadership covers a whole TI, and it is
editable in place. What picks inline vs. Drawer is the `pointer:` media query,
not viewport width: on a fine pointer the editor is always mounted (the "×" sits
at the chip's corner, revealed on hover/focus) and on a coarse pointer a tap
anywhere in the cell opens a Drawer with 44 px targets.

`AdvisorMunicipalityCell` is promoted to `shared/MunicipalityPortfolioCell` and
/campanha/assessores migrates onto it in the same change, so the second call site
is born with the first. One batch action only; the single-id advisor twin is
deleted with its int tests migrated. The anti-goal on always-mounted list inputs
is amended for chip-based relations on fine pointers, not reopened.

Also fixes a latent bug: `CampaignTableHead`'s `description` seam broke any RSC
caller, because its two helpers lived in a 'use client' module.
2026-07-27 22:35:19 -03:00

43 lines
1.9 KiB
TypeScript

import {
LEADERSHIP_MUNICIPALITY_CAP_MESSAGE,
LEADERSHIP_MUNICIPALITY_FLOOR_MESSAGE,
MAX_LEADERSHIP_MUNICIPALITIES,
} from '@/lib/schemas/leadership'
/**
* Returns the next municipality-id list after adding or removing a set of
* municipalities, plus the ids the write actually touches: `added` (what the
* caller scope-checks) and `changed` (what it revalidates — a requested id that
* was already in the desired state should not bust its route's cache). `null`
* when the leadership is already in the desired state
* (no write needed). Sibling of `nextStateDeputyIdsAfterMembership`, with two
* differences that come from the relation itself, not from taste: the change may
* carry a whole território/ZE at once (a set, not one id), and
* `leadership.municipalities` has a **floor of one** — the same invariant
* `requireAtLeastOneMunicipality` enforces in the collection, refused here too
* so the client cannot reach a state the collection would reject.
*/
export const nextMunicipalityIdsAfterLeadershipMembership = (
currentMunicipalityIDs: readonly number[],
municipalityIDs: readonly number[],
assigned: boolean,
): { next: number[]; added: number[]; changed: number[] } | null => {
const current = new Set(currentMunicipalityIDs)
const requested = new Set(municipalityIDs)
if (assigned) {
const added = [...requested].filter((id) => !current.has(id))
if (added.length === 0) return null
const next = [...currentMunicipalityIDs, ...added]
if (next.length > MAX_LEADERSHIP_MUNICIPALITIES) {
throw new Error(LEADERSHIP_MUNICIPALITY_CAP_MESSAGE)
}
return { next, added, changed: added }
}
const next = currentMunicipalityIDs.filter((id) => !requested.has(id))
if (next.length === currentMunicipalityIDs.length) return null
if (next.length === 0) throw new Error(LEADERSHIP_MUNICIPALITY_FLOOR_MESSAGE)
return { next, added: [], changed: currentMunicipalityIDs.filter((id) => requested.has(id)) }
}