teqo/src/app/(campaign)/campanha/(app)/demandas/[slug]/page.tsx
Francisco Ignácio Fontoura Solla 4806c417c8 Engineering Pass 3: consolidate patterns and lock them with guards.
Fold duplicated campanha access, loaders, list/filter/form shells, and scripts into shared modules; reorganize utilities by domain; stabilize e2e; and document prevention-first audit workflow so the same smells cannot creep back in.
2026-07-29 06:11:29 -03:00

183 lines
6.6 KiB
TypeScript

import config from '@payload-config'
import { ArrowLeftIcon, PaperclipIcon } from 'lucide-react'
import Link from 'next/link'
import { notFound } from 'next/navigation'
import { getPayload } from 'payload'
import { DemandWorkflowCard } from '@/components/campaign/demand/DemandWorkflowCard'
import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell'
import { Badge } from '@/components/ui/Badge'
import { Button } from '@/components/ui/button'
import {
campaignDemandKindLabels,
campaignDemandStatusLabels,
type CampaignDemandStatus,
} from '@/lib/schemas/campaignDemand'
import { isCampaignUnrestricted } from '@/utilities/campaignAccess'
import { loadDemandDetail } from '@/utilities/campaignDemandData'
import { requireCampaignPageActor } from '@/utilities/campaignPageActor'
import {
attachDemandReceiptFormAction,
setDemandCostFormAction,
transitionDemandFormAction,
} from './formActions'
type DemandDetailPageProps = {
params: Promise<{ slug: string }>
}
const statusVariant: Record<
CampaignDemandStatus,
'secondary' | 'estimate-pending' | 'destructive' | 'estimate-confirmed'
> = {
aberta: 'estimate-pending',
em_analise: 'secondary',
escalada: 'estimate-pending',
aprovada: 'estimate-confirmed',
rejeitada: 'destructive',
}
const dateTimeFormatter = new Intl.DateTimeFormat('pt-BR', {
dateStyle: 'short',
timeStyle: 'short',
})
const currencyFormatter = new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL',
})
export default async function DemandDetailPage({ params }: DemandDetailPageProps) {
const { slug } = await params
const [user, payload] = await Promise.all([
requireCampaignPageActor({ gate: 'staff' }),
getPayload({ config }),
])
const demand = await loadDemandDetail(payload, user, slug)
if (!demand) notFound()
return (
<CampaignPageShell>
<header className="flex flex-col gap-2">
<Button asChild variant="ghost" className="min-h-11 self-start">
<Link href="/campanha/demandas">
<ArrowLeftIcon data-icon="inline-start" aria-hidden="true" />
Voltar para demandas
</Link>
</Button>
<div className="flex flex-wrap items-center gap-2">
<h1 className="text-2xl font-semibold tracking-tight">{demand.title}</h1>
<Badge variant={statusVariant[demand.status]}>
{campaignDemandStatusLabels[demand.status]}
</Badge>
</div>
<p className="text-muted-foreground">
{campaignDemandKindLabels[demand.kind]} ·{' '}
<Link
href={`/campanha/municipios/${demand.municipalitySlug}`}
className="text-primary underline-offset-4 hover:underline"
>
{demand.municipalityName}
</Link>
{demand.activity ? (
<>
{' '}
· Atividade:{' '}
<Link
href={`/campanha/atividades/${demand.activity.slug}`}
className="text-primary underline-offset-4 hover:underline"
>
{demand.activity.title}
</Link>
</>
) : null}
{demand.requesterName ? ` · Solicitada por ${demand.requesterName}` : ''} · Aberta em{' '}
{dateTimeFormatter.format(new Date(demand.createdAt))}
</p>
</header>
{demand.description ? (
<section aria-label="Descrição da demanda" className="rounded-xl border p-4">
<p className="whitespace-pre-wrap text-sm">{demand.description}</p>
</section>
) : null}
{demand.decisionNote && (demand.status === 'aprovada' || demand.status === 'rejeitada') ? (
<section aria-label="Decisão" className="rounded-xl border p-4">
<h2 className="text-sm font-medium">Nota da decisão</h2>
<p className="mt-1 whitespace-pre-wrap text-sm text-muted-foreground">
{demand.decisionNote}
</p>
{demand.decidedAt ? (
<p className="mt-1 text-xs text-muted-foreground">
Decidida em {dateTimeFormatter.format(new Date(demand.decidedAt))}
</p>
) : null}
</section>
) : null}
<DemandWorkflowCard
demandID={demand.id}
status={demand.status}
canDecideEscalated={isCampaignUnrestricted(user)}
currentCost={demand.cost}
transitionFormAction={transitionDemandFormAction}
costFormAction={setDemandCostFormAction}
receiptFormAction={attachDemandReceiptFormAction}
/>
<section
aria-labelledby="demand-internal-title"
className="flex flex-col gap-3 rounded-xl border p-4"
>
<h2 id="demand-internal-title" className="text-base font-medium">
Controle interno
</h2>
<p className="text-sm text-muted-foreground">
Custo: {demand.cost != null ? currencyFormatter.format(demand.cost) : 'não registrado'}
</p>
{demand.receipts.length ? (
<ul className="flex flex-col gap-1">
{demand.receipts.map((receipt) => (
<li key={receipt.id} className="flex items-center gap-2 text-sm">
<PaperclipIcon className="size-4 text-muted-foreground" aria-hidden="true" />
{receipt.url ? (
<a
href={receipt.url}
target="_blank"
rel="noopener noreferrer"
className="text-primary underline-offset-4 hover:underline"
>
{receipt.filename ?? `Comprovante ${receipt.id}`}
</a>
) : (
<span>{receipt.filename ?? `Comprovante ${receipt.id}`}</span>
)}
</li>
))}
</ul>
) : (
<p className="text-sm text-muted-foreground">Nenhum comprovante anexado.</p>
)}
{demand.statusHistory.length ? (
<div className="flex flex-col gap-1">
<h3 className="text-sm font-medium">Histórico</h3>
<ul className="flex flex-col gap-1 text-sm text-muted-foreground">
{demand.statusHistory.map((entry, index) => (
<li key={`${entry.status}-${index}`}>
{campaignDemandStatusLabels[entry.status]}
{entry.authorName ? ` — ${entry.authorName}` : ''}
{entry.createdAt
? ` · ${dateTimeFormatter.format(new Date(entry.createdAt))}`
: ''}
{entry.note ? ` · ${entry.note}` : ''}
</li>
))}
</ul>
</div>
) : null}
</section>
</CampaignPageShell>
)
}