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:
@@ -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
|
||||
|
||||
@@ -24,3 +24,17 @@ export interface PipelineItem {
|
||||
notes?: string
|
||||
assignedTo?: string
|
||||
}
|
||||
|
||||
export interface PendingPipelineItem {
|
||||
resultId: string
|
||||
resultType: PipelineItem['resultType']
|
||||
title: string
|
||||
location?: string
|
||||
matchScore: number
|
||||
propertyId?: string
|
||||
unitId?: string
|
||||
propertyAddress?: string
|
||||
areaLabel?: string
|
||||
rentLabel?: string
|
||||
availabilityLabel?: string
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { usePipelineItems, useAddToPipeline, useMoveStage, useUpdateNotes, useLoseItem, useLinkInquiry } from './usePipeline'
|
||||
export { useProperties, useProperty, usePropertyDetail } from './useProperties'
|
||||
export { useMatches, useMatchesByNeed, useMatchesByProperty, useApproveMatch, useMatchDetail } from './useMatches'
|
||||
export { useFutureSignals, useFutureSignalsByProperty, useVerifySignal } from './useFutureSignals'
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { pipelineService } from '../services/pipelineService'
|
||||
import { usePipelineStore } from '../stores/pipelineStore'
|
||||
import { useToastStore } from '../stores/toastStore'
|
||||
import type { PipelineStage, PendingPipelineItem } from '../domain/pipeline'
|
||||
|
||||
export const PIPELINE_QUERY_KEY = ['pipeline', 'items'] as const
|
||||
|
||||
export function usePipelineItems() {
|
||||
return useQuery({
|
||||
queryKey: PIPELINE_QUERY_KEY,
|
||||
queryFn: () => pipelineService.getAll(),
|
||||
select: (res) => res.data ?? [],
|
||||
staleTime: Infinity,
|
||||
})
|
||||
}
|
||||
|
||||
export function useAddToPipeline() {
|
||||
const queryClient = useQueryClient()
|
||||
const { closeSavedDialog } = usePipelineStore()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ pending, notes }: { pending: PendingPipelineItem; notes?: string }) =>
|
||||
pipelineService.confirmSaved(pending, notes),
|
||||
onSuccess: (res) => {
|
||||
queryClient.invalidateQueries({ queryKey: PIPELINE_QUERY_KEY })
|
||||
closeSavedDialog()
|
||||
if (res.data === 'duplicate') {
|
||||
useToastStore.getState().showToast('Bereits in der Pipeline vorhanden.', 'warning')
|
||||
} else {
|
||||
useToastStore.getState().showToast('Zur Pipeline (Gemerkt) hinzugefügt.')
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
useToastStore.getState().showToast('Fehler beim Hinzufügen zur Pipeline.', 'error')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useMoveStage() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, stage }: { id: string; stage: PipelineStage }) =>
|
||||
pipelineService.moveStage(id, stage),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: PIPELINE_QUERY_KEY })
|
||||
},
|
||||
onError: () => {
|
||||
useToastStore.getState().showToast('Fehler beim Verschieben.', 'error')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useUpdateNotes() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, notes }: { id: string; notes: string }) =>
|
||||
pipelineService.updateNotes(id, notes),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: PIPELINE_QUERY_KEY })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useLoseItem() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => pipelineService.loseItem(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: PIPELINE_QUERY_KEY })
|
||||
},
|
||||
onError: () => {
|
||||
useToastStore.getState().showToast('Fehler beim Ablehnen.', 'error')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useLinkInquiry() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, inquiryId }: { id: string; inquiryId: string }) =>
|
||||
pipelineService.linkInquiry(id, inquiryId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: PIPELINE_QUERY_KEY })
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from '@mui/material'
|
||||
import { Search, Send, Paperclip, ArrowLeft, Bot, Building2, Kanban } from 'lucide-react'
|
||||
import { mockInquiries } from '../../mock-data/inquiries'
|
||||
import { usePipelineStore } from '../../stores/pipelineStore'
|
||||
import { usePipelineItems, useMoveStage } from '../../hooks/usePipeline'
|
||||
import { useToastStore } from '../../stores/toastStore'
|
||||
import type { InquiryMessage } from '../../domain/inquiry'
|
||||
import { STAGE_ORDER, STAGE_LABELS, detectKiStage } from './anfragenKiDetection'
|
||||
@@ -34,7 +34,8 @@ const FILTER_TABS = [
|
||||
export default function Anfragen() {
|
||||
const navigate = useNavigate()
|
||||
const [searchParams] = useSearchParams()
|
||||
const { findByPropertyId, findByInquiryId, moveStage } = usePipelineStore()
|
||||
const { data: pipelineItems = [] } = usePipelineItems()
|
||||
const { mutate: moveStage } = useMoveStage()
|
||||
const showToast = useToastStore(s => s.showToast)
|
||||
|
||||
const preselectedId = searchParams.get('inquiry')
|
||||
@@ -65,8 +66,8 @@ export default function Anfragen() {
|
||||
|
||||
// Pipeline link for currently selected inquiry
|
||||
const linkedPipelineItem = selected?.propertyId
|
||||
? findByPropertyId(selected.propertyId)
|
||||
: (selected ? findByInquiryId(selected.id) : undefined)
|
||||
? pipelineItems.find(i => i.propertyId === selected.propertyId)
|
||||
: (selected ? pipelineItems.find(i => i.inquiryId === selected.id) : undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (threadRef.current) {
|
||||
@@ -112,13 +113,13 @@ export default function Anfragen() {
|
||||
const ki = detectKiStage(text)
|
||||
if (ki && selected) {
|
||||
const pipelineItem = selected.propertyId
|
||||
? findByPropertyId(selected.propertyId)
|
||||
: findByInquiryId(selectedId)
|
||||
? pipelineItems.find(i => i.propertyId === selected.propertyId)
|
||||
: pipelineItems.find(i => i.inquiryId === selectedId)
|
||||
if (pipelineItem) {
|
||||
const currentIdx = STAGE_ORDER.indexOf(pipelineItem.stage)
|
||||
const targetIdx = STAGE_ORDER.indexOf(ki.stage)
|
||||
if (targetIdx > currentIdx) {
|
||||
moveStage(pipelineItem.id, ki.stage)
|
||||
moveStage({ id: pipelineItem.id, stage: ki.stage })
|
||||
setKiAlert({ title: pipelineItem.title, stage: STAGE_LABELS[ki.stage] })
|
||||
}
|
||||
}
|
||||
@@ -180,7 +181,7 @@ export default function Anfragen() {
|
||||
key={inq.id}
|
||||
inq={inq}
|
||||
isSelected={inq.id === selectedId}
|
||||
hasPipeline={!!(inq.propertyId ? findByPropertyId(inq.propertyId) : findByInquiryId(inq.id))}
|
||||
hasPipeline={!!(inq.propertyId ? pipelineItems.find(i => i.propertyId === inq.propertyId) : pipelineItems.find(i => i.inquiryId === inq.id))}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '@dnd-kit/core'
|
||||
import type { DragEndEvent, DragStartEvent } from '@dnd-kit/core'
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { usePipelineStore } from '../../stores/pipelineStore'
|
||||
import { usePipelineItems, useMoveStage } from '../../hooks/usePipeline'
|
||||
import { AddToPipelineDialog } from '../../components/shortlist'
|
||||
import type { PipelineItem, PipelineStage } from '../../domain/pipeline'
|
||||
import { STAGES } from '../../components/pipeline/pipelineConstants'
|
||||
@@ -20,7 +20,8 @@ import { DetailPanel } from '../../components/pipeline/PipelineDetailPanel'
|
||||
|
||||
export default function Pipeline() {
|
||||
const navigate = useNavigate()
|
||||
const { items, moveStage } = usePipelineStore()
|
||||
const { data: items = [] } = usePipelineItems()
|
||||
const { mutate: moveStage } = useMoveStage()
|
||||
const [selectedItem, setSelectedItem] = useState<PipelineItem | null>(null)
|
||||
const [activeId, setActiveId] = useState<string | null>(null)
|
||||
const [overId, setOverId] = useState<string | null>(null)
|
||||
@@ -53,7 +54,7 @@ export default function Pipeline() {
|
||||
const targetStage = over.id as PipelineStage
|
||||
const item = items.find(i => i.id === active.id)
|
||||
if (item && item.stage !== targetStage) {
|
||||
moveStage(item.id, targetStage)
|
||||
moveStage({ id: item.id, stage: targetStage })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { PipelineItem, PipelineStage } from '../domain/pipeline'
|
||||
|
||||
export interface IPipelineProvider {
|
||||
getAll(): Promise<PipelineItem[]>
|
||||
add(item: PipelineItem): Promise<void>
|
||||
moveStage(id: string, stage: PipelineStage): Promise<void>
|
||||
updateNotes(id: string, notes: string): Promise<void>
|
||||
loseItem(id: string): Promise<void>
|
||||
linkInquiry(id: string, inquiryId: string): Promise<void>
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { IPipelineProvider } from './IPipelineProvider'
|
||||
import type { PipelineItem, PipelineStage } from '../domain/pipeline'
|
||||
import { mockPipelineItems } from '../mock-data/pipelineItems'
|
||||
|
||||
const STORAGE_KEY = 'property_match_pipeline_items'
|
||||
|
||||
function load(): PipelineItem[] {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
if (raw) return JSON.parse(raw) as PipelineItem[]
|
||||
} catch { /* ignore */ }
|
||||
return [...mockPipelineItems]
|
||||
}
|
||||
|
||||
function save(items: PipelineItem[]): void {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(items))
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
export const MockupPipelineProvider: IPipelineProvider = {
|
||||
async getAll() {
|
||||
return load()
|
||||
},
|
||||
|
||||
async add(item) {
|
||||
const items = load()
|
||||
items.push(item)
|
||||
save(items)
|
||||
},
|
||||
|
||||
async moveStage(id: string, stage: PipelineStage) {
|
||||
const items = load()
|
||||
const idx = items.findIndex(i => i.id === id)
|
||||
if (idx !== -1) {
|
||||
items[idx] = { ...items[idx], stage, updatedAt: new Date().toISOString() }
|
||||
save(items)
|
||||
}
|
||||
},
|
||||
|
||||
async updateNotes(id: string, notes: string) {
|
||||
const items = load()
|
||||
const idx = items.findIndex(i => i.id === id)
|
||||
if (idx !== -1) {
|
||||
items[idx] = { ...items[idx], notes }
|
||||
save(items)
|
||||
}
|
||||
},
|
||||
|
||||
async loseItem(id: string) {
|
||||
const items = load()
|
||||
const idx = items.findIndex(i => i.id === id)
|
||||
if (idx !== -1) {
|
||||
items[idx] = { ...items[idx], stage: 'CLOSED_LOST', updatedAt: new Date().toISOString() }
|
||||
save(items)
|
||||
}
|
||||
},
|
||||
|
||||
async linkInquiry(id: string, inquiryId: string) {
|
||||
const items = load()
|
||||
const idx = items.findIndex(i => i.id === id)
|
||||
if (idx !== -1) {
|
||||
items[idx] = { ...items[idx], inquiryId }
|
||||
save(items)
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { MockupPipelineProvider } from '../provider/MockupPipelineProvider'
|
||||
import type { PipelineItem, PipelineStage, PendingPipelineItem } from '../domain/pipeline'
|
||||
import type { ListResponse, ItemResponse } from './types'
|
||||
import { throwServiceError } from './errors'
|
||||
|
||||
const provider = MockupPipelineProvider
|
||||
|
||||
export const pipelineService = {
|
||||
async getAll(): Promise<ListResponse<PipelineItem>> {
|
||||
try {
|
||||
const data = await provider.getAll()
|
||||
return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } }
|
||||
} catch (err) {
|
||||
throwServiceError(err)
|
||||
}
|
||||
},
|
||||
|
||||
async confirmSaved(pending: PendingPipelineItem, notes?: string): Promise<ItemResponse<'added' | 'duplicate'>> {
|
||||
try {
|
||||
const items = await provider.getAll()
|
||||
const alreadyExists = items.some(
|
||||
i => i.id === pending.resultId || (pending.propertyId && i.propertyId === pending.propertyId)
|
||||
)
|
||||
if (alreadyExists) return { data: 'duplicate' }
|
||||
const newItem: PipelineItem = {
|
||||
id: pending.resultId,
|
||||
matchId: pending.resultId,
|
||||
title: pending.title,
|
||||
location: pending.location ?? '–',
|
||||
matchScore: pending.matchScore,
|
||||
resultType: pending.resultType,
|
||||
stage: 'SAVED',
|
||||
propertyId: pending.propertyId,
|
||||
unitId: pending.unitId,
|
||||
propertyAddress: pending.propertyAddress,
|
||||
areaLabel: pending.areaLabel,
|
||||
rentLabel: pending.rentLabel,
|
||||
availabilityLabel: pending.availabilityLabel,
|
||||
notes: notes || undefined,
|
||||
addedAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
await provider.add(newItem)
|
||||
return { data: 'added' }
|
||||
} catch (err) {
|
||||
throwServiceError(err)
|
||||
}
|
||||
},
|
||||
|
||||
async moveStage(id: string, stage: PipelineStage): Promise<ItemResponse<void>> {
|
||||
try {
|
||||
await provider.moveStage(id, stage)
|
||||
return { data: undefined }
|
||||
} catch (err) {
|
||||
throwServiceError(err)
|
||||
}
|
||||
},
|
||||
|
||||
async updateNotes(id: string, notes: string): Promise<ItemResponse<void>> {
|
||||
try {
|
||||
await provider.updateNotes(id, notes)
|
||||
return { data: undefined }
|
||||
} catch (err) {
|
||||
throwServiceError(err)
|
||||
}
|
||||
},
|
||||
|
||||
async loseItem(id: string): Promise<ItemResponse<void>> {
|
||||
try {
|
||||
await provider.loseItem(id)
|
||||
return { data: undefined }
|
||||
} catch (err) {
|
||||
throwServiceError(err)
|
||||
}
|
||||
},
|
||||
|
||||
async linkInquiry(id: string, inquiryId: string): Promise<ItemResponse<void>> {
|
||||
try {
|
||||
await provider.linkInquiry(id, inquiryId)
|
||||
return { data: undefined }
|
||||
} catch (err) {
|
||||
throwServiceError(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -1,97 +1,18 @@
|
||||
import { create } from 'zustand'
|
||||
import { mockPipelineItems } from '../mock-data/pipelineItems'
|
||||
import type { PipelineItem, PipelineStage } from '../domain/pipeline'
|
||||
import type { PendingPipelineItem } from '../domain/pipeline'
|
||||
|
||||
export interface PendingPipelineItem {
|
||||
resultId: string
|
||||
resultType: PipelineItem['resultType']
|
||||
title: string
|
||||
location?: string
|
||||
matchScore: number
|
||||
propertyId?: string
|
||||
unitId?: string
|
||||
propertyAddress?: string
|
||||
areaLabel?: string
|
||||
rentLabel?: string
|
||||
availabilityLabel?: string
|
||||
}
|
||||
export type { PendingPipelineItem }
|
||||
|
||||
interface PipelineStore {
|
||||
items: PipelineItem[]
|
||||
interface PipelineUIStore {
|
||||
dialogOpen: boolean
|
||||
pendingItem: PendingPipelineItem | null
|
||||
openSavedDialog: (item: PendingPipelineItem) => void
|
||||
closeSavedDialog: () => void
|
||||
confirmSaved: (notes?: string) => 'added' | 'duplicate'
|
||||
moveStage: (id: string, stage: PipelineStage) => void
|
||||
updateNotes: (id: string, notes: string) => void
|
||||
loseItem: (id: string) => void
|
||||
linkInquiry: (id: string, inquiryId: string) => void
|
||||
findByPropertyId: (propertyId: string) => PipelineItem | undefined
|
||||
findByInquiryId: (inquiryId: string) => PipelineItem | undefined
|
||||
}
|
||||
|
||||
export const usePipelineStore = create<PipelineStore>((set, get) => ({
|
||||
items: mockPipelineItems,
|
||||
export const usePipelineStore = create<PipelineUIStore>((set) => ({
|
||||
dialogOpen: false,
|
||||
pendingItem: null,
|
||||
|
||||
openSavedDialog: (item) => set({ dialogOpen: true, pendingItem: item }),
|
||||
closeSavedDialog: () => set({ dialogOpen: false, pendingItem: null }),
|
||||
|
||||
confirmSaved: (notes) => {
|
||||
const { pendingItem, items } = get()
|
||||
if (!pendingItem) return 'duplicate'
|
||||
const alreadyExists = items.some(
|
||||
i => i.id === pendingItem.resultId ||
|
||||
(pendingItem.propertyId && i.propertyId === pendingItem.propertyId)
|
||||
)
|
||||
if (!alreadyExists) {
|
||||
const newItem: PipelineItem = {
|
||||
id: pendingItem.resultId,
|
||||
matchId: pendingItem.resultId,
|
||||
title: pendingItem.title,
|
||||
location: pendingItem.location ?? '–',
|
||||
matchScore: pendingItem.matchScore,
|
||||
resultType: pendingItem.resultType,
|
||||
stage: 'SAVED',
|
||||
propertyId: pendingItem.propertyId,
|
||||
unitId: pendingItem.unitId,
|
||||
propertyAddress: pendingItem.propertyAddress,
|
||||
areaLabel: pendingItem.areaLabel,
|
||||
rentLabel: pendingItem.rentLabel,
|
||||
availabilityLabel: pendingItem.availabilityLabel,
|
||||
notes: notes || undefined,
|
||||
addedAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
set({ items: [...items, newItem], dialogOpen: false, pendingItem: null })
|
||||
return 'added'
|
||||
}
|
||||
set({ dialogOpen: false, pendingItem: null })
|
||||
return 'duplicate'
|
||||
},
|
||||
|
||||
moveStage: (id, stage) => set(state => ({
|
||||
items: state.items.map(i =>
|
||||
i.id === id ? { ...i, stage, updatedAt: new Date().toISOString() } : i
|
||||
),
|
||||
})),
|
||||
|
||||
updateNotes: (id, notes) => set(state => ({
|
||||
items: state.items.map(i => i.id === id ? { ...i, notes } : i),
|
||||
})),
|
||||
|
||||
loseItem: (id) => set(state => ({
|
||||
items: state.items.map(i =>
|
||||
i.id === id ? { ...i, stage: 'CLOSED_LOST' as PipelineStage, updatedAt: new Date().toISOString() } : i
|
||||
),
|
||||
})),
|
||||
|
||||
linkInquiry: (id, inquiryId) => set(state => ({
|
||||
items: state.items.map(i => i.id === id ? { ...i, inquiryId } : i),
|
||||
})),
|
||||
|
||||
findByPropertyId: (propertyId) => get().items.find(i => i.propertyId === propertyId),
|
||||
findByInquiryId: (inquiryId) => get().items.find(i => i.inquiryId === inquiryId),
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user