- Add q param to demand list URL contract and buildDemandListWhere - Extend demandOmnibox with kind + search chips/suggestions/actions - Search title and leadership contact name (visible list columns) - clearDemandOmnibox drops bar filters but keeps activity deep-link - Unit + int tests; impl plan docs/plans/demandas-omnibox-tipo-busca-impl.md Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
218 lines
6.3 KiB
TypeScript
218 lines
6.3 KiB
TypeScript
import 'server-only'
|
|
|
|
import type { Payload } from 'payload'
|
|
|
|
import { isPopulatedRelationship, relationshipId } from '@/lib/relationship'
|
|
import type { CampaignDemandKind, CampaignDemandStatus } from '@/lib/schemas/campaignDemand'
|
|
import type { Activity, CampaignDemand, CampaignUser } from '@/payload-types'
|
|
import { isCampaignStaff } from '@/utilities/campaignAccess'
|
|
import {
|
|
buildDemandListHref,
|
|
buildDemandListWhere,
|
|
parseDemandListParams,
|
|
type DemandListState,
|
|
} from '@/utilities/demand/demandListUrl'
|
|
import {
|
|
loadCampaignUserNamesByIds,
|
|
loadLeadershipContactNamesByIds,
|
|
loadMunicipalityLabelsByIds,
|
|
} from '@/utilities/loadNamesByIds'
|
|
|
|
export { buildDemandListHref, parseDemandListParams }
|
|
export type { DemandListState }
|
|
|
|
const demandPageSize = 25
|
|
|
|
export type DemandRowViewModel = {
|
|
id: number
|
|
title: string
|
|
slug: string
|
|
kind: CampaignDemandKind
|
|
status: CampaignDemandStatus
|
|
municipalityId: number
|
|
municipalityName: string
|
|
municipalitySlug: string
|
|
requesterName: string | null
|
|
createdAt: string
|
|
}
|
|
|
|
const resolveMunicipalityAndRequesterNames = async (
|
|
payload: Payload,
|
|
demands: CampaignDemand[],
|
|
): Promise<{
|
|
municipalityBy: Map<number, { name: string; slug: string }>
|
|
requesterBy: Map<number, string>
|
|
}> => {
|
|
const municipalityIDs = [
|
|
...new Set(
|
|
demands
|
|
.map((demand) => relationshipId(demand.municipality))
|
|
.filter((id): id is number => id !== null),
|
|
),
|
|
]
|
|
const leadershipIDs = [
|
|
...new Set(
|
|
demands
|
|
.map((demand) => relationshipId(demand.leadership))
|
|
.filter((id): id is number => id !== null),
|
|
),
|
|
]
|
|
|
|
const [municipalityBy, contactNameByLeadershipId] = await Promise.all([
|
|
loadMunicipalityLabelsByIds(payload, municipalityIDs),
|
|
loadLeadershipContactNamesByIds(payload, leadershipIDs),
|
|
])
|
|
|
|
return {
|
|
municipalityBy,
|
|
requesterBy: contactNameByLeadershipId,
|
|
}
|
|
}
|
|
|
|
const toDemandRow = (
|
|
demand: CampaignDemand,
|
|
municipalityBy: Map<number, { name: string; slug: string }>,
|
|
requesterBy: Map<number, string>,
|
|
): DemandRowViewModel => {
|
|
const municipality = municipalityBy.get(relationshipId(demand.municipality) ?? -1)
|
|
const municipalityID = relationshipId(demand.municipality)
|
|
const leadershipID = relationshipId(demand.leadership)
|
|
return {
|
|
id: demand.id,
|
|
title: demand.title,
|
|
slug: demand.slug,
|
|
kind: demand.kind as CampaignDemandKind,
|
|
status: demand.status as CampaignDemandStatus,
|
|
municipalityId: municipalityID ?? 0,
|
|
municipalityName: municipality?.name ?? 'Município',
|
|
municipalitySlug: municipality?.slug ?? '',
|
|
requesterName: leadershipID ? (requesterBy.get(leadershipID) ?? null) : null,
|
|
createdAt: demand.createdAt,
|
|
}
|
|
}
|
|
|
|
export const loadDemandListPageData = async (
|
|
payload: Payload,
|
|
user: CampaignUser,
|
|
state: DemandListState,
|
|
): Promise<{
|
|
rows: DemandRowViewModel[]
|
|
totalDocs: number
|
|
totalPages: number
|
|
openCount: number
|
|
}> => {
|
|
const [result, openCount] = await Promise.all([
|
|
payload.find({
|
|
collection: 'campaignDemand',
|
|
where: buildDemandListWhere(state),
|
|
depth: 0,
|
|
limit: demandPageSize,
|
|
page: state.page,
|
|
sort: '-createdAt',
|
|
user,
|
|
overrideAccess: false,
|
|
}),
|
|
payload.count({
|
|
collection: 'campaignDemand',
|
|
where: { status: { in: ['aberta', 'em_analise', 'escalada'] } },
|
|
user,
|
|
overrideAccess: false,
|
|
}),
|
|
])
|
|
|
|
const { municipalityBy, requesterBy } = await resolveMunicipalityAndRequesterNames(
|
|
payload,
|
|
result.docs as CampaignDemand[],
|
|
)
|
|
|
|
return {
|
|
rows: (result.docs as CampaignDemand[]).map((demand) =>
|
|
toDemandRow(demand, municipalityBy, requesterBy),
|
|
),
|
|
totalDocs: result.totalDocs,
|
|
totalPages: result.totalPages,
|
|
openCount: openCount.totalDocs,
|
|
}
|
|
}
|
|
|
|
export type DemandDetailViewModel = DemandRowViewModel & {
|
|
description: string | null
|
|
decisionNote: string | null
|
|
decidedAt: string | null
|
|
/** Staff-only (field access strips for leaders). */
|
|
cost: number | null
|
|
receipts: Array<{ id: number; url: string | null; filename: string | null }>
|
|
statusHistory: Array<{
|
|
status: CampaignDemandStatus
|
|
note: string | null
|
|
authorName: string | null
|
|
createdAt: string | null
|
|
}>
|
|
canLeaderEdit: boolean
|
|
activity: { title: string; slug: string } | null
|
|
}
|
|
|
|
export const loadDemandDetail = async (
|
|
payload: Payload,
|
|
user: CampaignUser,
|
|
slug: string,
|
|
): Promise<DemandDetailViewModel | null> => {
|
|
const result = await payload.find({
|
|
collection: 'campaignDemand',
|
|
where: { slug: { equals: slug } },
|
|
depth: 1,
|
|
limit: 1,
|
|
pagination: false,
|
|
user,
|
|
overrideAccess: false,
|
|
})
|
|
const demand = result.docs[0] as CampaignDemand | undefined
|
|
if (!demand) return null
|
|
|
|
const { municipalityBy, requesterBy } = await resolveMunicipalityAndRequesterNames(payload, [
|
|
demand,
|
|
])
|
|
const row = toDemandRow(demand, municipalityBy, requesterBy)
|
|
|
|
const authorIDs = [
|
|
...new Set(
|
|
(demand.statusHistory ?? [])
|
|
.map((entry) => relationshipId(entry.author))
|
|
.filter((id): id is number => id !== null),
|
|
),
|
|
]
|
|
const authorNameById = await loadCampaignUserNamesByIds(payload, authorIDs)
|
|
|
|
return {
|
|
...row,
|
|
description: demand.description ?? null,
|
|
decisionNote: demand.decisionNote ?? null,
|
|
decidedAt: demand.decidedAt ?? null,
|
|
cost: isCampaignStaff(user) ? (demand.cost ?? null) : null,
|
|
receipts: isCampaignStaff(user)
|
|
? (demand.receipts ?? []).flatMap((receipt) =>
|
|
typeof receipt === 'object' && receipt !== null
|
|
? [
|
|
{
|
|
id: receipt.id,
|
|
url: receipt.url ?? null,
|
|
filename: receipt.filename ?? null,
|
|
},
|
|
]
|
|
: [],
|
|
)
|
|
: [],
|
|
statusHistory: isCampaignStaff(user)
|
|
? (demand.statusHistory ?? []).map((entry) => ({
|
|
status: entry.status as CampaignDemandStatus,
|
|
note: entry.note ?? null,
|
|
authorName: authorNameById.get(relationshipId(entry.author) ?? -1) ?? null,
|
|
createdAt: entry.createdAt ?? null,
|
|
}))
|
|
: [],
|
|
canLeaderEdit: user.role === 'leader' && demand.status === 'aberta',
|
|
activity: isPopulatedRelationship<Activity>(demand.activity)
|
|
? { title: demand.activity.title, slug: demand.activity.slug }
|
|
: null,
|
|
}
|
|
}
|