feat: F025 button & action logic audit — global toast, confirm dialog, wired mutations
- Add ToastProvider (global MUI Snackbar stack) + toastStore (Zustand) - Add ConfirmDialog reusable component for destructive actions - Wire toast feedback to all async mutations: review status, AI output status, match approval, shortlist add/remove/title/finalize, signal review actions - Fix dead UserMenu buttons (Profil, Einstellungen) — show info toast - Migrate local Snackbar instances in AddToShortlistDialog and FutureSignalDetailPanel to global toast store Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Box, Button, Chip, CircularProgress, Divider, IconButton, LinearProgress, Paper, Snackbar, Typography } from '@mui/material'
|
||||
import { Box, Button, Chip, CircularProgress, Divider, IconButton, LinearProgress, Paper, Typography } from '@mui/material'
|
||||
import { X } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { SignalTypeBadge } from './SignalTypeBadge'
|
||||
@@ -7,6 +7,7 @@ import { SignalReviewStatusBadge } from './SignalReviewStatusBadge'
|
||||
import { FutureSignalDisclaimer } from './FutureSignalDisclaimer'
|
||||
import { useUpdateSignalReviewStatus } from '../../hooks/useFutureSignals'
|
||||
import { useShortlistStore } from '../../stores/shortlistStore'
|
||||
import { useToastStore } from '../../stores/toastStore'
|
||||
import { reviewService } from '../../services/reviewService'
|
||||
import { ReviewStatus } from '../../domain/enums'
|
||||
import type { FutureSignal } from '../../domain/futureSignal'
|
||||
@@ -51,22 +52,37 @@ interface Props {
|
||||
export function FutureSignalDetailPanel({ signal, onClose }: Props) {
|
||||
const updateStatus = useUpdateSignalReviewStatus()
|
||||
const { openAddDialog } = useShortlistStore()
|
||||
const [snackbar, setSnackbar] = useState<string | null>(null)
|
||||
const showToast = useToastStore((s) => s.showToast)
|
||||
const [reviewTaskSent, setReviewTaskSent] = useState(false)
|
||||
|
||||
const reviewStatus = signal.reviewStatus ?? ReviewStatus.UNREVIEWED
|
||||
const isRejected = reviewStatus === ReviewStatus.REJECTED
|
||||
const isApproved = reviewStatus === ReviewStatus.APPROVED
|
||||
|
||||
const STATUS_TOAST: Record<string, string> = {
|
||||
IN_REVIEW: 'Signal zur Prüfung markiert.',
|
||||
APPROVED: 'Signal genehmigt.',
|
||||
REJECTED: 'Signal abgelehnt.',
|
||||
FLAGGED: 'Signal markiert.',
|
||||
}
|
||||
|
||||
async function handleStatus(status: typeof ReviewStatus[keyof typeof ReviewStatus]) {
|
||||
await updateStatus.mutateAsync({ id: signal.id, status })
|
||||
setSnackbar(`Status aktualisiert: ${status}`)
|
||||
try {
|
||||
await updateStatus.mutateAsync({ id: signal.id, status })
|
||||
showToast(STATUS_TOAST[status] ?? 'Status aktualisiert.')
|
||||
} catch {
|
||||
showToast('Statusänderung fehlgeschlagen.', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSendReview() {
|
||||
await reviewService.createReviewTask(signal.id)
|
||||
setReviewTaskSent(true)
|
||||
setSnackbar('Prüfungsaufgabe erstellt')
|
||||
try {
|
||||
await reviewService.createReviewTask(signal.id)
|
||||
setReviewTaskSent(true)
|
||||
showToast('Prüfungsaufgabe erstellt.')
|
||||
} catch {
|
||||
showToast('Prüfungsaufgabe konnte nicht erstellt werden.', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
function handleShortlist() {
|
||||
@@ -242,13 +258,6 @@ export function FutureSignalDetailPanel({ signal, onClose }: Props) {
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Snackbar
|
||||
open={!!snackbar}
|
||||
autoHideDuration={2500}
|
||||
onClose={() => setSnackbar(null)}
|
||||
message={snackbar}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ import { RightContextPanel } from './RightContextPanel'
|
||||
import { CompareTray } from './CompareTray'
|
||||
import { GlobalAIAssistantDrawer, GlobalAIAssistantButton } from '../assistant'
|
||||
import { useAssistantStore } from '../../stores/assistantStore'
|
||||
import { ToastProvider } from '../ui'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
@@ -546,6 +547,7 @@ export function AppShell() {
|
||||
<CompareTray />
|
||||
<GlobalAIAssistantDrawer />
|
||||
<GlobalAIAssistantButton />
|
||||
<ToastProvider />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router'
|
||||
import { Avatar, Box, Divider, IconButton, ListItemIcon, Menu, MenuItem, Typography } from '@mui/material'
|
||||
import { LogOut, Settings, User } from 'lucide-react'
|
||||
import { useSessionStore } from '../../stores/sessionStore'
|
||||
import { useToastStore } from '../../stores/toastStore'
|
||||
import { DemoRoleSwitcher } from '../auth/DemoRoleSwitcher'
|
||||
|
||||
const ROLE_LABELS: Record<string, string> = {
|
||||
@@ -27,6 +28,7 @@ export function UserMenu() {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
|
||||
const { currentUser, logout } = useSessionStore()
|
||||
const navigate = useNavigate()
|
||||
const showToast = useToastStore((s) => s.showToast)
|
||||
|
||||
function handleOpen(e: React.MouseEvent<HTMLElement>) {
|
||||
setAnchorEl(e.currentTarget)
|
||||
@@ -75,11 +77,11 @@ export function UserMenu() {
|
||||
|
||||
<Divider />
|
||||
|
||||
<MenuItem onClick={handleClose}>
|
||||
<MenuItem onClick={() => { handleClose(); showToast('Profilseite ist in Kürze verfügbar.', 'info') }}>
|
||||
<ListItemIcon><User size={16} /></ListItemIcon>
|
||||
Profil
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleClose}>
|
||||
<MenuItem onClick={() => { handleClose(); showToast('Einstellungen sind in Kürze verfügbar.', 'info') }}>
|
||||
<ListItemIcon><Settings size={16} /></ListItemIcon>
|
||||
Einstellungen
|
||||
</MenuItem>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useMatchesByProperty, useApproveMatch } from '../../hooks/useMatches'
|
||||
import { usePropertyById } from '../../hooks/useProperties'
|
||||
import { useMatchCenterStore } from '../../stores/matchCenterStore'
|
||||
import { useCompareStore } from '../../stores/compareStore'
|
||||
import { useToastStore } from '../../stores/toastStore'
|
||||
import { MatchCardExpanded } from '../match-card/MatchCardExpanded'
|
||||
import { buildMatchCardViewModel } from '../../features/matching/matchCardAdapter'
|
||||
import { reviewService } from '../../services/reviewService'
|
||||
@@ -17,6 +18,7 @@ export function MatchBriefingPanel() {
|
||||
const { selectedPropertyId, selectedNeedId } = useMatchCenterStore()
|
||||
const { addToCompare } = useCompareStore()
|
||||
const approveMatch = useApproveMatch()
|
||||
const showToast = useToastStore((s) => s.showToast)
|
||||
|
||||
const { data: matches = [], isLoading: matchesLoading } = useMatchesByProperty(selectedPropertyId ?? '')
|
||||
const { data: property = null, isLoading: propLoading } = usePropertyById(selectedPropertyId)
|
||||
@@ -52,7 +54,10 @@ export function MatchBriefingPanel() {
|
||||
label: 'Genehmigen',
|
||||
actionType: 'APPROVE',
|
||||
variant: 'primary',
|
||||
onClick: () => approveMatch.mutate(match.id),
|
||||
onClick: () => approveMatch.mutate(match.id, {
|
||||
onSuccess: () => showToast('Match genehmigt.'),
|
||||
onError: () => showToast('Genehmigung fehlgeschlagen.', 'error'),
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'review',
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
FormControlLabel,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Snackbar,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
@@ -18,6 +17,7 @@ import { Plus } from 'lucide-react'
|
||||
import { ShortlistStatusBadge } from './ShortlistStatusBadge'
|
||||
import { useShortlists, useCreateShortlist, useAddToShortlist } from '../../hooks/useShortlists'
|
||||
import { useShortlistStore } from '../../stores/shortlistStore'
|
||||
import { useToastStore } from '../../stores/toastStore'
|
||||
import { ShortlistStatus } from '../../domain/enums'
|
||||
|
||||
export function AddToShortlistDialog() {
|
||||
@@ -26,11 +26,11 @@ export function AddToShortlistDialog() {
|
||||
const createShortlist = useCreateShortlist()
|
||||
const addToShortlist = useAddToShortlist()
|
||||
|
||||
const showToast = useToastStore((s) => s.showToast)
|
||||
const [selectedId, setSelectedId] = useState<string>('')
|
||||
const [creatingNew, setCreatingNew] = useState(false)
|
||||
const [newTitle, setNewTitle] = useState('')
|
||||
const [note, setNote] = useState('')
|
||||
const [snackbar, setSnackbar] = useState<string | null>(null)
|
||||
|
||||
function handleClose() {
|
||||
closeAddDialog()
|
||||
@@ -65,12 +65,12 @@ export function AddToShortlistDialog() {
|
||||
|
||||
await addToShortlist.mutateAsync({ shortlistId: targetId, item: itemWithNote })
|
||||
|
||||
if (alreadyExists) {
|
||||
setSnackbar('Bereits in dieser Shortlist vorhanden')
|
||||
} else {
|
||||
setSnackbar('Zur Shortlist hinzugefügt')
|
||||
}
|
||||
handleClose()
|
||||
if (alreadyExists) {
|
||||
showToast('Bereits in dieser Shortlist vorhanden.', 'warning')
|
||||
} else {
|
||||
showToast('Zur Shortlist hinzugefügt.')
|
||||
}
|
||||
}
|
||||
|
||||
const isBusy = createShortlist.isPending || addToShortlist.isPending
|
||||
@@ -161,14 +161,6 @@ export function AddToShortlistDialog() {
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<Snackbar
|
||||
open={!!snackbar}
|
||||
autoHideDuration={3000}
|
||||
onClose={() => setSnackbar(null)}
|
||||
message={snackbar}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ShortlistItemCard } from './ShortlistItemCard'
|
||||
import { ShortlistEmptyState } from './ShortlistEmptyState'
|
||||
import { useUpdateShortlist } from '../../hooks/useShortlists'
|
||||
import { useCompareStore } from '../../stores/compareStore'
|
||||
import { useToastStore } from '../../stores/toastStore'
|
||||
import { ShortlistStatus } from '../../domain/enums'
|
||||
import type { Shortlist } from '../../domain/shortlist'
|
||||
|
||||
@@ -18,6 +19,7 @@ export function ShortlistDetail({ shortlist }: Props) {
|
||||
const navigate = useNavigate()
|
||||
const updateShortlist = useUpdateShortlist()
|
||||
const { addToCompare, compareItems } = useCompareStore()
|
||||
const showToast = useToastStore((s) => s.showToast)
|
||||
|
||||
const [editingTitle, setEditingTitle] = useState(false)
|
||||
const [titleDraft, setTitleDraft] = useState(shortlist.title)
|
||||
@@ -28,17 +30,35 @@ export function ShortlistDetail({ shortlist }: Props) {
|
||||
|
||||
function handleTitleSave() {
|
||||
if (titleDraft.trim() && titleDraft.trim() !== shortlist.title) {
|
||||
updateShortlist.mutate({ id: shortlist.id, data: { title: titleDraft.trim() } })
|
||||
updateShortlist.mutate(
|
||||
{ id: shortlist.id, data: { title: titleDraft.trim() } },
|
||||
{
|
||||
onSuccess: () => showToast('Titel gespeichert.'),
|
||||
onError: () => showToast('Titel konnte nicht gespeichert werden.', 'error'),
|
||||
}
|
||||
)
|
||||
}
|
||||
setEditingTitle(false)
|
||||
}
|
||||
|
||||
function handleMarkReviewReady() {
|
||||
updateShortlist.mutate({ id: shortlist.id, data: { status: ShortlistStatus.REVIEW_READY } })
|
||||
updateShortlist.mutate(
|
||||
{ id: shortlist.id, data: { status: ShortlistStatus.REVIEW_READY } },
|
||||
{
|
||||
onSuccess: () => showToast('Shortlist als prüfbereit markiert.'),
|
||||
onError: () => showToast('Statusänderung fehlgeschlagen.', 'error'),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function handleFinalize() {
|
||||
updateShortlist.mutate({ id: shortlist.id, data: { status: ShortlistStatus.FINALIZED } })
|
||||
updateShortlist.mutate(
|
||||
{ id: shortlist.id, data: { status: ShortlistStatus.FINALIZED } },
|
||||
{
|
||||
onSuccess: () => showToast('Shortlist finalisiert.'),
|
||||
onError: () => showToast('Finalisierung fehlgeschlagen.', 'error'),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function handleOpenCompare() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Box, Chip, IconButton, Typography } from '@mui/material'
|
||||
import { X } from 'lucide-react'
|
||||
import { useRemoveFromShortlist } from '../../hooks/useShortlists'
|
||||
import { useToastStore } from '../../stores/toastStore'
|
||||
import type { ShortlistItem } from '../../domain/shortlist'
|
||||
|
||||
const RESULT_TYPE_LABEL: Record<string, string> = {
|
||||
@@ -19,6 +20,7 @@ interface Props {
|
||||
|
||||
export function ShortlistItemCard({ item, shortlistId, isFinalized }: Props) {
|
||||
const removeItem = useRemoveFromShortlist()
|
||||
const showToast = useToastStore((s) => s.showToast)
|
||||
|
||||
const addedDate = new Date(item.addedAt).toLocaleDateString('de-CH', { day: '2-digit', month: '2-digit', year: 'numeric' })
|
||||
|
||||
@@ -58,7 +60,13 @@ export function ShortlistItemCard({ item, shortlistId, isFinalized }: Props) {
|
||||
<IconButton
|
||||
size="small"
|
||||
disabled={removeItem.isPending}
|
||||
onClick={() => removeItem.mutate({ shortlistId, resultId: item.resultId })}
|
||||
onClick={() => removeItem.mutate(
|
||||
{ shortlistId, resultId: item.resultId },
|
||||
{
|
||||
onSuccess: () => showToast('Objekt aus Shortlist entfernt.'),
|
||||
onError: () => showToast('Entfernen fehlgeschlagen.', 'error'),
|
||||
}
|
||||
)}
|
||||
sx={{ color: '#94a3b8', '&:hover': { color: '#c0392b' }, flexShrink: 0 }}
|
||||
>
|
||||
<X size={14} />
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Typography } from '@mui/material'
|
||||
import { AlertTriangle } from 'lucide-react'
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
title: string
|
||||
message: string
|
||||
confirmLabel?: string
|
||||
cancelLabel?: string
|
||||
destructive?: boolean
|
||||
onConfirm: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
export function ConfirmDialog({
|
||||
open,
|
||||
title,
|
||||
message,
|
||||
confirmLabel = 'Bestätigen',
|
||||
cancelLabel = 'Abbrechen',
|
||||
destructive = false,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: Props) {
|
||||
return (
|
||||
<Dialog open={open} onClose={onCancel} maxWidth="xs" fullWidth>
|
||||
<DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{destructive && <AlertTriangle size={18} color="#c0392b" />}
|
||||
{title}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{message}
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||
<Button variant="outlined" size="small" onClick={onCancel}>
|
||||
{cancelLabel}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
color={destructive ? 'error' : 'primary'}
|
||||
onClick={onConfirm}
|
||||
autoFocus
|
||||
>
|
||||
{confirmLabel}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Alert, Snackbar, Stack } from '@mui/material'
|
||||
import { useToastStore } from '../../stores/toastStore'
|
||||
|
||||
export function ToastProvider() {
|
||||
const { toasts, dismissToast } = useToastStore()
|
||||
|
||||
return (
|
||||
<Stack
|
||||
spacing={1}
|
||||
sx={{ position: 'fixed', bottom: 24, left: '50%', transform: 'translateX(-50%)', zIndex: 9999, alignItems: 'center' }}
|
||||
>
|
||||
{toasts.map((toast) => (
|
||||
<Snackbar
|
||||
key={toast.id}
|
||||
open
|
||||
autoHideDuration={toast.duration ?? 4000}
|
||||
onClose={() => dismissToast(toast.id)}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
sx={{ position: 'relative', transform: 'none', left: 'auto', bottom: 'auto' }}
|
||||
>
|
||||
<Alert
|
||||
onClose={() => dismissToast(toast.id)}
|
||||
severity={toast.severity}
|
||||
variant="filled"
|
||||
sx={{ minWidth: 320, boxShadow: 3 }}
|
||||
>
|
||||
{toast.message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
))}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -8,3 +8,5 @@ export { CardSkeleton } from './CardSkeleton'
|
||||
export { PanelLoadingState } from './PanelLoadingState'
|
||||
export { UnauthorizedState } from './UnauthorizedState'
|
||||
export { RestrictedState } from './RestrictedState'
|
||||
export { ToastProvider } from './ToastProvider'
|
||||
export { ConfirmDialog } from './ConfirmDialog'
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
AIMonitoringEmptyState,
|
||||
} from '../../components/ai-monitoring'
|
||||
import { useAIOutputs, useUpdateAIOutputReviewStatus } from '../../hooks/useAIMonitoring'
|
||||
import { useToastStore } from '../../stores/toastStore'
|
||||
import type { AIOutput, AIOutputType } from '../../domain/aiOutput'
|
||||
import type { ReviewStatus } from '../../domain/enums'
|
||||
|
||||
@@ -58,6 +59,7 @@ export default function AIMonitoring() {
|
||||
const [filters, setFilters] = useState<Filters>({ type: '', reviewStatus: '', hasError: null })
|
||||
const [selectedOutput, setSelectedOutput] = useState<AIOutput | null>(null)
|
||||
|
||||
const showToast = useToastStore((s) => s.showToast)
|
||||
const { data: allOutputs = [], isLoading } = useAIOutputs()
|
||||
const updateStatus = useUpdateAIOutputReviewStatus()
|
||||
|
||||
@@ -66,11 +68,25 @@ export default function AIMonitoring() {
|
||||
const failedCount = allOutputs.filter(o => !!o.error).length
|
||||
const pendingCount = allOutputs.filter(o => o.reviewStatus === 'UNREVIEWED' || o.reviewStatus === 'FLAGGED').length
|
||||
|
||||
const STATUS_TOAST: Record<ReviewStatus, string> = {
|
||||
UNREVIEWED: 'Status zurückgesetzt.',
|
||||
IN_REVIEW: 'Output zur Prüfung markiert.',
|
||||
APPROVED: 'Output genehmigt.',
|
||||
REJECTED: 'Output abgelehnt.',
|
||||
FLAGGED: 'Output markiert.',
|
||||
}
|
||||
|
||||
const handleUpdateStatus = (status: ReviewStatus) => {
|
||||
if (!selectedOutput) return
|
||||
updateStatus.mutate(
|
||||
{ id: selectedOutput.id, status },
|
||||
{ onSuccess: (res) => setSelectedOutput(res.data) }
|
||||
{
|
||||
onSuccess: (res) => {
|
||||
setSelectedOutput(res.data)
|
||||
showToast(STATUS_TOAST[status] ?? 'Status aktualisiert.')
|
||||
},
|
||||
onError: () => showToast('Statusänderung fehlgeschlagen.', 'error'),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '../../components/review'
|
||||
import { useReviewQueue, useUpdateReviewStatus, useAddReviewNote } from '../../hooks/useReviewQueue'
|
||||
import { useSessionStore } from '../../stores/sessionStore'
|
||||
import { useToastStore } from '../../stores/toastStore'
|
||||
import type { ReviewTask, ReviewTaskStatus } from '../../domain/review'
|
||||
import type { ReviewFilters } from '../../provider/IReviewProvider'
|
||||
|
||||
@@ -24,6 +25,7 @@ function filterTasks(tasks: ReviewTask[], filters: ReviewFilters): ReviewTask[]
|
||||
|
||||
export default function ReviewQueue() {
|
||||
const { currentUser } = useSessionStore()
|
||||
const showToast = useToastStore((s) => s.showToast)
|
||||
const userRole = currentUser?.role ?? 'REVIEWER'
|
||||
|
||||
const [filters, setFilters] = useState<ReviewFilters>({})
|
||||
@@ -40,6 +42,15 @@ export default function ReviewQueue() {
|
||||
const escalatedCount = allTasks.filter(t => t.status === 'ESCALATED').length
|
||||
const criticalCount = allTasks.filter(t => t.priority === 'CRITICAL' && (t.status === 'PENDING' || t.status === 'IN_REVIEW')).length
|
||||
|
||||
const STATUS_TOAST: Record<ReviewTaskStatus, string> = {
|
||||
PENDING: 'Status auf "Ausstehend" gesetzt.',
|
||||
IN_REVIEW: 'Aufgabe zur Prüfung übernommen.',
|
||||
APPROVED: 'Aufgabe genehmigt.',
|
||||
REJECTED: 'Aufgabe abgelehnt.',
|
||||
ESCALATED: 'Aufgabe eskaliert.',
|
||||
NEEDS_MORE_DATA: 'Weitere Daten angefordert.',
|
||||
}
|
||||
|
||||
const handleAction = (status: ReviewTaskStatus) => {
|
||||
if (!selectedTask) return
|
||||
updateStatus.mutate(
|
||||
@@ -47,7 +58,9 @@ export default function ReviewQueue() {
|
||||
{
|
||||
onSuccess: (res) => {
|
||||
setSelectedTask(res.data)
|
||||
showToast(STATUS_TOAST[status] ?? 'Status aktualisiert.')
|
||||
},
|
||||
onError: () => showToast('Statusänderung fehlgeschlagen.', 'error'),
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -59,7 +72,9 @@ export default function ReviewQueue() {
|
||||
{
|
||||
onSuccess: (res) => {
|
||||
setSelectedTask(res.data)
|
||||
showToast('Notiz hinzugefügt.')
|
||||
},
|
||||
onError: () => showToast('Notiz konnte nicht gespeichert werden.', 'error'),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useMatches, useApproveMatch } from '../../hooks/useMatches'
|
||||
import { useProperties } from '../../hooks/useProperties'
|
||||
import { useNeeds } from '../../hooks/useNeeds'
|
||||
import { useMatchCenterStore } from '../../stores/matchCenterStore'
|
||||
import { useToastStore } from '../../stores/toastStore'
|
||||
import { MatchListCard, MatchBriefingPanel, MatchCenterSkeleton } from '../../components/match-center'
|
||||
import type { Match } from '../../domain/match'
|
||||
|
||||
@@ -36,6 +37,7 @@ export default function MatchCenter() {
|
||||
const { data: needs = [] } = useNeeds()
|
||||
const { setSelectedProperty, setSelectedNeed } = useMatchCenterStore()
|
||||
const approveMatch = useApproveMatch()
|
||||
const showToast = useToastStore((s) => s.showToast)
|
||||
|
||||
const [selectedMatchId, setSelectedMatchId] = useState<string | null>(null)
|
||||
const [filterStrength, setFilterStrength] = useState('')
|
||||
@@ -152,7 +154,10 @@ export default function MatchCenter() {
|
||||
property={propMap.get(match.propertyId)}
|
||||
need={needMap.get(match.needId)}
|
||||
onSelect={() => handleSelectMatch(match)}
|
||||
onApprove={() => approveMatch.mutate(match.id)}
|
||||
onApprove={() => approveMatch.mutate(match.id, {
|
||||
onSuccess: () => showToast('Match genehmigt.'),
|
||||
onError: () => showToast('Genehmigung fehlgeschlagen.', 'error'),
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { create } from 'zustand'
|
||||
|
||||
export type ToastSeverity = 'success' | 'error' | 'warning' | 'info'
|
||||
|
||||
export interface ToastMessage {
|
||||
id: string
|
||||
message: string
|
||||
severity: ToastSeverity
|
||||
duration?: number
|
||||
}
|
||||
|
||||
interface ToastState {
|
||||
toasts: ToastMessage[]
|
||||
showToast: (message: string, severity?: ToastSeverity, duration?: number) => void
|
||||
dismissToast: (id: string) => void
|
||||
}
|
||||
|
||||
export const useToastStore = create<ToastState>((set) => ({
|
||||
toasts: [],
|
||||
showToast: (message, severity = 'success', duration = 4000) => {
|
||||
const id = crypto.randomUUID()
|
||||
set((s) => ({ toasts: [...s.toasts, { id, message, severity, duration }] }))
|
||||
},
|
||||
dismissToast: (id) => {
|
||||
set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) }))
|
||||
},
|
||||
}))
|
||||
Reference in New Issue
Block a user