mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
Closes the 'Feel the action' gaps (.cursor/rules/campanha-action-feedback.mdc): - BUG: action-plan task checkbox was optimistic but never revalidated — a successful toggle visually reverted when the transition ended. The action now revalidates the plan page; checkbox disables while pending - map compare + candidate compare picker: optimistic select value + useTransition; the map region dims with aria-busy and a live region instead of flashing the full-page skeleton - CampaignListPendingBoundary/CampaignListResults: one shared transition so filters (municípios/apoiadores/planos), pagination, demandas status chips (new CampaignFilterChips), and the name-search pages (new CampaignSearchForm on lideranças/organizações/dobradinhas) dim the RESULTS region, not only the control — children stay server-rendered (composition) - streaming: dashboard map section behind Suspense (KPI shell paints first; React cache() keeps the municipality scope shared, not reloaded); municipality detail tabs (elections/leaderships/updates) stream below the header; list-shaped loading.tsx for /municipios and /apoiadores - logout button gets a pending state; BahiaMap init shows a visible spinner (was sr-only)
73 lines
2.6 KiB
TypeScript
73 lines
2.6 KiB
TypeScript
'use client'
|
|
|
|
import { useOptimistic, useTransition } from 'react'
|
|
import { toast } from 'sonner'
|
|
|
|
import { toggleActionPlanTaskAction } from '@/app/(campaign)/campanha/(app)/planos/[slug]/taskActions'
|
|
import { Checkbox } from '@/components/ui/Checkbox'
|
|
import { formatBahiaDateTimeLabel } from '@/utilities/campaignTime'
|
|
import type { ActionPlanTaskViewModel } from '@/utilities/actionPlanViewModels'
|
|
|
|
type ActionPlanTaskChecklistProps = {
|
|
planId: number
|
|
tasks: ActionPlanTaskViewModel[]
|
|
}
|
|
|
|
export const ActionPlanTaskChecklist = ({ planId, tasks }: ActionPlanTaskChecklistProps) => {
|
|
const [isPending, startTransition] = useTransition()
|
|
const [optimisticTasks, setOptimisticDone] = useOptimistic(
|
|
tasks,
|
|
(currentTasks, { taskId, done }: { taskId: string; done: boolean }) =>
|
|
currentTasks.map((task) => (task.id === taskId ? { ...task, done } : task)),
|
|
)
|
|
|
|
const toggle = (taskId: string | null, done: boolean) => {
|
|
if (!taskId) return
|
|
|
|
startTransition(async () => {
|
|
setOptimisticDone({ taskId, done })
|
|
const result = await toggleActionPlanTaskAction(planId, taskId, done)
|
|
// On failure the optimistic state rolls back automatically when the
|
|
// transition ends (the action does not revalidate on error).
|
|
if (!result.ok) toast.error(result.message)
|
|
})
|
|
}
|
|
|
|
if (!optimisticTasks.length) {
|
|
return <p className="text-sm text-muted-foreground">Nenhuma tarefa cadastrada.</p>
|
|
}
|
|
|
|
return (
|
|
<ul role="list" className="flex flex-col gap-2" aria-busy={isPending}>
|
|
{optimisticTasks.map((task) => (
|
|
<li
|
|
key={task.id ?? task.title}
|
|
className="flex items-start gap-3 rounded-lg border p-3"
|
|
data-done={task.done}
|
|
data-pending={isPending || undefined}
|
|
>
|
|
<Checkbox
|
|
checked={task.done}
|
|
disabled={isPending}
|
|
onCheckedChange={(checked) => toggle(task.id, checked === true)}
|
|
aria-label={`Marcar tarefa "${task.title}" como ${task.done ? 'pendente' : 'concluída'}`}
|
|
className="mt-0.5"
|
|
/>
|
|
<div className="flex flex-1 flex-col gap-0.5">
|
|
<span className={task.done ? 'line-through text-muted-foreground' : undefined}>
|
|
{task.title}
|
|
</span>
|
|
<span className="text-xs text-muted-foreground">
|
|
{[
|
|
task.responsibleName ? `Resp: ${task.responsibleName}` : null,
|
|
task.due ? `Prazo: ${formatBahiaDateTimeLabel(task.due)}` : null,
|
|
]
|
|
.filter(Boolean)
|
|
.join(' · ')}
|
|
</span>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)
|
|
}
|