feat(ST-1109.8): show PBI status badge and consolidate filters into popover
- Pbi.status (lowercase API) flows from page.tsx via pbiStatusToApi - Status badge rendered in BacklogCard's badge slot using PBI_STATUS_COLORS - Two old Select dropdowns replaced by single Popover with three pill-button sections (Sorteren, Prioriteit, Status) and a "Wis filters" footer - Filter trigger shows active count "(n)" badge in label - Active priority/status filters still surface as dismissable chips next to the trigger for at-a-glance feedback - onEdit passes the full Pbi (incl. status) so the dialog opens with the correct current status — closes the data flow loop opened in ST-1109.7 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2381832c67
commit
72d72fd648
2 changed files with 139 additions and 34 deletions
|
|
@ -2,6 +2,7 @@ import { notFound, redirect } from 'next/navigation'
|
|||
import { getSession } from '@/lib/auth'
|
||||
import { getAccessibleProduct } from '@/lib/product-access'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { pbiStatusToApi } from '@/lib/task-status'
|
||||
import { SplitPane } from '@/components/split-pane/split-pane'
|
||||
import { PbiList } from '@/components/backlog/pbi-list'
|
||||
import { StoryPanel } from '@/components/backlog/story-panel'
|
||||
|
|
@ -94,7 +95,7 @@ export default async function ProductBacklogPage({ params }: Props) {
|
|||
left={
|
||||
<PbiList
|
||||
productId={id}
|
||||
pbis={pbis.map((p: (typeof pbis)[number]) => ({ id: p.id, code: p.code, title: p.title, priority: p.priority, description: p.description, created_at: p.created_at }))}
|
||||
pbis={pbis.map((p: (typeof pbis)[number]) => ({ id: p.id, code: p.code, title: p.title, priority: p.priority, description: p.description, created_at: p.created_at, status: pbiStatusToApi(p.status) }))}
|
||||
isDemo={isDemo}
|
||||
/>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import { CSS } from '@dnd-kit/utilities'
|
|||
import { toast } from 'sonner'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { PanelNavBar } from '@/components/shared/panel-nav-bar'
|
||||
import { useSelectionStore } from '@/stores/selection-store'
|
||||
import { usePlannerStore } from '@/stores/planner-store'
|
||||
|
|
@ -33,6 +33,8 @@ import { cn } from '@/lib/utils'
|
|||
import { PbiDialog, type PbiDialogState } from './pbi-dialog'
|
||||
import { BacklogCard } from './backlog-card'
|
||||
import { PRIORITY_COLORS } from '@/components/shared/priority-select'
|
||||
import { PBI_STATUS_LABELS, PBI_STATUS_COLORS } from '@/components/shared/pbi-status-select'
|
||||
import type { PbiStatusApi } from '@/lib/task-status'
|
||||
|
||||
const PRIORITY_LABELS: Record<number, string> = {
|
||||
1: 'Kritiek',
|
||||
|
|
@ -44,6 +46,62 @@ const PRIORITY_LABELS: Record<number, string> = {
|
|||
|
||||
type SortMode = 'priority' | 'code' | 'date'
|
||||
|
||||
const SORT_OPTIONS: Array<{ value: SortMode; label: string }> = [
|
||||
{ value: 'priority', label: 'Prioriteit' },
|
||||
{ value: 'code', label: 'Code' },
|
||||
{ value: 'date', label: 'Datum' },
|
||||
]
|
||||
|
||||
const PRIORITY_OPTIONS: Array<{ value: number | 'all'; label: string }> = [
|
||||
{ value: 'all', label: 'Alle' },
|
||||
{ value: 1, label: 'Kritiek' },
|
||||
{ value: 2, label: 'Hoog' },
|
||||
{ value: 3, label: 'Gemiddeld' },
|
||||
{ value: 4, label: 'Laag' },
|
||||
]
|
||||
|
||||
const STATUS_OPTIONS: Array<{ value: PbiStatusApi | 'all'; label: string }> = [
|
||||
{ value: 'all', label: 'Alle' },
|
||||
{ value: 'ready', label: 'Klaar' },
|
||||
{ value: 'blocked', label: 'Geblokkeerd' },
|
||||
{ value: 'done', label: 'Afgerond' },
|
||||
]
|
||||
|
||||
function FilterPills<T extends string | number>({
|
||||
label,
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
label: string
|
||||
options: Array<{ value: T; label: string }>
|
||||
value: T
|
||||
onChange: (v: T) => void
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-xs font-medium text-muted-foreground">{label}</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{options.map((opt) => (
|
||||
<button
|
||||
key={String(opt.value)}
|
||||
type="button"
|
||||
onClick={() => onChange(opt.value)}
|
||||
className={cn(
|
||||
'text-xs px-2.5 py-1 rounded-full border transition-colors',
|
||||
value === opt.value
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'bg-transparent border-border hover:bg-surface-container'
|
||||
)}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface Pbi {
|
||||
id: string
|
||||
code: string | null
|
||||
|
|
@ -51,6 +109,7 @@ interface Pbi {
|
|||
priority: number
|
||||
description?: string | null
|
||||
created_at: Date
|
||||
status: PbiStatusApi
|
||||
}
|
||||
|
||||
interface PbiListProps {
|
||||
|
|
@ -100,6 +159,11 @@ function SortablePbiRow({
|
|||
aria-selected={isSelected}
|
||||
onClick={onSelect}
|
||||
onKeyDown={(e: React.KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onSelect() } }}
|
||||
badge={
|
||||
<Badge className={cn('text-xs font-normal', PBI_STATUS_COLORS[pbi.status])}>
|
||||
{PBI_STATUS_LABELS[pbi.status]}
|
||||
</Badge>
|
||||
}
|
||||
actions={!isDemo ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
|
|
@ -126,7 +190,8 @@ function SortablePbiRow({
|
|||
export function PbiList({ productId, pbis, isDemo }: PbiListProps) {
|
||||
const { selectedPbiId, selectPbi } = useSelectionStore()
|
||||
const { pbiOrder, pbiPriority, initPbis, reorderPbis, rollbackPbis, updatePbiPriority } = usePlannerStore()
|
||||
const [filterPriority, setFilterPriority] = useState<number | null>(null)
|
||||
const [filterPriority, setFilterPriority] = useState<number | 'all'>('all')
|
||||
const [filterStatus, setFilterStatus] = useState<PbiStatusApi | 'all'>('all')
|
||||
const [sortMode, setSortMode] = useState<SortMode>(() => {
|
||||
const saved = typeof window !== 'undefined' ? localStorage.getItem('scrum4me:pbi_sort') : null
|
||||
return (saved === 'priority' || saved === 'code' || saved === 'date') ? saved : 'priority'
|
||||
|
|
@ -154,7 +219,16 @@ export function PbiList({ productId, pbis, isDemo }: PbiListProps) {
|
|||
.filter(Boolean)
|
||||
.map(p => ({ ...p, priority: pbiPriority[p.id] ?? p.priority }))
|
||||
|
||||
const base = filterPriority ? orderedPbis.filter(p => p.priority === filterPriority) : orderedPbis
|
||||
const base = orderedPbis.filter(p => {
|
||||
if (filterPriority !== 'all' && p.priority !== filterPriority) return false
|
||||
if (filterStatus !== 'all' && p.status !== filterStatus) return false
|
||||
return true
|
||||
})
|
||||
|
||||
const activeFilterCount =
|
||||
(filterPriority !== 'all' ? 1 : 0) +
|
||||
(filterStatus !== 'all' ? 1 : 0) +
|
||||
(sortMode !== 'priority' ? 1 : 0)
|
||||
|
||||
const filtered = [...base].sort((a, b) => {
|
||||
if (sortMode === 'code') {
|
||||
|
|
@ -228,10 +302,11 @@ export function PbiList({ productId, pbis, isDemo }: PbiListProps) {
|
|||
title="Product Backlog"
|
||||
actions={
|
||||
<>
|
||||
{filterPriority !== null && (
|
||||
{filterPriority !== 'all' && (
|
||||
<button
|
||||
onClick={() => setFilterPriority(null)}
|
||||
onClick={() => setFilterPriority('all')}
|
||||
className="flex items-center gap-1 text-xs text-primary hover:underline"
|
||||
aria-label="Wis prioriteitsfilter"
|
||||
>
|
||||
<Badge className={cn('text-xs', PRIORITY_COLORS[filterPriority])}>
|
||||
{PRIORITY_LABELS[filterPriority]}
|
||||
|
|
@ -239,34 +314,63 @@ export function PbiList({ productId, pbis, isDemo }: PbiListProps) {
|
|||
<span>×</span>
|
||||
</button>
|
||||
)}
|
||||
<Select
|
||||
value={sortMode}
|
||||
onValueChange={(v) => setSortMode(v as SortMode)}
|
||||
>
|
||||
<SelectTrigger className="h-7 w-28 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="priority">Prioriteit</SelectItem>
|
||||
<SelectItem value="code">Code</SelectItem>
|
||||
<SelectItem value="date">Datum</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={filterPriority?.toString() ?? 'all'}
|
||||
onValueChange={(v) => setFilterPriority(!v || v === 'all' ? null : parseInt(v))}
|
||||
>
|
||||
<SelectTrigger className="h-7 w-28 text-xs">
|
||||
<SelectValue placeholder="Filter" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Alle</SelectItem>
|
||||
<SelectItem value="1">Kritiek</SelectItem>
|
||||
<SelectItem value="2">Hoog</SelectItem>
|
||||
<SelectItem value="3">Gemiddeld</SelectItem>
|
||||
<SelectItem value="4">Laag</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{filterStatus !== 'all' && (
|
||||
<button
|
||||
onClick={() => setFilterStatus('all')}
|
||||
className="flex items-center gap-1 text-xs text-primary hover:underline"
|
||||
aria-label="Wis statusfilter"
|
||||
>
|
||||
<Badge className={cn('text-xs', PBI_STATUS_COLORS[filterStatus])}>
|
||||
{PBI_STATUS_LABELS[filterStatus]}
|
||||
</Badge>
|
||||
<span>×</span>
|
||||
</button>
|
||||
)}
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs">
|
||||
{`Filters${activeFilterCount > 0 ? ` (${activeFilterCount})` : ''}`}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<PopoverContent align="end" className="w-72 space-y-4">
|
||||
<FilterPills
|
||||
label="Sorteren op"
|
||||
options={SORT_OPTIONS}
|
||||
value={sortMode}
|
||||
onChange={setSortMode}
|
||||
/>
|
||||
<FilterPills
|
||||
label="Prioriteit"
|
||||
options={PRIORITY_OPTIONS}
|
||||
value={filterPriority}
|
||||
onChange={setFilterPriority}
|
||||
/>
|
||||
<FilterPills
|
||||
label="Status"
|
||||
options={STATUS_OPTIONS}
|
||||
value={filterStatus}
|
||||
onChange={setFilterStatus}
|
||||
/>
|
||||
<div className="flex justify-end pt-1 border-t border-border">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
disabled={activeFilterCount === 0}
|
||||
onClick={() => {
|
||||
setFilterPriority('all')
|
||||
setFilterStatus('all')
|
||||
setSortMode('priority')
|
||||
}}
|
||||
>
|
||||
Wis filters
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{!isDemo && (
|
||||
<Button
|
||||
size="sm"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue