refactor: move PipelineItems from Zustand to Provider→Service→React Query

PipelineItems are domain data and must not live in Zustand. Moves the
full stack to the correct layer: MockupPipelineProvider (localStorage
persistence + seed fallback) → pipelineService → usePipeline hooks
(useQuery for reads, useMutation for writes with cache invalidation).

pipelineStore is now UI-only: dialogOpen, pendingItem, openSavedDialog,
closeSavedDialog. All consumers updated to use the new hooks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-24 12:32:57 +02:00
parent 0582031930
commit 723f553939
12 changed files with 303 additions and 115 deletions
@@ -1,6 +1,7 @@
import { Box, Button, Chip, IconButton, Tooltip, Typography } from '@mui/material'
import { X, AlertTriangle, BookmarkCheck, ExternalLink, Kanban } from 'lucide-react'
import { useNavigate } from 'react-router'
import { usePipelineItems } from '../../hooks/usePipeline'
import { usePipelineStore } from '../../stores/pipelineStore'
import type { UnifiedMatchResult } from '../../domain/unifiedResult'
@@ -20,7 +21,8 @@ interface Props {
export function CompareColumnHeader({ item, onRemove }: Props) {
const navigate = useNavigate()
const { items: pipelineItems, openSavedDialog } = usePipelineStore()
const { data: pipelineItems = [] } = usePipelineItems()
const { openSavedDialog } = usePipelineStore()
const meta = TYPE_META[item.resultType] ?? { label: item.resultType, color: '#64748b' }
const prop = item.resultType !== 'FUTURE_AVAILABILITY' ? (item as any).property : null
@@ -7,7 +7,7 @@ import {
AlertTriangle, ChevronRight, CheckCircle, ExternalLink, FileText,
MapPin, MessageSquare, Sparkles, StickyNote, X,
} from 'lucide-react'
import { usePipelineStore } from '../../stores/pipelineStore'
import { useMoveStage, useUpdateNotes, useLoseItem } from '../../hooks/usePipeline'
import type { PipelineItem } from '../../domain/pipeline'
import { STAGES, NEXT_STAGE, MOCK_DOCS } from './pipelineConstants'
import { scoreColor, detailPath, getKiInsight } from './pipelineUtils'
@@ -16,7 +16,9 @@ import { scoreColor, detailPath, getKiInsight } from './pipelineUtils'
export function DetailPanel({ item, onClose }: { item: PipelineItem; onClose: () => void }) {
const navigate = useNavigate()
const { moveStage, updateNotes, loseItem } = usePipelineStore()
const { mutate: moveStage } = useMoveStage()
const { mutate: updateNotes } = useUpdateNotes()
const { mutate: loseItem } = useLoseItem()
const path = detailPath(item)
const [notes, setNotes] = useState(item.notes ?? '')
const stageConfig = STAGES.find(s => s.key === item.stage)!
@@ -128,7 +130,7 @@ export function DetailPanel({ item, onClose }: { item: PipelineItem; onClose: ()
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
{nextStage && (
<Button size="small" variant="contained" endIcon={<ChevronRight size={14} />}
onClick={() => moveStage(item.id, nextStage.key)}
onClick={() => moveStage({ id: item.id, stage: nextStage.key })}
sx={{ bgcolor: stageConfig.color, '&:hover': { filter: 'brightness(0.9)' }, fontSize: '0.75rem', py: 0.5 }}
>
{nextStage.label}
@@ -157,7 +159,7 @@ export function DetailPanel({ item, onClose }: { item: PipelineItem; onClose: ()
placeholder="Notiz hinzufügen…"
value={notes}
onChange={e => setNotes(e.target.value)}
onBlur={() => updateNotes(item.id, notes)}
onBlur={() => updateNotes({ id: item.id, notes })}
sx={{ '& .MuiOutlinedInput-root': { fontSize: '0.8rem', borderRadius: 1.5 } }}
/>
</Box>
@@ -5,29 +5,22 @@ import {
} from '@mui/material'
import { Bookmark } from 'lucide-react'
import { usePipelineStore } from '../../stores/pipelineStore'
import { useToastStore } from '../../stores/toastStore'
import { useAddToPipeline } from '../../hooks/usePipeline'
export function AddToPipelineDialog() {
const { dialogOpen, pendingItem, closeSavedDialog, confirmSaved } = usePipelineStore()
const showToast = useToastStore((s) => s.showToast)
const { dialogOpen, pendingItem, closeSavedDialog } = usePipelineStore()
const { mutate: addToPipeline, isPending } = useAddToPipeline()
const [note, setNote] = useState('')
const [saving, setSaving] = useState(false)
function handleClose() {
closeSavedDialog()
setNote('')
}
async function handleConfirm() {
setSaving(true)
const result = confirmSaved(note.trim() || undefined)
setSaving(false)
function handleConfirm() {
if (!pendingItem) return
addToPipeline({ pending: pendingItem, notes: note.trim() || undefined })
setNote('')
if (result === 'duplicate') {
showToast('Bereits in der Pipeline vorhanden.', 'warning')
} else {
showToast('Zur Pipeline (Gemerkt) hinzugefügt.')
}
}
return (
@@ -59,12 +52,12 @@ export function AddToPipelineDialog() {
/>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<Button onClick={handleClose} disabled={saving}>Abbrechen</Button>
<Button onClick={handleClose} disabled={isPending}>Abbrechen</Button>
<Button
variant="contained"
onClick={handleConfirm}
disabled={saving}
endIcon={saving ? <CircularProgress size={14} color="inherit" /> : undefined}
disabled={isPending}
endIcon={isPending ? <CircularProgress size={14} color="inherit" /> : undefined}
sx={{ bgcolor: '#1e3a5f', '&:hover': { bgcolor: '#162d4a' } }}
>
Merken