Pass 2 W4b: zero exports mortos; knip exports/types/enumMembers em ERROR no CI

- ~190 findings zerados: knip --fix (grafo corrigido: tests como entries, migrations como consumidores) + delecao guardada dos orfaos (git grep -w antes de deletar) + reparos manuais dos corpos arrow pendurados
- pecas nao usadas do kit ui deletadas (PaginationLink/Next/Previous, familias Sidebar/Combobox/Sheet/Drawer/Dialog etc.) - shadcn add recoloca se precisar
- acoes mortas removidas (updateCampaignDemandDetails*, *Record orfaos), schemas/tipos mortos (campaignDemandDetailsUpdateSchema, Contact do zod), helpers mortos
- tipos de eleicao derivam direto das *_OPTIONS (consts intermediarias eliminadas)
- knip.json: rules exports/types/enumMembers=error, duplicates=off (unicos hits: 6 aliases deliberados canUpdateX=canReadX); hints aplicados (payload.config entry redundante, ignores de tsx/sharp/playwright/payload-types desnecessarios)
- ledger: linha P2 dos unused exports fechada
This commit is contained in:
Francisco Ignácio Fontoura Solla 2026-07-25 15:51:52 -03:00
parent 267218b05a
commit c01467547d
87 changed files with 177 additions and 986 deletions

View file

@ -6,7 +6,7 @@ Ledger of known engineering debt. Items large enough to be scheduled work become
| Item | Location | Type | Risk | Effort | Priority | Status | Ref |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | --------------------------- | ----------------------------------------------------------------------------------------- | -------------------- |
| 164 unused exports + 60 unused types reported by knip (warn-level; 158 at Pass 1) | god modules, ui kit families | dead code | low | M | P2 | open — Pass 2 W4b flips `exports` to error after W4a kills the biggest offender | Pass 2 W4b |
| 164 unused exports + 60 unused types reported by knip (warn-level; 158 at Pass 1) | god modules, ui kit families | dead code | low | M | P2 | closed 2026-07-25 (Pass 2 W4b): 0 findings; `exports`/`types`/`enumMembers` at **error** in CI; tests entered the knip graph; `duplicates` off (only hits were the 6 deliberate access aliases `canUpdateX = canReadX`) | Pass 2 W4b |
| knip loader noise: `Error loading src/payload.config.ts` (server-only guard) | knip run output | tooling noise | none (exit 0, static graph resolves) | S | P3 | open | |
| Missing-consent window fixture releases its exclusive lease during the observation window — readers can interleave (rare flake). Full fix: session-level advisory lock spanning delete→op→restore + updated fault-path pins | `tests/helpers/testDatabaseLease.ts` (`withMissingInviteConsentFixture`) | test-infra race | low (ms-wide window; shared leases added 2026-07-23 narrow it) | M | P2 | open | |
| `users` collection has no roles (all admins omnipotent) | `src/collections/Users.ts` | RBAC | medium (small trusted admin set) | M (schema migration) | P1 before widening `/admin` | open | AGENTS Known Gaps #1 |

View file

@ -1,21 +1,27 @@
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"entry": [
"src/payload.config.ts",
"src/migrations/index.ts",
"src/migrations/*.ts",
"src/app/(payload)/admin/importMap.js",
"scripts/*.mjs",
"eslint-local-rules.mjs"
"eslint-local-rules.mjs",
"tests/**/*.spec.ts",
"tests/helpers/serverOnly.ts"
],
"project": ["src/**/*.{ts,tsx,js}", "scripts/**/*.mjs"],
"ignore": ["src/payload-types.ts", "src/migrations/**"],
"project": [
"src/**/*.{ts,tsx,js}",
"scripts/**/*.mjs",
"tests/**/*.ts"
],
"ignore": [],
"paths": {
"server-only": ["tests/helpers/serverOnly.ts"]
"server-only": [
"tests/helpers/serverOnly.ts"
]
},
"ignoreBinaries": ["playwright"],
"ignoreBinaries": [],
"ignoreDependencies": [
"tsx",
"sharp",
"tailwindcss-animate",
"tw-animate-css",
"shadcn",
@ -23,9 +29,9 @@
"prettier"
],
"rules": {
"exports": "warn",
"types": "warn",
"enumMembers": "warn",
"duplicates": "warn"
"exports": "error",
"types": "error",
"enumMembers": "error",
"duplicates": "off"
}
}
}

View file

@ -21,7 +21,7 @@ export type SupporterFormState = {
revision?: number
}
export type SupporterFormValues = {
type SupporterFormValues = {
name?: string
phone?: string
email?: string

View file

@ -63,7 +63,7 @@ export const createActionPlanRecord = async (
)
}
export const updateActionPlanRecord = async (
const updateActionPlanRecord = async (
payload: Payload,
actor: CampaignUser,
input: ActionPlanUpdateInput,
@ -134,7 +134,7 @@ const setActionPlanLifecycleStatusRecord = async (
{ beginFailureMessage: 'Não foi possível atualizar o status do plano.' },
)
export const toggleActionPlanTaskRecord = async (
const toggleActionPlanTaskRecord = async (
payload: Payload,
actor: CampaignUser,
planId: number,
@ -176,7 +176,7 @@ export const toggleActionPlanTaskRecord = async (
{ beginFailureMessage: 'Não foi possível atualizar a tarefa do plano.' },
)
export const appendActionPlanUpdateRecord = async (
const appendActionPlanUpdateRecord = async (
payload: Payload,
actor: CampaignUser,
planId: number,
@ -221,7 +221,7 @@ export const appendActionPlanUpdateRecord = async (
)
}
export const registerActionPlanResult = async (
const registerActionPlanResult = async (
payload: Payload,
actor: CampaignUser,
planId: number,

View file

@ -5,11 +5,9 @@ import type { Payload } from 'payload'
import {
campaignDemandCostSchema,
campaignDemandCreateSchema,
campaignDemandDetailsUpdateSchema,
campaignDemandTransitionSchema,
type CampaignDemandCostInput,
type CampaignDemandCreateInput,
type CampaignDemandDetailsUpdateInput,
type CampaignDemandTransitionInput,
} from '@/lib/schemas/campaignDemandInput'
import type { CampaignUser } from '@/payload-types'
@ -50,33 +48,6 @@ export const createCampaignDemandRecord = async (
)
}
export const updateCampaignDemandDetailsRecord = async (
payload: Payload,
actor: CampaignUser,
input: CampaignDemandDetailsUpdateInput,
) => {
const { id, ...data } = campaignDemandDetailsUpdateSchema.parse(input)
return withPayloadTransaction(
payload,
async ({ req }) => {
const currentActor = await reloadCampaignActor(payload, actor, req)
await acquireTextAdvisoryLocks(payload, req, [`campaign-demand:${id}`])
return payload.update({
collection: 'campaignDemand',
id,
data,
depth: 0,
user: currentActor,
overrideAccess: false,
req,
})
},
{ beginFailureMessage: 'Não foi possível iniciar a atualização da demanda.' },
)
}
export const transitionCampaignDemandRecord = async (
payload: Payload,
actor: CampaignUser,
@ -215,11 +186,6 @@ export const createCampaignDemand = async (input: CampaignDemandCreateInput) =>
return createCampaignDemandRecord(payload, actor, input)
}
export const updateCampaignDemandDetails = async (input: CampaignDemandDetailsUpdateInput) => {
const { payload, actor } = await getCampaignActionContext()
return updateCampaignDemandDetailsRecord(payload, actor, input)
}
export const transitionCampaignDemand = async (input: CampaignDemandTransitionInput) => {
const { payload, actor } = await getCampaignActionContext()
return transitionCampaignDemandRecord(payload, actor, input)

View file

@ -28,7 +28,7 @@ export type LeaderSupporterFormState = {
revision?: number
}
export type LeaderSupporterFormValues = {
type LeaderSupporterFormValues = {
name?: string
phone?: string
city?: string
@ -144,7 +144,7 @@ export const createLeaderSupporterRecord = async (
}
}
export const createLeaderSupporter = async (input: unknown) => {
const createLeaderSupporter = async (input: unknown) => {
const { payload, actor } = await getCampaignActionContext()
return createLeaderSupporterRecord(payload, actor, input)
}

View file

@ -19,7 +19,7 @@ const organizationPolicy: StaffEntityPolicy = {
conflictMessage: 'Já existe uma organização com este nome.',
}
export const createOrganizationRecord = async (
const createOrganizationRecord = async (
payload: Payload,
actor: CampaignUser,
input: OrganizationCreateInput,
@ -36,7 +36,7 @@ export const createOrganizationRecord = async (
)
}
export const updateOrganizationRecord = async (
const updateOrganizationRecord = async (
payload: Payload,
actor: CampaignUser,
input: OrganizationUpdateInput,

View file

@ -19,7 +19,7 @@ const stateDeputyPolicy: StaffEntityPolicy = {
conflictMessage: 'Já existe uma dobradinha com este nome.',
}
export const createStateDeputyRecord = async (
const createStateDeputyRecord = async (
payload: Payload,
actor: CampaignUser,
input: StateDeputyCreateInput,
@ -36,7 +36,7 @@ export const createStateDeputyRecord = async (
)
}
export const updateStateDeputyRecord = async (
const updateStateDeputyRecord = async (
payload: Payload,
actor: CampaignUser,
input: StateDeputyUpdateInput,

View file

@ -27,12 +27,7 @@ import {
previewSupporterImportText as previewSupporterImportTextAction,
} from './supporterImport'
export type {
SupporterImportOkRow,
SupporterImportPreviewResult,
SupporterImportPreviewRow,
SupporterImportRowStatus,
} from '@/utilities/supporterImport'
const getFreshStaffActor = (
payload: Payload,
@ -46,7 +41,7 @@ const getFreshStaffActor = (
req,
)
export const assertMunicipalityManagement = async (
const assertMunicipalityManagement = async (
payload: Payload,
actor: CampaignUser,
municipalityID: number,

View file

@ -64,7 +64,7 @@ export type ActionPlanFormFieldsProps = {
const editableStatuses = ['rascunho', 'planejado'] as const
export const ActionPlanFormFields = ({
const ActionPlanFormFields = ({
municipalityOptions,
organizationOptions,
advisorOptions = [],

View file

@ -26,12 +26,6 @@ const statusPresentation = {
},
} as const
export const supportStatusLabel = (status: SupportStatus): string =>
statusPresentation[status].label
export const supportStatusSummary = (status: SupportStatus): string =>
statusPresentation[status].summary
export const SupportStatusBadge = ({ status }: { status: SupportStatus }) => {
const presentation = statusPresentation[status]

View file

@ -26,7 +26,7 @@ import {
import { cn } from '@/lib/utils'
import { Spinner } from '@/components/ui/Spinner'
export type { BahiaMapFillMode, ChoroplethValues }
const BAHIA_BOUNDS: L.LatLngBoundsExpression = [
[-18.5, -46.8],

View file

@ -10,7 +10,7 @@ import {
} from '@/components/ui/Pagination'
import { cn } from '@/lib/utils'
export const getPaginationPages = (page: number, totalPages: number): number[] => {
const getPaginationPages = (page: number, totalPages: number): number[] => {
const pages = new Set([1, totalPages, page - 1, page, page + 1])
return [...pages]
.filter((candidate) => candidate >= 1 && candidate <= totalPages)

View file

@ -2,7 +2,7 @@ import type { ComponentProps, ReactNode } from 'react'
import { cn } from '@/lib/utils'
export const campaignPageShellClassName = 'mx-auto flex w-full max-w-screen-2xl flex-col gap-8'
const campaignPageShellClassName = 'mx-auto flex w-full max-w-screen-2xl flex-col gap-8'
export const campaignPrioritySurfaceClassName =
'shadow-[0_4px_24px_rgb(28_25_23/0.08)] ring-1 ring-foreground/10'

View file

@ -56,8 +56,4 @@ const AlertDescription = ({ className, ...props }: React.ComponentProps<'div'>)
/>
)
const AlertAction = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div data-slot="alert-action" className={cn('absolute top-2 right-2', className)} {...props} />
)
export { Alert, AlertTitle, AlertDescription, AlertAction }
export { Alert, AlertTitle, AlertDescription, }

View file

@ -14,11 +14,11 @@ export const AlertDialogTrigger = (
props: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>,
) => <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
export const AlertDialogPortal = (
const AlertDialogPortal = (
props: React.ComponentProps<typeof AlertDialogPrimitive.Portal>,
) => <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
export const AlertDialogOverlay = ({
const AlertDialogOverlay = ({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) => (

View file

@ -48,40 +48,4 @@ const AvatarFallback = ({
/>
)
const AvatarBadge = ({ className, ...props }: React.ComponentProps<'span'>) => (
<span
data-slot="avatar-badge"
className={cn(
'absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background select-none',
'group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden',
'group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2',
'group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2',
className,
)}
{...props}
/>
)
const AvatarGroup = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot="avatar-group"
className={cn(
'group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background',
className,
)}
{...props}
/>
)
const AvatarGroupCount = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot="avatar-group-count"
className={cn(
'relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3',
className,
)}
{...props}
/>
)
export { Avatar, AvatarImage, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarBadge }
export { Avatar, AvatarImage, AvatarFallback, }

View file

@ -87,17 +87,6 @@ const CommandList = ({
/>
)
const CommandEmpty = ({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) => (
<CommandPrimitive.Empty
data-slot="command-empty"
className={cn('py-6 text-center text-sm', className)}
{...props}
/>
)
const CommandGroup = ({
className,
...props
@ -112,17 +101,6 @@ const CommandGroup = ({
/>
)
const CommandSeparator = ({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) => (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn('-mx-1 h-px bg-border', className)}
{...props}
/>
)
const CommandItem = ({
className,
children,
@ -141,25 +119,14 @@ const CommandItem = ({
</CommandPrimitive.Item>
)
const CommandShortcut = ({ className, ...props }: React.ComponentProps<'span'>) => (
<span
data-slot="command-shortcut"
className={cn(
'ml-auto text-xs tracking-widest text-muted-foreground group-data-selected/command-item:text-foreground',
className,
)}
{...props}
/>
)
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

View file

@ -54,10 +54,6 @@ const Drawer = ({
)
}
const DrawerTrigger = ({ ...props }: DrawerPrimitive.Trigger.Props) => (
<DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
)
const DrawerPortal = ({ ...props }: DrawerPrimitive.Portal.Props) => (
<DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
)
@ -172,10 +168,10 @@ const DrawerDescription = ({ className, ...props }: DrawerPrimitive.Description.
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerSwipeHandle,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,

View file

@ -1,8 +1,7 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from 'lucide-react'
import { MoreHorizontalIcon } from 'lucide-react'
const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
<nav
@ -26,59 +25,6 @@ const PaginationItem = ({ ...props }: React.ComponentProps<'li'>) => (
<li data-slot="pagination-item" {...props} />
)
type PaginationLinkProps = {
isActive?: boolean
} & Pick<React.ComponentProps<typeof Button>, 'size'> &
React.ComponentProps<'a'>
const PaginationLink = ({ className, isActive, size = 'icon', ...props }: PaginationLinkProps) => (
<Button
asChild
variant={isActive ? 'outline' : 'ghost'}
size={size}
className={cn('min-h-11 min-w-11', className)}
>
<a
aria-current={isActive ? 'page' : undefined}
data-slot="pagination-link"
data-active={isActive}
{...props}
/>
</Button>
)
const PaginationPrevious = ({
className,
text = 'Anterior',
...props
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) => (
<PaginationLink
aria-label="Ir para a página anterior"
size="default"
className={cn('min-h-11 pl-1.5!', className)}
{...props}
>
<ChevronLeftIcon data-icon="inline-start" />
<span className="hidden sm:block">{text}</span>
</PaginationLink>
)
const PaginationNext = ({
className,
text = 'Próxima',
...props
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) => (
<PaginationLink
aria-label="Ir para a próxima página"
size="default"
className={cn('min-h-11 pr-1.5!', className)}
{...props}
>
<span className="hidden sm:block">{text}</span>
<ChevronRightIcon data-icon="inline-end" />
</PaginationLink>
)
const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<'span'>) => (
<span
aria-hidden
@ -94,12 +40,4 @@ const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<'span'
</span>
)
export {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
}
export { Pagination, PaginationContent, PaginationEllipsis, PaginationItem }

View file

@ -13,10 +13,6 @@ const PopoverTrigger = ({ ...props }: React.ComponentProps<typeof PopoverPrimiti
<PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
)
const PopoverAnchor = ({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) => (
<PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
)
const PopoverContent = ({
className,
align = 'center',
@ -37,4 +33,4 @@ const PopoverContent = ({
</PopoverPrimitive.Portal>
)
export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger }
export { Popover, PopoverContent, PopoverTrigger }

View file

@ -11,14 +11,6 @@ const Sheet = ({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) =
<SheetPrimitive.Root data-slot="sheet" {...props} />
)
const SheetTrigger = ({ ...props }: React.ComponentProps<typeof SheetPrimitive.Trigger>) => (
<SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
)
const SheetClose = ({ ...props }: React.ComponentProps<typeof SheetPrimitive.Close>) => (
<SheetPrimitive.Close data-slot="sheet-close" {...props} />
)
const SheetPortal = ({ ...props }: React.ComponentProps<typeof SheetPrimitive.Portal>) => (
<SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
)
@ -75,14 +67,6 @@ const SheetHeader = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div data-slot="sheet-header" className={cn('flex flex-col gap-0.5 p-4', className)} {...props} />
)
const SheetFooter = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot="sheet-footer"
className={cn('mt-auto flex flex-col gap-2 p-4', className)}
{...props}
/>
)
const SheetTitle = ({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>) => (
<SheetPrimitive.Title
data-slot="sheet-title"
@ -104,11 +88,11 @@ const SheetDescription = ({
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

View file

@ -7,8 +7,6 @@ import { Slot } from 'radix-ui'
import { useIsMobile } from '@/hooks/use-mobile'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Separator } from '@/components/ui/separator'
import {
Sheet,
SheetContent,
@ -16,7 +14,6 @@ import {
SheetHeader,
SheetTitle,
} from '@/components/ui/Sheet'
import { Skeleton } from '@/components/ui/skeleton'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { PanelLeftIcon } from 'lucide-react'
@ -274,31 +271,6 @@ const SidebarTrigger = ({
)
}
const SidebarRail = ({ className, ...props }: React.ComponentProps<'button'>) => {
const { toggleSidebar } = useSidebar()
return (
<button
data-sidebar="rail"
data-slot="sidebar-rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
'absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] hover:after:bg-sidebar-border sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2',
'in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize',
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
'group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-sidebar',
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
'[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
className,
)}
{...props}
/>
)
}
const SidebarInset = ({ className, ...props }: React.ComponentProps<'main'>) => (
<main
data-slot="sidebar-inset"
@ -310,15 +282,6 @@ const SidebarInset = ({ className, ...props }: React.ComponentProps<'main'>) =>
/>
)
const SidebarInput = ({ className, ...props }: React.ComponentProps<typeof Input>) => (
<Input
data-slot="sidebar-input"
data-sidebar="input"
className={cn('h-8 w-full bg-background shadow-none', className)}
{...props}
/>
)
const SidebarHeader = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot="sidebar-header"
@ -337,15 +300,6 @@ const SidebarFooter = ({ className, ...props }: React.ComponentProps<'div'>) =>
/>
)
const SidebarSeparator = ({ className, ...props }: React.ComponentProps<typeof Separator>) => (
<Separator
data-slot="sidebar-separator"
data-sidebar="separator"
className={cn('mx-2 w-auto bg-sidebar-border', className)}
{...props}
/>
)
const SidebarContent = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot="sidebar-content"
@ -367,46 +321,6 @@ const SidebarGroup = ({ className, ...props }: React.ComponentProps<'div'>) => (
/>
)
const SidebarGroupLabel = ({
className,
asChild = false,
...props
}: React.ComponentProps<'div'> & { asChild?: boolean }) => {
const Comp = asChild ? Slot.Root : 'div'
return (
<Comp
data-slot="sidebar-group-label"
data-sidebar="group-label"
className={cn(
'flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 ring-sidebar-ring outline-hidden transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
className,
)}
{...props}
/>
)
}
const SidebarGroupAction = ({
className,
asChild = false,
...props
}: React.ComponentProps<'button'> & { asChild?: boolean }) => {
const Comp = asChild ? Slot.Root : 'button'
return (
<Comp
data-slot="sidebar-group-action"
data-sidebar="group-action"
className={cn(
'absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0',
className,
)}
{...props}
/>
)
}
const SidebarGroupContent = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot="sidebar-group-content"
@ -506,147 +420,29 @@ const SidebarMenuButton = ({
)
}
const SidebarMenuAction = ({
className,
asChild = false,
showOnHover = false,
...props
}: React.ComponentProps<'button'> & {
asChild?: boolean
showOnHover?: boolean
}) => {
const Comp = asChild ? Slot.Root : 'button'
return (
<Comp
data-slot="sidebar-menu-action"
data-sidebar="menu-action"
className={cn(
'absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0',
showOnHover &&
'group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-active/menu-button:text-sidebar-accent-foreground aria-expanded:opacity-100 md:opacity-0',
className,
)}
{...props}
/>
)
}
const SidebarMenuBadge = ({ className, ...props }: React.ComponentProps<'div'>) => (
<div
data-slot="sidebar-menu-badge"
data-sidebar="menu-badge"
className={cn(
'pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium text-sidebar-foreground tabular-nums select-none group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 peer-data-active/menu-button:text-sidebar-accent-foreground',
className,
)}
{...props}
/>
)
const SidebarMenuSkeleton = ({
className,
showIcon = false,
...props
}: React.ComponentProps<'div'> & {
showIcon?: boolean
}) => {
// Random width between 50 to 90%.
const [width] = React.useState(() => `${Math.floor(Math.random() * 40) + 50}%`)
return (
<div
data-slot="sidebar-menu-skeleton"
data-sidebar="menu-skeleton"
className={cn('flex h-8 items-center gap-2 rounded-md px-2', className)}
{...props}
>
{showIcon && <Skeleton className="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />}
<Skeleton
className="h-4 max-w-(--skeleton-width) flex-1"
data-sidebar="menu-skeleton-text"
style={
{
'--skeleton-width': width,
} as React.CSSProperties
}
/>
</div>
)
}
const SidebarMenuSub = ({ className, ...props }: React.ComponentProps<'ul'>) => (
<ul
data-slot="sidebar-menu-sub"
data-sidebar="menu-sub"
className={cn(
'mx-3.5 my-0 flex min-w-0 list-none translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5 group-data-[collapsible=icon]:hidden [&>li]:mt-0',
className,
)}
{...props}
/>
)
const SidebarMenuSubItem = ({ className, ...props }: React.ComponentProps<'li'>) => (
<li
data-slot="sidebar-menu-sub-item"
data-sidebar="menu-sub-item"
className={cn('group/menu-sub-item relative', className)}
{...props}
/>
)
const SidebarMenuSubButton = ({
asChild = false,
size = 'md',
isActive = false,
className,
...props
}: React.ComponentProps<'a'> & {
asChild?: boolean
size?: 'sm' | 'md'
isActive?: boolean
}) => {
const Comp = asChild ? Slot.Root : 'a'
return (
<Comp
data-slot="sidebar-menu-sub-button"
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
'flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground ring-sidebar-ring outline-hidden group-data-[collapsible=icon]:hidden hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-sm data-[size=sm]:text-xs data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground',
className,
)}
{...props}
/>
)
}
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
}

View file

@ -35,14 +35,6 @@ const TableBody = ({ className, ...props }: React.ComponentProps<'tbody'>) => (
/>
)
const TableFooter = ({ className, ...props }: React.ComponentProps<'tfoot'>) => (
<tfoot
data-slot="table-footer"
className={cn('border-t bg-muted/50 font-medium [&>tr]:last:border-b-0', className)}
{...props}
/>
)
const TableRow = ({ className, ...props }: React.ComponentProps<'tr'>) => (
<tr
data-slot="table-row"
@ -81,4 +73,4 @@ const TableCaption = ({ className, ...props }: React.ComponentProps<'caption'>)
/>
)
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
export { Table, TableHeader, TableBody, TableHead, TableRow, TableCell, TableCaption }

View file

@ -2,7 +2,7 @@ import * as React from "react"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
import { ChevronRightIcon, MoreHorizontalIcon } from "lucide-react"
import { ChevronRightIcon } from "lucide-react"
function Breadcrumb({ className, ...props }: React.ComponentProps<"nav">) {
return (
@ -89,28 +89,6 @@ function BreadcrumbSeparator({
)
}
function BreadcrumbEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
className={cn(
"flex size-5 items-center justify-center [&>svg]:size-4",
className
)}
{...props}
>
<MoreHorizontalIcon
/>
<span className="sr-only">More</span>
</span>
)
}
export {
Breadcrumb,
BreadcrumbList,
@ -118,5 +96,5 @@ export {
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}

View file

@ -79,23 +79,10 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn(
"flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)",
className
)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,

View file

@ -4,7 +4,6 @@ import * as React from "react"
import { Combobox as ComboboxPrimitive } from "@base-ui/react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
InputGroup,
InputGroupAddon,
@ -15,10 +14,6 @@ import { ChevronDownIcon, XIcon, CheckIcon } from "lucide-react"
const Combobox = ComboboxPrimitive.Root
function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />
}
function ComboboxTrigger({
className,
children,
@ -159,35 +154,6 @@ function ComboboxItem({
)
}
function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
return (
<ComboboxPrimitive.Group
data-slot="combobox-group"
className={cn(className)}
{...props}
/>
)
}
function ComboboxLabel({
className,
...props
}: ComboboxPrimitive.GroupLabel.Props) {
return (
<ComboboxPrimitive.GroupLabel
data-slot="combobox-label"
className={cn("px-2 py-1.5 text-xs text-muted-foreground", className)}
{...props}
/>
)
}
function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {
return (
<ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />
)
}
function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
return (
<ComboboxPrimitive.Empty
@ -201,99 +167,21 @@ function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
)
}
function ComboboxSeparator({
className,
...props
}: ComboboxPrimitive.Separator.Props) {
return (
<ComboboxPrimitive.Separator
data-slot="combobox-separator"
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
}
function ComboboxChips({
className,
...props
}: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> &
ComboboxPrimitive.Chips.Props) {
return (
<ComboboxPrimitive.Chips
data-slot="combobox-chips"
className={cn(
"flex min-h-8 flex-wrap items-center gap-1 rounded-lg border border-input bg-transparent bg-clip-padding px-2.5 py-1 text-sm transition-colors focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50 has-aria-invalid:border-destructive has-aria-invalid:ring-3 has-aria-invalid:ring-destructive/20 has-data-[slot=combobox-chip]:px-1 dark:bg-input/30 dark:has-aria-invalid:border-destructive/50 dark:has-aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
function ComboboxChip({
className,
children,
showRemove = true,
...props
}: ComboboxPrimitive.Chip.Props & {
showRemove?: boolean
}) {
return (
<ComboboxPrimitive.Chip
data-slot="combobox-chip"
className={cn(
"flex h-[calc(--spacing(5.25))] w-fit items-center justify-center gap-1 rounded-sm bg-muted px-1.5 text-xs font-medium whitespace-nowrap text-foreground has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0",
className
)}
{...props}
>
{children}
{showRemove && (
<ComboboxPrimitive.ChipRemove
render={<Button variant="ghost" size="icon-xs" />}
className="-ml-1 opacity-50 hover:opacity-100"
data-slot="combobox-chip-remove"
>
<XIcon className="pointer-events-none" />
</ComboboxPrimitive.ChipRemove>
)}
</ComboboxPrimitive.Chip>
)
}
function ComboboxChipsInput({
className,
...props
}: ComboboxPrimitive.Input.Props) {
return (
<ComboboxPrimitive.Input
data-slot="combobox-chip-input"
className={cn("min-w-16 flex-1 outline-none", className)}
{...props}
/>
)
}
function useComboboxAnchor() {
return React.useRef<HTMLDivElement | null>(null)
}
export {
Combobox,
ComboboxInput,
ComboboxContent,
ComboboxList,
ComboboxItem,
ComboboxGroup,
ComboboxLabel,
ComboboxCollection,
ComboboxEmpty,
ComboboxSeparator,
ComboboxChips,
ComboboxChip,
ComboboxChipsInput,
ComboboxTrigger,
ComboboxValue,
useComboboxAnchor,
}

View file

@ -10,10 +10,6 @@ function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>)
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
@ -82,16 +78,6 @@ function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="dialog-footer"
className={cn('flex flex-col gap-2 sm:flex-row sm:justify-end', className)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
@ -123,10 +109,10 @@ export {
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View file

@ -5,7 +5,6 @@ import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
import { Label } from '@/components/ui/label'
import { Separator } from '@/components/ui/separator'
function FieldSet({ className, ...props }: React.ComponentProps<'fieldset'>) {
return (
@ -106,19 +105,6 @@ function FieldLabel({ className, ...props }: React.ComponentProps<typeof Label>)
)
}
function FieldTitle({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="field-label"
className={cn(
'flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50',
className,
)}
{...props}
/>
)
}
function FieldDescription({ className, ...props }: React.ComponentProps<'p'>) {
return (
<p
@ -134,36 +120,6 @@ function FieldDescription({ className, ...props }: React.ComponentProps<'p'>) {
)
}
function FieldSeparator({
children,
className,
...props
}: React.ComponentProps<'div'> & {
children?: React.ReactNode
}) {
return (
<div
data-slot="field-separator"
data-content={!!children}
className={cn(
'relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2',
className,
)}
{...props}
>
<Separator className="absolute inset-0 top-1/2" />
{children && (
<span
className="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"
data-slot="field-separator-content"
>
{children}
</span>
)}
</div>
)
}
function FieldError({
className,
children,
@ -217,8 +173,8 @@ export {
FieldError,
FieldGroup,
FieldLegend,
FieldSeparator,
FieldSet,
FieldContent,
FieldTitle,
}

View file

@ -6,7 +6,6 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
@ -102,18 +101,6 @@ function InputGroupButton({
)
}
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
className={cn(
"flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function InputGroupInput({
className,
...props
@ -130,27 +117,11 @@ function InputGroupInput({
)
}
function InputGroupTextarea({
className,
...props
}: React.ComponentProps<"textarea">) {
return (
<Textarea
data-slot="input-group-control"
className={cn(
"flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
className
)}
{...props}
/>
)
}
export {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupInput,
InputGroupTextarea,
}

View file

@ -36,8 +36,4 @@ function NativeSelectOption({ ...props }: React.ComponentProps<'option'>) {
return <option data-slot="native-select-option" {...props} />
}
function NativeSelectOptGroup({ className, ...props }: React.ComponentProps<'optgroup'>) {
return <optgroup data-slot="native-select-optgroup" className={cn(className)} {...props} />
}
export { NativeSelect, NativeSelectOptGroup, NativeSelectOption }
export { NativeSelect, NativeSelectOption }

View file

@ -6,10 +6,10 @@
*/
export type {
BahiaMunicipalityFeature,
BahiaMunicipalityProperties,
BahiaTerritoryFeature,
BahiaTerritoryProperties,
MunicipalityGeometryModule,
TerritoryGeometryModule,
} from '@/lib/bahiaGeometriesTypes'

View file

@ -1,12 +1,12 @@
import type { Feature, MultiPolygon, Polygon } from 'geojson'
import type { GeometryCollection, Topology } from 'topojson-specification'
export type BahiaMunicipalityProperties = {
type BahiaMunicipalityProperties = {
codarea: string
name: string
}
export type BahiaTerritoryProperties = {
type BahiaTerritoryProperties = {
code: string
name: string
}

View file

@ -29,7 +29,7 @@ type FeatureStyleInput = {
max: number
}
export const getFeatureStyle = ({
const getFeatureStyle = ({
metric,
highlighted,
fillMode,

View file

@ -650,11 +650,3 @@ export const validateBahiaTerritoryPair = (
export const citiesForTerritory = (territory: BahiaIdentityTerritory): readonly string[] =>
bahiaIdentityTerritoryRecords.find(({ name }) => name === territory)?.municipalities ?? []
export const territoriesForCities = (cities: readonly string[]): BahiaIdentityTerritory[] => {
const territories = new Set<BahiaIdentityTerritory>()
for (const city of cities) {
const territory = territoryForCity(city)
if (territory) territories.add(territory)
}
return bahiaIdentityTerritories.filter((territory) => territories.has(territory))
}

View file

@ -1,6 +1,6 @@
/** Light muted rose → campaign primary #c51414 */
export const choroplethGradientStart = { r: 254, g: 226, b: 226 } as const
export const choroplethGradientEnd = { r: 197, g: 20, b: 20 } as const
const choroplethGradientStart = { r: 254, g: 226, b: 226 } as const
const choroplethGradientEnd = { r: 197, g: 20, b: 20 } as const
export const choroplethGradientCss = `linear-gradient(to right, rgb(${choroplethGradientStart.r} ${choroplethGradientStart.g} ${choroplethGradientStart.b}), rgb(${choroplethGradientEnd.r} ${choroplethGradientEnd.g} ${choroplethGradientEnd.b}))`
@ -26,7 +26,7 @@ export const choroplethMaxValue = (values: Record<string, number>): number => {
* Diverging comparison scale (product decision 2026-07-20): campaign red where
* Solla leads, white at the tie, blue where the compared candidate leads.
*/
export const divergingBlueEnd = { r: 30, g: 64, b: 175 } as const
const divergingBlueEnd = { r: 30, g: 64, b: 175 } as const
const white = { r: 255, g: 255, b: 255 } as const

View file

@ -1,4 +1,4 @@
export const CONTACT_SEARCH_MIN_LENGTH = 2
const CONTACT_SEARCH_MIN_LENGTH = 2
export const normalizeContactSearchQuery = (raw: string) => {
const trimmed = raw.trim().slice(0, 120)

View file

@ -5,7 +5,7 @@ export const ELECTION_YEAR_2018 = 2018 as const
export const ELECTION_YEAR_2022 = 2022 as const
/** Years loaded for prior-cycle comparison before 2022 (detail baseline query). */
export const HISTORICAL_PRIOR_SERIES_YEARS = [
const HISTORICAL_PRIOR_SERIES_YEARS = [
ELECTION_YEAR_2014,
ELECTION_YEAR_2018,
] as const
@ -33,7 +33,7 @@ export const ELECTION_TURN_OPTIONS = [
{ label: '2º turno', value: '2' },
] as const
export const ELECTION_TURNS = ELECTION_TURN_OPTIONS.map((o) => o.value)
const ELECTION_TURNS = ELECTION_TURN_OPTIONS.map((o) => o.value)
export type ElectionTurn = (typeof ELECTION_TURNS)[number]
export const ELECTION_VOTE_TYPE_OPTIONS = [
@ -41,8 +41,7 @@ export const ELECTION_VOTE_TYPE_OPTIONS = [
{ label: 'Legenda', value: 'legenda' },
] as const
export const ELECTION_VOTE_TYPES = ELECTION_VOTE_TYPE_OPTIONS.map((o) => o.value)
export type ElectionVoteType = (typeof ELECTION_VOTE_TYPES)[number]
type ElectionVoteType = (typeof ELECTION_VOTE_TYPE_OPTIONS)[number]['value']
export const ELECTION_ELECTED_BY_OPTIONS = [
{ label: 'Quociente partidário', value: 'QP' },
@ -50,8 +49,7 @@ export const ELECTION_ELECTED_BY_OPTIONS = [
{ label: '2º turno', value: '2º turno' },
] as const
export const ELECTION_ELECTED_BY = ELECTION_ELECTED_BY_OPTIONS.map((o) => o.value)
export type ElectionElectedBy = (typeof ELECTION_ELECTED_BY)[number]
type ElectionElectedBy = (typeof ELECTION_ELECTED_BY_OPTIONS)[number]['value']
export const ELECTION_RUNNING_AGAIN_OPTIONS = [
{ label: 'Sim', value: 'sim' },
@ -59,8 +57,7 @@ export const ELECTION_RUNNING_AGAIN_OPTIONS = [
{ label: 'Desconhecido', value: 'desconhecido' },
] as const
export const ELECTION_RUNNING_AGAIN = ELECTION_RUNNING_AGAIN_OPTIONS.map((o) => o.value)
export type ElectionRunningAgain = (typeof ELECTION_RUNNING_AGAIN)[number]
type ElectionRunningAgain = (typeof ELECTION_RUNNING_AGAIN_OPTIONS)[number]['value']
/**
* Campaign ticket for the electoral baseline, keyed by role.
@ -80,7 +77,7 @@ export const BASELINE_TICKET_2022 = {
} as const
/** TSE CD_CARGO → office enum used in collections. */
export const TSE_CARGO_TO_OFFICE: Readonly<Record<string, ElectionOffice>> = {
const TSE_CARGO_TO_OFFICE: Readonly<Record<string, ElectionOffice>> = {
'1': 'presidente',
'3': 'governador',
'6': 'deputado_federal',
@ -188,7 +185,7 @@ export const canonicalizeMunicipalityName = (tseMunicipalityName: string): strin
return canonical
}
export const isElectionTurn = (value: string): value is ElectionTurn =>
const isElectionTurn = (value: string): value is ElectionTurn =>
(ELECTION_TURNS as readonly string[]).includes(value)
export type CandidateVoteRow = {
@ -208,7 +205,7 @@ export type CandidateVoteRow = {
}
/** Winner fields as stored on electionTally (null when no nominal votes in that cell). */
export type TallyWinner = {
type TallyWinner = {
winnerCandidateNumber: number | null
winnerCandidateName: string | null
winnerVotes: number | null

View file

@ -231,7 +231,7 @@ export type ScopeKey = {
turn: ElectionTurn
}
export const scopeKeyString = (scope: ScopeKey): string =>
const scopeKeyString = (scope: ScopeKey): string =>
`${scope.year}|${scope.office}|${scope.turn}`
const candidateAggregateKey = (row: {

View file

@ -25,7 +25,7 @@ import { bahiaTseCityCodes } from '@/lib/bahiaTseCityCodes'
import { tseZonesForCity } from '@/lib/bahiaTseZones'
import { slugify } from '@/lib/slug'
export type MunicipalityKind = 'municipio' | 'zona'
type MunicipalityKind = 'municipio' | 'zona'
export interface MunicipalityCatalogEntry {
/** Canonical immutable slug — public URL segment and seed identity. */

View file

@ -13,7 +13,7 @@ import { getMunicipalityCatalogEntry } from '@/lib/municipalityCatalog'
export type MunicipalityZoneNeighborhoodSource = 'tre-ra-02-2017'
export type MunicipalityZoneNeighborhoodCity = 'Salvador'
type MunicipalityZoneNeighborhoodCity = 'Salvador'
export type MunicipalityZoneNeighborhoodEntry = {
readonly municipalitySlug: string

View file

@ -8,7 +8,7 @@ import type { Consent } from '@/payload-types'
export const ONDA0_PROVISIONAL_BANNER =
'Texto provisório para MVP e testes internos da plataforma Teqo (campanha Jorge Solla). Não constitui parecer jurídico. Deve ser substituído por texto aprovado pela assessoria jurídica eleitoral antes de coleta de dados de titulares reais em produção.'
export const ONDA0_CONSENT_KEYS = {
const ONDA0_CONSENT_KEYS = {
leadershipAutofill: CAMPAIGN_INVITE_CONSENT_KEY,
supporterRegistration: SUPPORTER_REGISTRATION_CONSENT_KEY,
supporterVoteIntention: SUPPORTER_VOTE_INTENTION_CONSENT_KEY,

View file

@ -64,10 +64,10 @@ export const actionPlanStatusLabels: Record<(typeof actionPlanStatuses)[number],
cancelado: 'Cancelado',
}
export const MAX_ACTION_PLAN_ORGANIZATIONS = 20
const MAX_ACTION_PLAN_ORGANIZATIONS = 20
export const MAX_ACTION_PLAN_DEMAND_DRAFTS = 20
export const actionPlanDemandDraftSchema = campaignDemandCreateSchema.pick({
const actionPlanDemandDraftSchema = campaignDemandCreateSchema.pick({
title: true,
kind: true,
description: true,

View file

@ -17,13 +17,6 @@ export const campaignDemandCreateSchema = z.object({
leadership: positiveRelationshipId.optional(),
})
export const campaignDemandDetailsUpdateSchema = z.object({
id: positiveRelationshipId,
kind: z.enum(campaignDemandKinds).optional(),
description: trimmedNullableText(4000),
actionPlan: positiveRelationshipId.nullable().optional(),
})
export const campaignDemandTransitionSchema = z.object({
id: positiveRelationshipId,
status: z.enum(campaignDemandStatuses),
@ -36,6 +29,5 @@ export const campaignDemandCostSchema = z.object({
})
export type CampaignDemandCreateInput = z.input<typeof campaignDemandCreateSchema>
export type CampaignDemandDetailsUpdateInput = z.input<typeof campaignDemandDetailsUpdateSchema>
export type CampaignDemandTransitionInput = z.input<typeof campaignDemandTransitionSchema>
export type CampaignDemandCostInput = z.input<typeof campaignDemandCostSchema>

View file

@ -9,4 +9,3 @@ export const commentSchema = z.object({
.optional(),
})
export type Comment = z.infer<typeof commentSchema>

View file

@ -32,4 +32,3 @@ export const contactSchema = z.object({
.optional(),
})
export type Contact = z.infer<typeof contactSchema>

View file

@ -36,8 +36,8 @@ export const isSupportStatus = (value: unknown): value is SupportStatus =>
typeof value === 'string' && leadershipSupportStatuses.some((status) => status === value)
export const MAX_LEADERSHIP_MUNICIPALITIES = 30
export const MAX_LEADERSHIP_ORGANIZATIONS = 20
export const MAX_LEADERSHIP_STATE_DEPUTIES = 20
const MAX_LEADERSHIP_ORGANIZATIONS = 20
const MAX_LEADERSHIP_STATE_DEPUTIES = 20
const municipalitiesArraySchema = z
.array(positiveRelationshipId)
@ -82,5 +82,4 @@ export const leadershipInternalUpdateSchema = z.object({
consentNote: trimmedNullableText(2000),
})
export type LeadershipCreateInput = z.input<typeof leadershipCreateSchema>
export type LeadershipInternalUpdateInput = z.input<typeof leadershipInternalUpdateSchema>

View file

@ -9,7 +9,7 @@ import {
} from '@/lib/schemas/primitives'
import { normalizeSearchPhrase } from '@/lib/wordStartFilter'
export const supporterVoteIntentions = ['certo', 'tende_a_certo', 'indeciso', 'outro'] as const
const supporterVoteIntentions = ['certo', 'tende_a_certo', 'indeciso', 'outro'] as const
export type SupporterVoteIntention = (typeof supporterVoteIntentions)[number]
@ -96,7 +96,3 @@ export const supporterRemoveSchema = z.object({
id: positiveRelationshipId,
})
export type SupporterCreateInput = z.input<typeof supporterCreateSchema>
export type LeaderSupporterCreateInput = z.input<typeof leaderSupporterCreateSchema>
export type SupporterVoteIntentionInput = z.input<typeof supporterVoteIntentionSchema>
export type SupporterImportConfirmInput = z.input<typeof supporterImportConfirmSchema>

View file

@ -127,9 +127,6 @@ export const formatVoteEstimateScenarioAriaLabel = (
}`
}).join('; ')
export const hasCompleteVoteEstimateRange = (estimates: VoteEstimateScenarioViewModel): boolean =>
estimates.pessimistic != null && estimates.central != null && estimates.optimistic != null
/** Coerce staff-entered scenarios into pessimistic ≤ central ≤ optimistic on save. */
export const normalizeVoteEstimateOnSave = (
estimates: VoteEstimateScenarioViewModel,
@ -154,14 +151,3 @@ export const normalizeVoteEstimateOnSave = (
return { pessimistic, central, optimistic }
}
export const appendVoteEstimateScenarioToFormData = (
formData: FormData,
prefix: 'estimatedVotes' | 'expectedVotes',
values: VoteEstimateScenarioFields,
): void => {
for (const scenario of VOTE_ESTIMATE_SCENARIOS) {
const value = values[scenario]
const fieldName = `${prefix}${scenario.charAt(0).toUpperCase()}${scenario.slice(1)}`
formData.set(fieldName, value == null ? '' : String(value))
}
}

View file

@ -6,9 +6,9 @@
* absolute-threshold insight clusters are superseded by E10's relative
* territorial classification (see `docs/plans/classificacao-territorial-relativa.md`).
*/
export const VOTE_TREND_STABLE_BAND = 0.1
const VOTE_TREND_STABLE_BAND = 0.1
export type VoteTrendStatus = 'decline' | 'stable' | 'increase' | 'noBaseline'
type VoteTrendStatus = 'decline' | 'stable' | 'increase' | 'noBaseline'
export type VoteTrendSeries = {
y2014: number

View file

@ -18,7 +18,7 @@ type ContactID = number
const ACCESSIBLE_CONTACT_IDS_CONTEXT_KEY = 'campaignAccessibleContactIds'
export const getAccessibleContactIds = async (
const getAccessibleContactIds = async (
req: PayloadRequest,
user: CampaignActor = req.user,
): Promise<ContactID[]> => {

View file

@ -10,7 +10,7 @@ import { isCampaignStaff, isPayloadAdmin } from '@/utilities/access/shared'
export type ElectionDataReader = CampaignUser | User
export const canReadElectionDataAsUser = (user: CampaignActor): user is ElectionDataReader =>
const canReadElectionDataAsUser = (user: CampaignActor): user is ElectionDataReader =>
isPayloadAdmin(user) || isCampaignStaff(user)
export function assertCanReadElectionData(user: CampaignActor): asserts user is ElectionDataReader {

View file

@ -87,7 +87,7 @@ type OwnLeadership = { id: number; municipalityIDs: number[]; organizationIDs: n
* The engaged leadership record linked to the authenticated leader account
* (contact is unique per person, so there is at most one). Cached per request.
*/
export const getOwnEngagedLeadership = async (
const getOwnEngagedLeadership = async (
req: PayloadRequest,
user: CampaignActor = req.user,
): Promise<OwnLeadership> => {

View file

@ -49,10 +49,6 @@ export const isCampaignUser = (user: CampaignActor): user is CampaignUser =>
export const isCampaignCoordinator = (user: CampaignActor): boolean =>
isCampaignUser(user) && user.role === 'coordinator'
/** "Candidato" — full campaign visibility (superset of coordinator for reads). */
export const isCampaignCandidate = (user: CampaignActor): boolean =>
isCampaignUser(user) && user.role === 'candidate'
/** Coordinator or candidate — unrestricted scope (all municipalities, decisions). */
export const isCampaignUnrestricted = (user: CampaignActor): boolean =>
isCampaignUser(user) && isUnrestrictedCampaignRole(user.role)

View file

@ -14,7 +14,7 @@ export const actionPlanPageSize = 25
export const actionPlanTabs = ['proximos', 'todos', 'realizados', 'rascunhos'] as const
export type ActionPlanTab = (typeof actionPlanTabs)[number]
export type ActionPlanKind = (typeof actionPlanKinds)[number]
type ActionPlanKind = (typeof actionPlanKinds)[number]
export type ActionPlanStatus = (typeof actionPlanStatuses)[number]
export const actionPlanTabLabels: Record<ActionPlanTab, string> = {
@ -34,7 +34,7 @@ export type ActionPlanListState = {
type RawSearchParams = CampaignListRawSearchParams
export const actionPlanListParamNames = ['tab', 'kind', 'status', 'municipality', 'page'] as const
const actionPlanListParamNames = ['tab', 'kind', 'status', 'municipality', 'page'] as const
const actionPlanListParamNameSet = new Set<string>(actionPlanListParamNames)

View file

@ -17,7 +17,7 @@ export const actionPlanMunicipalitySummary = (
: null
/** Short "where" line, e.g. "Itabuna · Feira do Malhado". */
export const formatActionPlanLocationLabel = ({
const formatActionPlanLocationLabel = ({
municipalityName,
locality,
}: {
@ -103,7 +103,7 @@ export const actionPlanFormSelect = {
tasks: true,
} as const
export type ActionPlanFormTaskViewModel = {
type ActionPlanFormTaskViewModel = {
id: string | null
title: string
responsible: { id: number; name: string; phone: string | null } | null
@ -185,7 +185,7 @@ export const toActionPlanFormViewModel = (plan: ActionPlan): ActionPlanFormViewM
})),
})
export const actionPlanDetailContextSelect = {
const actionPlanDetailContextSelect = {
title: true,
slug: true,
kind: true,
@ -246,7 +246,7 @@ export type ActionPlanUpdateViewModel = {
createdAt: string | null
}
export type ActionPlanResultViewModel = {
type ActionPlanResultViewModel = {
summary: string
recordedByName: string | null
recordedAt: string | null

View file

@ -5,11 +5,11 @@
* (or a new one) and re-export them here.
*/
export type { CampaignTransactionRequest } from '@/utilities/access/shared'
export {
eligibleCampaignStaffWhere,
getFreshCampaignUser,
isCampaignCandidate,
isCampaignCoordinator,
isCampaignLeader,
isCampaignStaff,
@ -43,13 +43,13 @@ export {
getAccessibleMunicipalityIds,
getAdvisorMunicipalityIds,
getEngagedLeaderMunicipalityIds,
getOwnEngagedLeadership,
} from '@/utilities/access/municipalities'
export {
canManageContacts,
canReadContacts,
getAccessibleContactIds,
} from '@/utilities/access/contacts'
export {
@ -120,12 +120,12 @@ export {
canUpdateActionPlan,
} from '@/utilities/access/actionPlans'
export type { ElectionDataReader } from '@/utilities/access/elections'
export {
assertCanReadElectionData,
canMutateElectionData,
canReadElectionData,
canReadElectionDataAsUser,
} from '@/utilities/access/elections'
export {

View file

@ -14,7 +14,7 @@ import {
} from '@/utilities/campaignListUrl'
import { isPopulatedRelationship, relationshipId } from '@/utilities/relationship'
export const demandPageSize = 25
const demandPageSize = 25
export type DemandRowViewModel = {
id: number

View file

@ -9,7 +9,7 @@ import { aggregatePledgesByMunicipality } from '@/utilities/votePledgeData'
import { type MunicipalityPledgeAggregate } from '@/utilities/votePledgeViews'
/** Superset of the fields the dashboard, map, and list overview consume. */
export type ScopedMunicipalityDoc = Pick<
type ScopedMunicipalityDoc = Pick<
Municipality,
| 'id'
| 'name'

View file

@ -2,7 +2,7 @@ export const CAMPAIGN_PWA_SCOPE = '/campanha'
export const CAMPAIGN_PWA_MANIFEST_PATH = '/campanha/manifest.webmanifest'
export const CAMPAIGN_PWA_SW_PATH = '/campanha/sw.js'
export const CAMPAIGN_PWA_THEME_COLOR = '#c51414'
export const CAMPAIGN_PWA_BACKGROUND_COLOR = '#ffffff'
const CAMPAIGN_PWA_BACKGROUND_COLOR = '#ffffff'
export const CAMPAIGN_CACHE_PREFIX = 'campanha-'
export const CAMPAIGN_PWA_INSTALL_TOAST_KEY = 'pwa-install-toast-dismissed'
export const CAMPAIGN_PWA_CLEAR_CACHES_MESSAGE = 'clear-campaign-caches' as const
@ -29,7 +29,7 @@ const MANIFEST_ICONS = [
]
/** Precache list: manifest icons + apple-touch (not in the web manifest). */
export const CAMPAIGN_PWA_ICON_PATHS = [
const CAMPAIGN_PWA_ICON_PATHS = [
...MANIFEST_ICONS.map((icon) => icon.src),
'/campaign-icons/apple-touch-icon.png',
] as const

View file

@ -60,19 +60,6 @@ const zonedCivilToInstant = (civil: CivilDateTime): Date => {
throw new Error('Não foi possível calcular o intervalo semanal no fuso da Bahia.')
}
const addCivilDays = (civil: CivilDateTime, days: number): CivilDateTime => {
const date = new Date(civilAsUtcMilliseconds(civil))
date.setUTCDate(date.getUTCDate() + days)
return {
year: date.getUTCFullYear(),
month: date.getUTCMonth() + 1,
day: date.getUTCDate(),
hour: civil.hour,
minute: civil.minute,
second: civil.second,
}
}
const bahiaDateTimeInputPattern = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})$/
/**
@ -131,18 +118,3 @@ export const latestIsoTimestamp = (
return right > left ? right : left
}
export const getBahiaWeekRange = (now: Date): { start: Date; end: Date } => {
const local = getZonedParts(now)
const localMidnight = { ...local, hour: 0, minute: 0, second: 0 }
const weekday = new Date(
Date.UTC(localMidnight.year, localMidnight.month - 1, localMidnight.day),
).getUTCDay()
const daysSinceMonday = weekday === 0 ? 6 : weekday - 1
const monday = addCivilDays(localMidnight, -daysSinceMonday)
const nextMonday = addCivilDays(monday, 7)
return {
start: zonedCivilToInstant(monday),
end: zonedCivilToInstant(nextMonday),
}
}

View file

@ -1,10 +1,10 @@
import type { CampaignUser, Media } from '@/payload-types'
export const CAMPAIGN_AVATAR_MAX_BYTES = 2 * 1024 * 1024
const CAMPAIGN_AVATAR_MAX_BYTES = 2 * 1024 * 1024
export const CAMPAIGN_AVATAR_UNSUPPORTED_MIME_MESSAGE = 'Envie uma imagem JPEG, PNG ou WebP.'
export const CAMPAIGN_AVATAR_MAX_SIZE_MESSAGE = 'A imagem deve ter no máximo 2 MB.'
export const CAMPAIGN_AVATAR_EMPTY_FILE_MESSAGE = 'O arquivo enviado está vazio.'
const CAMPAIGN_AVATAR_UNSUPPORTED_MIME_MESSAGE = 'Envie uma imagem JPEG, PNG ou WebP.'
const CAMPAIGN_AVATAR_MAX_SIZE_MESSAGE = 'A imagem deve ter no máximo 2 MB.'
const CAMPAIGN_AVATAR_EMPTY_FILE_MESSAGE = 'O arquivo enviado está vazio.'
export const campaignRoleLabels: Record<CampaignUser['role'], string> = {
coordinator: 'Coordenador Geral',
@ -27,7 +27,7 @@ export const campaignUserShellView = (
avatarUrl: mediaDocumentUrl(user.avatar),
})
export const CAMPAIGN_AVATAR_MIME_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp'])
const CAMPAIGN_AVATAR_MIME_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp'])
export const campaignUserInitials = (name: string): string => {
const parts = name.trim().split(/\s+/).filter(Boolean).slice(0, 2)

View file

@ -8,9 +8,6 @@ import { getPayload } from 'payload'
type Collection = keyof Config['collections']
export const getDocuments = <Slug extends Collection>(collection: Slug, depth?: number) =>
getPayload({ config: configPromise }).then((payload) => payload.find({ collection, depth }))
/** Bare petition ids (no relationship population) — for `generateStaticParams`. */
export const getPetitionIds = async (): Promise<string[]> => {
const payload = await getPayload({ config: configPromise })
@ -24,7 +21,7 @@ export const getPetitionIds = async (): Promise<string[]> => {
return result.docs.map((doc) => String(doc.id))
}
export const getDocumentById = <Slug extends Collection>(
const getDocumentById = <Slug extends Collection>(
collection: Slug,
id: string | number,
depth?: number,
@ -52,7 +49,7 @@ export const revalidateDocumentById = <Slug extends Collection>(
const getListingTag = <Slug extends Collection>(collection: Slug) => `${collection}s`
export const revalidateCollectionListing = <Slug extends Collection>(collection: Slug) =>
const revalidateCollectionListing = <Slug extends Collection>(collection: Slug) =>
revalidateTag(getListingTag(collection))
export const revalidatePostsListing = () => revalidateCollectionListing('post')

View file

@ -1,6 +1,5 @@
import 'server-only'
import { sql } from '@payloadcms/db-postgres'
import type { Payload } from 'payload'
export type DrizzleTables = Record<string, Record<string, unknown>>
@ -9,10 +8,6 @@ type PayloadDbTables = {
tables: DrizzleTables
}
type PostgresExecuteDb = {
execute: (query: ReturnType<typeof sql>) => Promise<unknown>
}
/** ~15-16 columns × 500 ≈ 7500-8000 bind params — well under PG's 65535 param limit. */
export const INSERT_CHUNK_SIZE = 500
@ -67,17 +62,3 @@ export const drizzleResultRows = (result: unknown): Array<Record<string, unknown
return []
}
/** Returns the pool drizzle client for read-only `execute` aggregates (non-transactional). */
export const requirePostgresDrizzle = (
payload: Pick<Payload, 'db'>,
contextLabel: string,
): PostgresExecuteDb => {
if (payload.db.name !== 'postgres') {
throw new Error(`${contextLabel} exige o adaptador PostgreSQL.`)
}
const drizzle = (payload.db as unknown as { drizzle?: PostgresExecuteDb }).drizzle
if (!drizzle || typeof drizzle.execute !== 'function') {
throw new Error(`A sessão PostgreSQL de ${contextLabel} não está disponível.`)
}
return drizzle
}

View file

@ -19,7 +19,7 @@ const VOTE_TABLE = 'election_candidate_vote'
const TALLY_TABLE = 'election_tally'
const CANDIDATE_TABLE = 'election_candidate'
export type ElectionImportScope = {
type ElectionImportScope = {
year: number
office: ElectionOffice
turn: ElectionTurn
@ -98,7 +98,7 @@ const withTimestamps = <T extends Record<string, unknown>>(row: T, now: string)
* Replace all election data for a single (year, office, turn) scope in one transaction:
* delete existing rows, then bulk-insert the provided bundle.
*/
export const importElectionScope = async (
const importElectionScope = async (
payload: Payload,
bundle: ElectionImportBundle,
): Promise<ElectionImportCounts> => {

View file

@ -8,7 +8,7 @@ import { getPayload } from 'payload'
type Global = keyof Config['globals']
export async function getGlobal<Slug extends Global>(slug: Slug, depth?: number) {
async function getGlobal<Slug extends Global>(slug: Slug, depth?: number) {
const payload = await getPayload({ config: configPromise })
const global = await payload.findGlobal({

View file

@ -13,10 +13,10 @@ import {
strictDecimalInteger,
type RawSearchParams,
} from '@/utilities/campaignListUrl'
import { relationshipId, requireRelationshipId } from '@/utilities/relationship'
import { relationshipId } from '@/utilities/relationship'
import { loadStateDeputySummaries, type StateDeputySummary } from '@/utilities/stateDeputyData'
export const leadershipPageSize = 25
const leadershipPageSize = 25
export type LeadershipRowViewModel = {
id: number
@ -292,6 +292,3 @@ export const loadLeadershipDetail = async (
consentNote: doc.consentNote ?? null,
}
}
export const requireLeadershipContactId = (leadership: Leadership): number =>
requireRelationshipId(leadership.contact)

View file

@ -30,5 +30,3 @@ export const municipalityGeographyWhere = (geography: MunicipalityElectionGeogra
and: [{ cityCode: { equals: geography.cityCode } }, { zoneNumber: { in: geography.zones } }],
})
export const zonesByCityCode = (geography: MunicipalityElectionGeography): Map<string, number[]> =>
new Map([[geography.cityCode, [...geography.zones]]])

View file

@ -24,7 +24,7 @@ import {
type ElectionReader = CampaignUser | User
export type MunicipalityVoteSeriesPoint = {
type MunicipalityVoteSeriesPoint = {
year: number
votes: number
}
@ -186,44 +186,6 @@ const queryMunicipalityElectoralBaseline = async (
}
}
export type CandidateVotesByCity = Map<string, number>
/**
* Statewide nominal votes of one federal-deputy candidate in one year, grouped
* by TSE cityCode. Used by the municipalities map (choropleth per municipality).
*/
export const loadCandidateVotesByCity = async (
payload: Payload,
user: ElectionReader,
{ year, candidateNumber }: { year: number; candidateNumber: number },
): Promise<CandidateVotesByCity> => {
assertCanReadElectionData(user)
const result = await payload.find({
collection: 'electionCandidateVote',
where: {
and: [
{ year: { equals: year } },
{ office: { equals: FEDERAL_DEPUTY_OFFICE } },
{ turn: { equals: '1' } },
{ voteType: { equals: 'nominal' } },
{ candidateNumber: { equals: candidateNumber } },
],
},
depth: 0,
limit: 0,
pagination: false,
select: { cityCode: true, votes: true },
overrideAccess: true,
})
const votesByCity: CandidateVotesByCity = new Map()
for (const row of result.docs) {
votesByCity.set(row.cityCode, (votesByCity.get(row.cityCode) ?? 0) + (row.votes ?? 0))
}
return votesByCity
}
/**
* Nominal votes of one federal-deputy candidate inside a set of municipality
* geographies (per municipality slug), for the selected year.

View file

@ -32,7 +32,7 @@ export const municipalityMapYearLabels: Record<MunicipalityMapYear, string> = {
2026: '2026 (estimativas)',
}
export type MunicipalityZoneBreakdownRow = {
type MunicipalityZoneBreakdownRow = {
slug: string
name: string
votesByYear: Record<string, number>

View file

@ -34,18 +34,18 @@ import {
} from '@/lib/voteEstimate'
import { emptyMunicipalityPledgeAggregate, resolveMunicipalityStaffVoteTotal, type MunicipalityPledgeAggregate } from '@/utilities/votePledgeViews'
export type { MunicipalityMapSlugEntry, MunicipalitiesByIbgeCode } from '@/utilities/municipalityMapNavigation'
// Client components import the map vocabulary from the contract module; the
// server side re-exports it so data callers have one import surface.
export type {
MunicipalityMapBundle,
MunicipalityMapComparison,
MunicipalityMapScaleMode,
MunicipalityMapYear,
MunicipalityZoneBreakdownRow,
} from '@/utilities/municipalityMapContract'
export type ScopedMunicipality = {
type ScopedMunicipality = {
id: number
slug: string
name: string
@ -64,7 +64,7 @@ type ScopedMunicipalityDoc = {
expectedVotes?: VoteEstimateScenarioFields | null
}
export const scopeMunicipalitiesFromDocs = (docs: ReadonlyArray<ScopedMunicipalityDoc>): ScopedMunicipality[] =>
const scopeMunicipalitiesFromDocs = (docs: ReadonlyArray<ScopedMunicipalityDoc>): ScopedMunicipality[] =>
docs.flatMap((municipality) => {
const entry = getMunicipalityCatalogEntry(municipality.slug)
if (!entry) return []
@ -81,7 +81,7 @@ export const scopeMunicipalitiesFromDocs = (docs: ReadonlyArray<ScopedMunicipali
]
})
export const buildMunicipalityMapBundleFromMunicipalities = async (
const buildMunicipalityMapBundleFromMunicipalities = async (
payload: Payload,
user: CampaignUser,
state: MunicipalityListState,

View file

@ -1,4 +1,4 @@
export type MunicipalityMapSlugEntry = {
type MunicipalityMapSlugEntry = {
slug: string
name: string
}

View file

@ -1,6 +1,6 @@
import { revalidatePath } from 'next/cache'
export type MunicipalityListRevalidateScope = 'list' | 'detail' | 'both'
type MunicipalityListRevalidateScope = 'list' | 'detail' | 'both'
const municipalitySlugPathPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/

View file

@ -14,7 +14,7 @@ import {
} from '@/utilities/campaignListUrl'
import { relationshipId } from '@/utilities/relationship'
export const organizationPageSize = 25
const organizationPageSize = 25
export type OrganizationRowViewModel = {
id: number

View file

@ -6,7 +6,7 @@ import configPromise from '@payload-config'
import { unstable_cache } from 'next/cache'
import { getPayload } from 'payload'
export const POST_TYPES = ['noticia', 'campanha', 'artigo', 'evento'] as const
const POST_TYPES = ['noticia', 'campanha', 'artigo', 'evento'] as const
export type PostType = (typeof POST_TYPES)[number]
@ -97,7 +97,7 @@ const findPublishedPostBySlug = (slug: string, depth: number) =>
* Cached list of every published post. Tagged with the shared `posts` listing
* tag so `revalidatePostsListing()` (called from Post/Tag `afterChange`) busts it.
*/
export const getCachedPublishedPosts = (depth = 1) =>
const getCachedPublishedPosts = (depth = 1) =>
unstable_cache(() => findPublishedPosts(depth), ['posts', String(depth)], {
tags: ['posts'],
})

View file

@ -2,7 +2,7 @@ export const STORAGE_KEY = 'teqo:campaign:recent-visits'
export const MAX_ENTRIES = 8
export const RECORD_DWELL_MS = 2000
export type RecentVisitKind = 'municipality' | 'municipalityList'
type RecentVisitKind = 'municipality' | 'municipalityList'
export type RecentVisitEntry = {
href: string

View file

@ -11,7 +11,7 @@ const ALLOWED_REVALIDATE_TAGS = [
ELECTION_TSE_CACHE_TAG,
] as const
export type AllowedRevalidateTag = (typeof ALLOWED_REVALIDATE_TAGS)[number]
type AllowedRevalidateTag = (typeof ALLOWED_REVALIDATE_TAGS)[number]
const allowedTagSet = new Set<string>(ALLOWED_REVALIDATE_TAGS)

View file

@ -12,7 +12,7 @@ import {
} from '@/utilities/campaignListUrl'
import { relationshipId } from '@/utilities/relationship'
export const stateDeputyPageSize = 25
const stateDeputyPageSize = 25
export type StateDeputyRowViewModel = {
id: number

View file

@ -1,6 +1,6 @@
import type { SupporterVoteIntention } from '@/lib/schemas/supporter'
export type SupporterImportRowStatus =
type SupporterImportRowStatus =
| 'ok'
| 'duplicado_pelo_telefone'
| 'telefone_invalido'

View file

@ -7,7 +7,7 @@ import type { Payload } from 'payload'
import type { SupporterVoteIntention } from '@/lib/schemas/supporter'
import type { PayloadTransactionRequest } from '@/utilities/payloadTransaction'
export type SupporterImportOkRow = {
type SupporterImportOkRow = {
nome: string
telefone: string
municipio?: string

View file

@ -50,7 +50,7 @@ const supporterDetailSelect = {
createdBy: true,
} as const
export const loadSupporterListPageData = async (
const loadSupporterListPageData = async (
payload: Pick<Payload, 'find' | 'count'>,
user: CampaignUser,
searchParams: Promise<SupporterListSearchParams> | SupporterListSearchParams,

View file

@ -44,7 +44,7 @@ export type SupporterListState = {
type RawSearchParams = CampaignListRawSearchParams
export const supporterListParamNames = [
const supporterListParamNames = [
'q',
'voteIntention',
'city',

View file

@ -1,5 +1,4 @@
import {
bahiaIdentityTerritories,
bahiaMunicipalities,
citiesForTerritory,
isBahiaIdentityTerritory,
@ -13,10 +12,8 @@ export type StrictComboboxOption = {
const toOptions = (values: readonly string[]): StrictComboboxOption[] =>
values.map((value) => ({ label: value, value }))
export const territoryComboboxOptions = toOptions(bahiaIdentityTerritories)
/** Full Bahia municipality list — stable module reference for client filters. */
export const allMunicipalityComboboxOptions = toOptions(bahiaMunicipalities)
const allMunicipalityComboboxOptions = toOptions(bahiaMunicipalities)
export const municipalityComboboxOptions = (region?: string | null): StrictComboboxOption[] =>
region && isBahiaIdentityTerritory(region)

View file

@ -29,7 +29,7 @@ export type TerritoryMunicipalityInput = {
advisorCount: number
}
export type TerritoryOverviewSubRow = {
type TerritoryOverviewSubRow = {
label: string
municipalityCount: number
votesByYear: Record<string, number>

View file

@ -338,7 +338,7 @@ class CampaignE2EOwnership {
}
}
export type CampaignE2EFixture = {
type CampaignE2EFixture = {
baseURL: string
fixtures: CampaignE2EOwnership
login: (page: Page, identifier: string, password: string) => Promise<void>

View file

@ -315,7 +315,7 @@ export const acquireSharedTestDatabaseLease = async (
leaseKey: string,
): Promise<TestDatabaseLease> => beginTestDatabaseLease(payload, leaseKey, 'shared')
export const withTestDatabaseLease = async <Result>(
const withTestDatabaseLease = async <Result>(
payload: Payload,
leaseKey: string,
operation: () => Promise<Result>,

View file

@ -1,14 +1,11 @@
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import type { Payload } from 'payload'
import { ELECTION_YEAR_2014, ELECTION_YEAR_2018 } from '@/lib/electionResults'
import { parseTseCsvString } from '@/lib/electionResultsCsv'
import { buildElectionResultsFromCsvRows, FEDERAL_ONLY_OFFICES } from '@/lib/electionResultsBuild'
import { buildElectionResultsFromCsvRows } from '@/lib/electionResultsBuild'
import type { BuiltElectionResults } from '@/lib/electionResultsBuild'
import type { TseCsvRow } from '@/lib/electionResultsParse'
import type { ElectionOffice } from '@/lib/electionResults'
import { buildImportBundles, importElectionBundles } from '@/utilities/electionResultsImport'
const fixtureDir = join(process.cwd(), 'tests/fixtures/tse')
@ -62,21 +59,3 @@ export const TSE_FIXTURE_EXPECTED = {
tallyRowCount: 8,
} as const
/** Per-zone federal deputy T1 totals used by list-election overview int tests. */
export const TSE_FIXTURE_ZONE_EXPECTED = {
salvadorZ1: { aptos: 10_000, abstencoes: 2000, sollaVotes2022: 1200, confirmedVoteEstimate: 2000 },
salvadorZ2: { aptos: 9000, abstencoes: 1800, sollaVotes2022: 900, confirmedVoteEstimate: 100 },
} as const
/** Import 2022 full scope plus federal-only 2018/2014 for E2 trend series int tests. */
export const seedMultiYearFederalCandidateFixture = async (payload: Payload): Promise<void> => {
await importElectionBundles(payload, buildImportBundles(loadTseFixtureResults()))
await importElectionBundles(
payload,
buildImportBundles(loadTseFixtureResultsForYear(ELECTION_YEAR_2018, FEDERAL_ONLY_OFFICES)),
)
await importElectionBundles(
payload,
buildImportBundles(loadTseFixtureResultsForYear(ELECTION_YEAR_2014, FEDERAL_ONLY_OFFICES)),
)
}