/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ /** * BCFTopicDetail - Topic detail view with comments, viewpoints, and editing. */ import React, { useCallback, useState, useMemo } from 'react'; import { X, MessageSquare, Camera, ChevronLeft, Send, Trash2, User, MousePointer2, Focus, EyeOff, Crosshair, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Tooltip, TooltipContent, TooltipTrigger, } from '@/components/ui/tooltip'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Separator } from '@/components/ui/separator'; import type { BCFTopic, BCFViewpoint } from '@ifc-lite/bcf'; import { PriorityBadge, formatDate, formatDateTime, TOPIC_STATUSES } from './bcfHelpers'; // ============================================================================ // Types // ============================================================================ export interface BCFTopicDetailProps { topic: BCFTopic; onBack: () => void; onAddComment: (text: string, viewpointGuid?: string) => void; onAddViewpoint: () => void; onActivateViewpoint: (viewpoint: BCFViewpoint) => void; onDeleteViewpoint: (viewpointGuid: string) => void; onUpdateStatus: (status: string) => void; onZoomToTopic: () => void; canZoomToTopic: boolean; onDeleteTopic: () => void; // Viewer state info for capture feedback selectionCount: number; hasIsolation: boolean; hasHiddenEntities: boolean; } // ============================================================================ // Component // ============================================================================ export function BCFTopicDetail({ topic, onBack, onAddComment, onAddViewpoint, onActivateViewpoint, onDeleteViewpoint, onUpdateStatus, onZoomToTopic, canZoomToTopic, onDeleteTopic, selectionCount, hasIsolation, hasHiddenEntities, }: BCFTopicDetailProps) { const [commentText, setCommentText] = useState(''); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [selectedViewpointGuid, setSelectedViewpointGuid] = useState(null); // Get the selected viewpoint for display const selectedViewpoint = useMemo(() => { if (!selectedViewpointGuid) return null; return topic.viewpoints.find(vp => vp.guid === selectedViewpointGuid) || null; }, [selectedViewpointGuid, topic.viewpoints]); const handleSubmitComment = useCallback(() => { if (commentText.trim()) { // Associate comment with selected viewpoint if one is selected onAddComment(commentText.trim(), selectedViewpointGuid || undefined); setCommentText(''); setSelectedViewpointGuid(null); // Clear selection after commenting } }, [commentText, onAddComment, selectedViewpointGuid]); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSubmitComment(); } }, [handleSubmitComment] ); return (
{/* Header */}

{topic.title}

Zoom to
{/* Topic Info */}
{topic.topicType && ( {topic.topicType} )}
{topic.description && (

{topic.description}

)}

Created by {topic.creationAuthor} on{' '} {formatDateTime(topic.creationDate)}

{topic.assignedTo &&

Assigned to: {topic.assignedTo}

} {topic.dueDate &&

Due: {formatDate(topic.dueDate)}

}
{/* Viewpoints */}

Viewpoints

{/* Capture info - what will be included */} {(selectionCount > 0 || hasIsolation || hasHiddenEntities) && (

Capture will include:

    {selectionCount > 0 && (
  • {selectionCount} selected {selectionCount === 1 ? 'object' : 'objects'}
  • )} {hasIsolation && (
  • Isolated objects (others hidden)
  • )} {hasHiddenEntities && !hasIsolation && (
  • Hidden objects
  • )}
)} {topic.viewpoints.length === 0 ? (

No viewpoints captured

) : (
{topic.viewpoints.map((vp) => { const isSelected = selectedViewpointGuid === vp.guid; const commentCount = topic.comments.filter(c => c.viewpointGuid === vp.guid).length; return (
{/* Snapshot */}
{vp.snapshot ? ( Viewpoint onActivateViewpoint(vp)} /> ) : (
onActivateViewpoint(vp)} >
)} {/* Delete button - hover only */}
{/* Action bar - always visible */}
); })}
)}
{/* Comments */}

Comments ({topic.comments.length})

{topic.comments.map((comment) => { // Find associated viewpoint if any const associatedViewpoint = comment.viewpointGuid ? topic.viewpoints.find(vp => vp.guid === comment.viewpointGuid) : null; return (
{/* Show associated viewpoint thumbnail if present */} {associatedViewpoint?.snapshot && (
onActivateViewpoint(associatedViewpoint)} > Associated viewpoint
)}
{comment.author.split('@')[0]} - {formatDateTime(comment.date)} {comment.viewpointGuid && ( )}

{comment.comment}

); })}
{/* Comment Input */}
{/* Show selected viewpoint indicator */} {selectedViewpoint && (
{selectedViewpoint.snapshot && ( Selected viewpoint )}

Commenting on viewpoint

)}
setCommentText(e.target.value)} onKeyDown={handleKeyDown} className="flex-1" />
{/* Delete Confirmation */} {showDeleteConfirm && (

Delete Topic?

This will permanently delete this topic and all its comments and viewpoints.

)}
); }