fix(tickets): refresh status and resources in detail panel after update
Some checks failed
Mirror to GitHub / mirror (push) Successful in 3s
CI / backend (pull_request) Failing after 17m32s
CI / frontend (pull_request) Failing after 48s
CI / e2e (pull_request) Has been skipped

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:
2026-04-16 21:28:48 +00:00
parent 60851b400a
commit 04ff2ea301
6 changed files with 33 additions and 11 deletions

View File

@@ -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):

View File

@@ -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,
) )

View File

@@ -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}
/> />

View File

@@ -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>
) )
)} )}

View File

@@ -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>

View File

@@ -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 {