import { useEffect, useState } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { Copy, Check, Eye, Save } from 'lucide-react' import { sessionsApi, stepsApi } from '@/api' import { ExportPreviewModal } from '@/components/session/ExportPreviewModal' import { SaveSessionAsTreeModal } from '@/components/session/SaveSessionAsTreeModal' import { StepRatingModal } from '@/components/session/StepRatingModal' import { useUserPreferencesStore } from '@/store/userPreferencesStore' import type { Session, SessionExport, SaveAsTreeRequest, Step } from '@/types' import { hasRatedSession, markSessionRated } from '@/lib/sessionRatings' import { cn } from '@/lib/utils' import { toast } from '@/lib/toast' export function SessionDetailPage() { const { id } = useParams<{ id: string }>() const navigate = useNavigate() const { defaultExportFormat } = useUserPreferencesStore() const [session, setSession] = useState(null) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) const [isExporting, setIsExporting] = useState(false) const [exportFormat, setExportFormat] = useState<'markdown' | 'text' | 'html' | 'psa'>(defaultExportFormat) const [exportContent, setExportContent] = useState(null) const [showPreview, setShowPreview] = useState(false) const [copied, setCopied] = useState(false) const [copiedPsa, setCopiedPsa] = useState(false) const [showSaveAsTreeModal, setShowSaveAsTreeModal] = useState(false) const [isSavingTree, setIsSavingTree] = useState(false) const [showRatingModal, setShowRatingModal] = useState(false) const [isSavingRatings, setIsSavingRatings] = useState(false) const [librarySteps, setLibrarySteps] = useState([]) useEffect(() => { if (id) { loadSession() } }, [id]) // Auto-show rating modal for completed sessions with library steps useEffect(() => { if (!session || !session.completed_at) return // Check if already rated if (hasRatedSession(session.id)) return // Extract library steps from custom_steps const stepsFromLibrary = session.custom_steps?.filter( (customStep) => { // Check if step_data is a Step (from library) by checking if it has an id const stepData = customStep.step_data return 'id' in stepData && stepData.id } ) || [] if (stepsFromLibrary.length === 0) return // Extract the Step objects const steps = stepsFromLibrary.map((cs) => cs.step_data as Step) setLibrarySteps(steps) // Show modal after 1 second delay const timer = setTimeout(() => { setShowRatingModal(true) }, 1000) return () => clearTimeout(timer) }, [session]) const loadSession = async () => { setIsLoading(true) setError(null) try { const data = await sessionsApi.get(id!) setSession(data) } catch (err) { setError('Failed to load session') console.error(err) } finally { setIsLoading(false) } } const getFilename = () => { if (!session) return 'export.txt' const ext = exportFormat === 'markdown' ? 'md' : exportFormat === 'html' ? 'html' : 'txt' // psa and text both use .txt return `session-${session.ticket_number || session.id}.${ext}` } const fetchExportContent = async () => { if (!session) return null const options: SessionExport = { format: exportFormat, include_timestamps: true, include_tree_info: true, } return await sessionsApi.export(session.id, options) } const handlePreview = async () => { setIsExporting(true) try { const content = await fetchExportContent() if (content) { setExportContent(content) setShowPreview(true) } } catch (err) { console.error('Export failed:', err) toast.error('Failed to generate export preview') } finally { setIsExporting(false) } } const handleCopy = async () => { setIsExporting(true) try { const content = await fetchExportContent() if (content) { await navigator.clipboard.writeText(content) setCopied(true) setTimeout(() => setCopied(false), 2000) toast.success('Copied to clipboard') } } catch (err) { console.error('Copy failed:', err) toast.error('Failed to copy to clipboard') } finally { setIsExporting(false) } } const handleCopyForTicket = async () => { if (!session) return try { const options: SessionExport = { format: 'psa', include_timestamps: true, include_tree_info: true, } const content = await sessionsApi.export(session.id, options) if (content) { await navigator.clipboard.writeText(content) setCopiedPsa(true) setTimeout(() => setCopiedPsa(false), 2000) toast.success('Copied ticket notes to clipboard') } } catch (err) { console.error('Copy for ticket failed:', err) toast.error('Failed to copy ticket notes') } } const handleDownload = () => { if (!exportContent || !session) return const blob = new Blob([exportContent], { type: 'text/plain' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = getFilename() document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(url) } const handleSaveAsTree = async (data: SaveAsTreeRequest) => { if (!session) return setIsSavingTree(true) try { const result = await sessionsApi.saveAsTree(session.id, data) toast.success(result.message) setShowSaveAsTreeModal(false) // Navigate to tree editor with the new tree navigate(`/trees/${result.tree_id}/edit`) } catch (err) { console.error('Failed to save session as tree:', err) toast.error('Failed to save session as tree') } finally { setIsSavingTree(false) } } const getDefaultTreeName = () => { if (!session) return '' const treeName = session.tree_snapshot?.name || 'Tree' const ticket = session.ticket_number ? ` - ${session.ticket_number}` : '' return `${treeName}${ticket}` } const handleSubmitRatings = async (ratings: Map) => { if (!session) return setIsSavingRatings(true) try { // Submit each rating individually const ratingPromises = Array.from(ratings.entries()).map(([stepId, data]) => stepsApi.rate(stepId, { rating: data.rating, review_text: data.review || undefined, was_helpful: data.helpful !== null ? data.helpful : undefined, session_id: session.id, is_verified_use: true }) ) await Promise.all(ratingPromises) toast.success(`Submitted ${ratings.size} rating${ratings.size > 1 ? 's' : ''}!`) markSessionRated(session.id) setShowRatingModal(false) } catch (err) { console.error('Failed to submit ratings:', err) toast.error('Failed to submit ratings') } finally { setIsSavingRatings(false) } } const formatDate = (dateString: string) => { return new Date(dateString).toLocaleString() } if (isLoading) { return (
) } if (error || !session) { return (
{error || 'Session not found'}
) } return (
{/* Header */}

{session.ticket_number || 'Session Details'}

{session.completed_at ? 'Completed' : 'In Progress'} {session.client_name && Client: {session.client_name}}
{/* Actions */}
{/* Save as Tree - Only for completed sessions */} {session.completed_at && ( )} {/* Copy for Ticket */} {/* Export Controls */}
{/* Timeline */}

Decision Timeline

Session started: {formatDate(session.started_at)}
{session.decisions.map((decision, index) => (
{decision.question && (

{decision.question}

)} {decision.answer && (

Answer: {decision.answer}

)} {decision.action_performed && (

Action: {decision.action_performed}

)} {decision.notes && (

Notes: {decision.notes}

)}

{formatDate(decision.timestamp)}

))} {session.completed_at && (
Session completed: {formatDate(session.completed_at)}
)}
{/* Export Preview Modal */} setShowPreview(false)} content={exportContent || ''} filename={getFilename()} format={exportFormat} onDownload={handleDownload} /> {/* Save as Tree Modal */} setShowSaveAsTreeModal(false)} onSave={handleSaveAsTree} defaultTreeName={getDefaultTreeName()} isSaving={isSavingTree} /> {/* Step Rating Modal */} setShowRatingModal(false)} onSubmit={handleSubmitRatings} librarySteps={librarySteps} isSaving={isSavingRatings} />
) } export default SessionDetailPage