fix(tickets): refresh status and resources in detail panel after update
Status update was returning only new_status (string) and the parent list's onStatusUpdated only set status_name. The <select> was bound to status_id, which never changed — so it visually reverted to the old status even though the PATCH succeeded. - Backend: include new_status_id in the status-update response. - Panel: own currentStatusId/currentStatusName state so the select reflects the change immediately and survives stale parent snapshots. - Parent list: update status_id on both the row and selectedTicket so the list row stays in sync when the panel stays open. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@ class PSATicketStatusUpdateSchema(BaseModel):
|
|||||||
ticket_id: int
|
ticket_id: int
|
||||||
previous_status: str
|
previous_status: str
|
||||||
new_status: str
|
new_status: str
|
||||||
|
new_status_id: int
|
||||||
|
|
||||||
|
|
||||||
class TicketCreatePayloadSchema(BaseModel):
|
class TicketCreatePayloadSchema(BaseModel):
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ async def update_status(
|
|||||||
ticket_id=ticket_id,
|
ticket_id=ticket_id,
|
||||||
previous_status=previous_status,
|
previous_status=previous_status,
|
||||||
new_status=new_status,
|
new_status=new_status,
|
||||||
|
new_status_id=status_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import type { PSAResource } from '@/types/tickets'
|
|||||||
interface Props {
|
interface Props {
|
||||||
ticket: PSATicketSearchResult
|
ticket: PSATicketSearchResult
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onStatusUpdated?: (ticketId: number, newStatus: string) => void
|
onStatusUpdated?: (ticketId: number, newStatus: string, newStatusId: number) => void
|
||||||
onSelectRelated?: (ticketId: number) => void
|
onSelectRelated?: (ticketId: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +37,11 @@ export function TicketDetailPanel({ ticket, onClose, onStatusUpdated, onSelectRe
|
|||||||
const [contextLoading, setContextLoading] = useState(true)
|
const [contextLoading, setContextLoading] = useState(true)
|
||||||
const [resourcesLoading, setResourcesLoading] = useState(true)
|
const [resourcesLoading, setResourcesLoading] = useState(true)
|
||||||
|
|
||||||
|
// Local status state so the select reflects updates immediately, independent
|
||||||
|
// of the parent list's stale `selectedTicket` snapshot.
|
||||||
|
const [currentStatusId, setCurrentStatusId] = useState<number | null>(ticket.status_id ?? null)
|
||||||
|
const [currentStatusName, setCurrentStatusName] = useState<string | null>(ticket.status_name ?? null)
|
||||||
|
|
||||||
const ticketIdNum = Number(ticket.id)
|
const ticketIdNum = Number(ticket.id)
|
||||||
|
|
||||||
const loadResources = useCallback(() => {
|
const loadResources = useCallback(() => {
|
||||||
@@ -51,6 +56,8 @@ export function TicketDetailPanel({ ticket, onClose, onStatusUpdated, onSelectRe
|
|||||||
setContext(null)
|
setContext(null)
|
||||||
setResources([])
|
setResources([])
|
||||||
setStatuses([])
|
setStatuses([])
|
||||||
|
setCurrentStatusId(ticket.status_id ?? null)
|
||||||
|
setCurrentStatusName(ticket.status_name ?? null)
|
||||||
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
psaContextApi.getTicketContext(ticketIdNum),
|
psaContextApi.getTicketContext(ticketIdNum),
|
||||||
@@ -69,10 +76,13 @@ export function TicketDetailPanel({ ticket, onClose, onStatusUpdated, onSelectRe
|
|||||||
setContextLoading(false)
|
setContextLoading(false)
|
||||||
setResourcesLoading(false)
|
setResourcesLoading(false)
|
||||||
})
|
})
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [ticket.id, ticketIdNum])
|
}, [ticket.id, ticketIdNum])
|
||||||
|
|
||||||
function handleStatusUpdated(ticketId: number, newStatus: string) {
|
function handleStatusUpdated(ticketId: number, newStatus: string, newStatusId: number) {
|
||||||
onStatusUpdated?.(ticketId, newStatus)
|
setCurrentStatusId(newStatusId)
|
||||||
|
setCurrentStatusName(newStatus)
|
||||||
|
onStatusUpdated?.(ticketId, newStatus, newStatusId)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -96,6 +106,8 @@ export function TicketDetailPanel({ ticket, onClose, onStatusUpdated, onSelectRe
|
|||||||
{/* Header with status selector — optimistic, no loading gate */}
|
{/* Header with status selector — optimistic, no loading gate */}
|
||||||
<TicketDetailHeader
|
<TicketDetailHeader
|
||||||
ticket={ticket}
|
ticket={ticket}
|
||||||
|
currentStatusId={currentStatusId}
|
||||||
|
currentStatusName={currentStatusName}
|
||||||
statuses={statuses}
|
statuses={statuses}
|
||||||
onStatusUpdated={handleStatusUpdated}
|
onStatusUpdated={handleStatusUpdated}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import type { PSATicketStatusUpdate } from '@/types/tickets'
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
ticket: PSATicketSearchResult
|
ticket: PSATicketSearchResult
|
||||||
|
currentStatusId: number | null
|
||||||
|
currentStatusName: string | null
|
||||||
statuses: PSATicketStatusItem[]
|
statuses: PSATicketStatusItem[]
|
||||||
onStatusUpdated: (ticketId: number, newStatus: string) => void
|
onStatusUpdated: (ticketId: number, newStatus: string, newStatusId: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TicketDetailHeader({ ticket, statuses, onStatusUpdated }: Props) {
|
export function TicketDetailHeader({ ticket, currentStatusId, currentStatusName, statuses, onStatusUpdated }: Props) {
|
||||||
const [updating, setUpdating] = useState(false)
|
const [updating, setUpdating] = useState(false)
|
||||||
|
|
||||||
async function handleStatusChange(statusId: number) {
|
async function handleStatusChange(statusId: number) {
|
||||||
@@ -18,7 +20,7 @@ export function TicketDetailHeader({ ticket, statuses, onStatusUpdated }: Props)
|
|||||||
setUpdating(true)
|
setUpdating(true)
|
||||||
try {
|
try {
|
||||||
const result: PSATicketStatusUpdate = await ticketsApi.updateStatus(Number(ticket.id), statusId)
|
const result: PSATicketStatusUpdate = await ticketsApi.updateStatus(Number(ticket.id), statusId)
|
||||||
onStatusUpdated(result.ticket_id, result.new_status)
|
onStatusUpdated(result.ticket_id, result.new_status, result.new_status_id)
|
||||||
toast.success(`Status updated to ${result.new_status}`)
|
toast.success(`Status updated to ${result.new_status}`)
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Failed to update status')
|
toast.error('Failed to update status')
|
||||||
@@ -48,7 +50,7 @@ export function TicketDetailHeader({ ticket, statuses, onStatusUpdated }: Props)
|
|||||||
{statuses.length > 0 ? (
|
{statuses.length > 0 ? (
|
||||||
<select
|
<select
|
||||||
disabled={updating}
|
disabled={updating}
|
||||||
value={ticket.status_id ?? ''}
|
value={currentStatusId ?? ''}
|
||||||
onChange={e => handleStatusChange(Number(e.target.value))}
|
onChange={e => handleStatusChange(Number(e.target.value))}
|
||||||
className="bg-input border border-default rounded-[5px] px-2 py-1 text-xs text-primary focus:border-accent focus:outline-none"
|
className="bg-input border border-default rounded-[5px] px-2 py-1 text-xs text-primary focus:border-accent focus:outline-none"
|
||||||
>
|
>
|
||||||
@@ -57,9 +59,9 @@ export function TicketDetailHeader({ ticket, statuses, onStatusUpdated }: Props)
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
) : (
|
) : (
|
||||||
ticket.status_name && (
|
currentStatusName && (
|
||||||
<span className="px-2 py-0.5 bg-elevated rounded text-xs text-muted-foreground">
|
<span className="px-2 py-0.5 bg-elevated rounded text-xs text-muted-foreground">
|
||||||
{ticket.status_name}
|
{currentStatusName}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -231,10 +231,15 @@ export default function TicketsPage() {
|
|||||||
<TicketDetailPanel
|
<TicketDetailPanel
|
||||||
ticket={selectedTicket}
|
ticket={selectedTicket}
|
||||||
onClose={() => setSelectedTicket(null)}
|
onClose={() => setSelectedTicket(null)}
|
||||||
onStatusUpdated={(ticketId, newStatus) => {
|
onStatusUpdated={(ticketId, newStatus, newStatusId) => {
|
||||||
setTickets(prev => prev.map(t =>
|
setTickets(prev => prev.map(t =>
|
||||||
t.id === String(ticketId) ? { ...t, status_name: newStatus } : t
|
t.id === String(ticketId) ? { ...t, status_name: newStatus, status_id: newStatusId } : t
|
||||||
))
|
))
|
||||||
|
setSelectedTicket(prev =>
|
||||||
|
prev && prev.id === String(ticketId)
|
||||||
|
? { ...prev, status_name: newStatus, status_id: newStatusId }
|
||||||
|
: prev
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export interface PSATicketStatusUpdate {
|
|||||||
ticket_id: number
|
ticket_id: number
|
||||||
previous_status: string
|
previous_status: string
|
||||||
new_status: string
|
new_status: string
|
||||||
|
new_status_id: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TicketListResponse {
|
export interface TicketListResponse {
|
||||||
|
|||||||
Reference in New Issue
Block a user