feat: wire meeting/support detail routes and fix code-first navigation
- Add routes for /meetings/:meetingId and /supports/:supportId in App.tsx - Fix MilestoneDetailPage to navigate to code-first detail URLs - Update table headers from '#' to 'Code' for supports/meetings lists - Fix TypeScript types for supports/meetings (use any[] instead of Task[]) - MeetingDetailPage: full detail view with attend, transition, edit, delete - SupportDetailPage: full detail view with take, transition, edit, delete
This commit is contained in:
298
src/pages/SupportDetailPage.tsx
Normal file
298
src/pages/SupportDetailPage.tsx
Normal file
@@ -0,0 +1,298 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import api from '@/services/api'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
|
||||
interface SupportItem {
|
||||
id: number
|
||||
code: string | null
|
||||
support_code: string | null
|
||||
title: string
|
||||
description: string | null
|
||||
status: string
|
||||
priority: string
|
||||
project_id: number
|
||||
project_code: string | null
|
||||
milestone_id: number
|
||||
milestone_code: string | null
|
||||
reporter_id: number
|
||||
assignee_id: number | null
|
||||
taken_by: string | null
|
||||
created_at: string
|
||||
updated_at: string | null
|
||||
}
|
||||
|
||||
const STATUS_OPTIONS = ['open', 'in_progress', 'resolved', 'closed']
|
||||
const PRIORITY_OPTIONS = ['low', 'medium', 'high', 'critical']
|
||||
|
||||
export default function SupportDetailPage() {
|
||||
const { supportId } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const { user } = useAuth()
|
||||
const [support, setSupport] = useState<SupportItem | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [message, setMessage] = useState('')
|
||||
const [editMode, setEditMode] = useState(false)
|
||||
const [editForm, setEditForm] = useState({ title: '', description: '', priority: '' })
|
||||
const [transitionStatus, setTransitionStatus] = useState('')
|
||||
|
||||
const fetchSupport = async () => {
|
||||
try {
|
||||
const { data } = await api.get<SupportItem>(`/supports/${supportId}`)
|
||||
setSupport(data)
|
||||
setEditForm({
|
||||
title: data.title,
|
||||
description: data.description || '',
|
||||
priority: data.priority,
|
||||
})
|
||||
} catch (err: any) {
|
||||
setMessage(err.response?.data?.detail || 'Failed to load support ticket')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchSupport()
|
||||
}, [supportId])
|
||||
|
||||
const handleTake = async () => {
|
||||
if (!support) return
|
||||
setSaving(true)
|
||||
setMessage('')
|
||||
try {
|
||||
const { data } = await api.post<SupportItem>(`/supports/${supportId}/take`)
|
||||
setSupport(data)
|
||||
setMessage('You have taken this support ticket')
|
||||
} catch (err: any) {
|
||||
setMessage(err.response?.data?.detail || 'Failed to take support ticket')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleTransition = async () => {
|
||||
if (!support || !transitionStatus) return
|
||||
setSaving(true)
|
||||
setMessage('')
|
||||
try {
|
||||
const { data } = await api.post<SupportItem>(`/supports/${supportId}/transition`, {
|
||||
status: transitionStatus,
|
||||
})
|
||||
setSupport(data)
|
||||
setTransitionStatus('')
|
||||
setMessage(`Status changed to ${transitionStatus}`)
|
||||
} catch (err: any) {
|
||||
setMessage(err.response?.data?.detail || 'Failed to transition support ticket')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!support) return
|
||||
setSaving(true)
|
||||
setMessage('')
|
||||
try {
|
||||
const payload: Record<string, any> = {}
|
||||
if (editForm.title.trim() !== support.title) payload.title = editForm.title.trim()
|
||||
if ((editForm.description || '') !== (support.description || '')) payload.description = editForm.description || null
|
||||
if (editForm.priority !== support.priority) payload.priority = editForm.priority
|
||||
|
||||
if (Object.keys(payload).length === 0) {
|
||||
setEditMode(false)
|
||||
return
|
||||
}
|
||||
|
||||
const { data } = await api.patch<SupportItem>(`/supports/${supportId}`, payload)
|
||||
setSupport(data)
|
||||
setEditMode(false)
|
||||
setMessage('Support ticket updated')
|
||||
} catch (err: any) {
|
||||
setMessage(err.response?.data?.detail || 'Failed to update support ticket')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!support) return
|
||||
if (!confirm(`Delete support ticket ${support.support_code || support.id}? This cannot be undone.`)) return
|
||||
setSaving(true)
|
||||
try {
|
||||
await api.delete(`/supports/${supportId}`)
|
||||
navigate(-1)
|
||||
} catch (err: any) {
|
||||
setMessage(err.response?.data?.detail || 'Failed to delete support ticket')
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) return <div className="loading">Loading support ticket...</div>
|
||||
if (!support) return <div className="loading">{message || 'Support ticket not found'}</div>
|
||||
|
||||
const canTake = user && (!support.assignee_id || support.assignee_id === user.id) && support.status !== 'closed' && support.status !== 'resolved'
|
||||
const isMine = user && support.assignee_id === user.id
|
||||
const availableTransitions = STATUS_OPTIONS.filter((s) => s !== support.status)
|
||||
|
||||
return (
|
||||
<div className="section">
|
||||
<button className="btn-back" onClick={() => navigate(-1)}>← Back</button>
|
||||
|
||||
<div className="task-header">
|
||||
<h2>🎫 {support.support_code || `#${support.id}`}</h2>
|
||||
<div className="task-meta">
|
||||
<span className={`badge status-${support.status}`}>{support.status}</span>
|
||||
<span className={`badge priority-${support.priority}`}>{support.priority}</span>
|
||||
{support.project_code && <span className="text-dim">Project: {support.project_code}</span>}
|
||||
{support.milestone_code && <span className="text-dim">Milestone: {support.milestone_code}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{message && (
|
||||
<div
|
||||
style={{
|
||||
padding: '10px 12px',
|
||||
marginBottom: '16px',
|
||||
borderRadius: '8px',
|
||||
background: message.toLowerCase().includes('fail') || message.toLowerCase().includes('error')
|
||||
? 'rgba(239,68,68,.12)'
|
||||
: 'rgba(16,185,129,.12)',
|
||||
border: `1px solid ${
|
||||
message.toLowerCase().includes('fail') || message.toLowerCase().includes('error')
|
||||
? 'rgba(239,68,68,.35)'
|
||||
: 'rgba(16,185,129,.35)'
|
||||
}`,
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 300px', gap: '20px', alignItems: 'start' }}>
|
||||
{/* Main content */}
|
||||
<div className="monitor-card">
|
||||
{editMode ? (
|
||||
<div className="task-create-form">
|
||||
<label>
|
||||
Title
|
||||
<input
|
||||
value={editForm.title}
|
||||
onChange={(e) => setEditForm({ ...editForm, title: e.target.value })}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Description
|
||||
<textarea
|
||||
value={editForm.description}
|
||||
onChange={(e) => setEditForm({ ...editForm, description: e.target.value })}
|
||||
rows={6}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Priority
|
||||
<select
|
||||
value={editForm.priority}
|
||||
onChange={(e) => setEditForm({ ...editForm, priority: e.target.value })}
|
||||
>
|
||||
{PRIORITY_OPTIONS.map((p) => (
|
||||
<option key={p} value={p}>{p}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<button className="btn-primary" disabled={saving} onClick={handleSave}>
|
||||
{saving ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
<button className="btn-back" onClick={() => setEditMode(false)}>Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<h3>{support.title}</h3>
|
||||
{support.description && (
|
||||
<p style={{ whiteSpace: 'pre-wrap', marginTop: 12 }}>{support.description}</p>
|
||||
)}
|
||||
{!support.description && <p className="text-dim">No description provided.</p>}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{/* Ownership */}
|
||||
<div className="monitor-card">
|
||||
<h4 style={{ marginBottom: 8 }}>Assigned To</h4>
|
||||
{support.taken_by ? (
|
||||
<div>
|
||||
<span className="badge">{support.taken_by}</span>
|
||||
{isMine && <span className="text-dim" style={{ marginLeft: 8 }}>(you)</span>}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-dim">Unassigned</span>
|
||||
)}
|
||||
{canTake && !isMine && (
|
||||
<button
|
||||
className="btn-primary"
|
||||
style={{ marginTop: 8, width: '100%' }}
|
||||
disabled={saving}
|
||||
onClick={handleTake}
|
||||
>
|
||||
Take
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Transition */}
|
||||
<div className="monitor-card">
|
||||
<h4 style={{ marginBottom: 8 }}>Transition</h4>
|
||||
<select
|
||||
value={transitionStatus}
|
||||
onChange={(e) => setTransitionStatus(e.target.value)}
|
||||
style={{ width: '100%', marginBottom: 8 }}
|
||||
>
|
||||
<option value="">Select status...</option>
|
||||
{availableTransitions.map((s) => (
|
||||
<option key={s} value={s}>{s}</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
className="btn-primary"
|
||||
style={{ width: '100%' }}
|
||||
disabled={saving || !transitionStatus}
|
||||
onClick={handleTransition}
|
||||
>
|
||||
Transition
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="monitor-card">
|
||||
<h4 style={{ marginBottom: 8 }}>Actions</h4>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{!editMode && (
|
||||
<button className="btn-secondary" onClick={() => setEditMode(true)}>
|
||||
Edit
|
||||
</button>
|
||||
)}
|
||||
<button className="btn-danger" disabled={saving} onClick={handleDelete}>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="monitor-card">
|
||||
<h4 style={{ marginBottom: 8 }}>Info</h4>
|
||||
<div className="text-dim" style={{ fontSize: '0.9em' }}>
|
||||
<div>Created: {new Date(support.created_at).toLocaleString()}</div>
|
||||
{support.updated_at && <div>Updated: {new Date(support.updated_at).toLocaleString()}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user