mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
122 lines
4.4 KiB
TypeScript
122 lines
4.4 KiB
TypeScript
import { engagementLevelRank, type EngagementLevel } from '@/lib/engagementLevel'
|
|
import { HOME_SEARCH_SUGGEST_LIMIT } from '@/lib/homeSearchSuggest'
|
|
import type { PoliticalTrendStatusValue } from '@/lib/schemas/municipality'
|
|
import type { MunicipalityTerritorialClass } from '@/lib/territorialClassAnchors'
|
|
import { territorialClassSortWeight } from '@/lib/territorialClassSortWeight'
|
|
|
|
export type WizardMunicipalitySuggestInput = {
|
|
slug: string
|
|
name: string
|
|
/** ISO timestamp or null when never signaled — colder sorts first. */
|
|
lastSignalAt: string | null
|
|
engagementLevel: EngagementLevel | null
|
|
politicalTrend: PoliticalTrendStatusValue | null
|
|
territorialClass: MunicipalityTerritorialClass
|
|
/** Nominal 2022 candidate votes; null when absent from artifact. */
|
|
votes2022: number | null
|
|
}
|
|
|
|
/** Higher = more urgent for wizard tie-break (desfavorável first). */
|
|
const politicalTrendSortWeight: Record<PoliticalTrendStatusValue, number> = {
|
|
desfavoravel: 3,
|
|
neutra: 2,
|
|
favoravel: 1,
|
|
}
|
|
|
|
const municipalityNameCompare = (
|
|
left: WizardMunicipalitySuggestInput,
|
|
right: WizardMunicipalitySuggestInput,
|
|
): number => left.name.localeCompare(right.name, 'pt-BR')
|
|
|
|
const compareEngagementLevel = (
|
|
left: WizardMunicipalitySuggestInput,
|
|
right: WizardMunicipalitySuggestInput,
|
|
): number => {
|
|
const leftRank = left.engagementLevel === null ? -1 : engagementLevelRank[left.engagementLevel]
|
|
const rightRank = right.engagementLevel === null ? -1 : engagementLevelRank[right.engagementLevel]
|
|
if (leftRank !== rightRank) return rightRank - leftRank
|
|
return 0
|
|
}
|
|
|
|
const comparePoliticalTrend = (
|
|
left: WizardMunicipalitySuggestInput,
|
|
right: WizardMunicipalitySuggestInput,
|
|
): number => {
|
|
const leftWeight =
|
|
left.politicalTrend === null ? 0 : politicalTrendSortWeight[left.politicalTrend]
|
|
const rightWeight =
|
|
right.politicalTrend === null ? 0 : politicalTrendSortWeight[right.politicalTrend]
|
|
if (leftWeight !== rightWeight) return rightWeight - leftWeight
|
|
return 0
|
|
}
|
|
|
|
const compareTerritorialClass = (
|
|
left: WizardMunicipalitySuggestInput,
|
|
right: WizardMunicipalitySuggestInput,
|
|
): number => {
|
|
const leftWeight = territorialClassSortWeight[left.territorialClass]
|
|
const rightWeight = territorialClassSortWeight[right.territorialClass]
|
|
if (leftWeight === null && rightWeight === null) return 0
|
|
if (leftWeight === null) return 1
|
|
if (rightWeight === null) return -1
|
|
if (leftWeight !== rightWeight) return rightWeight - leftWeight
|
|
return 0
|
|
}
|
|
|
|
const compareVotes2022 = (
|
|
left: WizardMunicipalitySuggestInput,
|
|
right: WizardMunicipalitySuggestInput,
|
|
): number => {
|
|
const leftVotes = left.votes2022
|
|
const rightVotes = right.votes2022
|
|
if (leftVotes === null && rightVotes === null) return 0
|
|
if (leftVotes === null || leftVotes === 0) return 1
|
|
if (rightVotes === null || rightVotes === 0) return -1
|
|
if (leftVotes !== rightVotes) return rightVotes - leftVotes
|
|
return 0
|
|
}
|
|
|
|
/** Older signal first; null = coldest; equal timestamps defer to later tie-breakers. */
|
|
const compareSignalOldestFirst = (
|
|
left: WizardMunicipalitySuggestInput,
|
|
right: WizardMunicipalitySuggestInput,
|
|
): number => {
|
|
if (left.lastSignalAt === null && right.lastSignalAt === null) return 0
|
|
if (left.lastSignalAt === null) return -1
|
|
if (right.lastSignalAt === null) return 1
|
|
const byTime = left.lastSignalAt.localeCompare(right.lastSignalAt)
|
|
if (byTime !== 0) return byTime
|
|
return 0
|
|
}
|
|
|
|
const compareWizardMunicipalitySuggest = (
|
|
left: WizardMunicipalitySuggestInput,
|
|
right: WizardMunicipalitySuggestInput,
|
|
): number => {
|
|
const bySignal = compareSignalOldestFirst(left, right)
|
|
if (bySignal !== 0) return bySignal
|
|
|
|
const byEngagement = compareEngagementLevel(left, right)
|
|
if (byEngagement !== 0) return byEngagement
|
|
|
|
const byTrend = comparePoliticalTrend(left, right)
|
|
if (byTrend !== 0) return byTrend
|
|
|
|
const byClass = compareTerritorialClass(left, right)
|
|
if (byClass !== 0) return byClass
|
|
|
|
const byVotes = compareVotes2022(left, right)
|
|
if (byVotes !== 0) return byVotes
|
|
|
|
return municipalityNameCompare(left, right)
|
|
}
|
|
|
|
/**
|
|
* B92 — wizard municipality idle suggestions: coldest signal first, then
|
|
* operational tie-breakers (E14 → trend → E10 class → 2022 votes → name).
|
|
*/
|
|
export const rankWizardMunicipalitySuggestions = (
|
|
municipalities: WizardMunicipalitySuggestInput[],
|
|
limit = HOME_SEARCH_SUGGEST_LIMIT,
|
|
): WizardMunicipalitySuggestInput[] =>
|
|
[...municipalities].sort(compareWizardMunicipalitySuggest).slice(0, limit)
|