🔍 Code Extractor

class DocumentDetail

Maturity: 27

Document detail view component

File:
/tf/active/vicechatdev/document_detail_backup.py
Lines:
64 - 2129
Complexity:
moderate

Purpose

Document detail view component

Source Code

class DocumentDetail(param.Parameterized):
    """Document detail view component"""
    
    document_uid = param.String(default='')
    doc_number = param.String(default='')
    current_tab = param.String(default='overview')
    
    def __init__(self, parent_app=None, **params):
        super().__init__(**params)
        self.parent_app = parent_app
        self.template = None  # No template needed when embedded
        self.session_manager = SessionManager()
        self.user = None
        self.document = None
        self.current_version = None
        self.notification_area = pn.pane.Markdown("")
        self.main_content = pn.Column(sizing_mode='stretch_width')
        
        # Document info and actions areas
        self.doc_info_area = pn.Column(sizing_mode='stretch_width')
        self.doc_actions_area = pn.Column(sizing_mode='stretch_width')
        
        # Create tabs for different sections
        self.tabs = pn.layout.Tabs(sizing_mode='stretch_width')
    
    def set_user(self, user):
        """Set the current user."""
        self.user = user
        return True

    def load_document(self, document_uid=None, doc_number=None):
        """Load document by UID or document number."""
        if document_uid:
            self.document_uid = document_uid
        
        if doc_number:
            self.doc_number = doc_number
            
        return self._load_document()
    
    def get_document_view(self):
        """Get the document view for embedding in other panels."""
        container = pn.Column(
            self.notification_area,
            self.main_content,
            sizing_mode='stretch_width'
        )
        return container
    
    def _get_current_user(self) -> DocUser:
        """Get the current user from session"""
        user_id = self.session_manager.get_user_id()
        if user_id:
            return DocUser(uid=user_id)
        return None
    
    def _setup_header(self):
        """Set up the header with title and actions"""
        # Create back button
        back_btn = Button(
            name='Back to Dashboard',
            button_type='default',
            width=150
        )
        back_btn.on_click(self._navigate_back)
        
        # Create refresh button
        refresh_btn = Button(
            name='Refresh',
            button_type='default',
            width=100
        )
        refresh_btn.on_click(self._load_document)
        
        # Header with buttons
        header = Row(
            pn.pane.Markdown("# Document Details"),
            refresh_btn,
            back_btn,
            sizing_mode='stretch_width',
            align='end'
        )
        
        self.template.header.append(header)
    
    def _setup_sidebar(self):
        """Set up the sidebar with document actions"""
        # Document info area
        self.doc_info_area = Column(
            sizing_mode='stretch_width'
        )
        
        # Document actions area
        self.doc_actions_area = Column(
            sizing_mode='stretch_width'
        )
        
        # Add to sidebar
        self.template.sidebar.append(self.doc_info_area)
        self.template.sidebar.append(self.doc_actions_area)
    
    def _setup_main_area(self):
        """Set up the main area with document content tabs"""
        # Create notification area
        self.template.main.append(self.notification_area)
        
        # Create tabs for different sections
        self.tabs = Tabs(
            sizing_mode='stretch_width'
        )
        
        # Add tabs container to main area
        self.template.main.append(self.tabs)
    
    def _load_document(self, event=None):
        """Load document data and update the UI."""
        try:
            # Guard against recursive calls
            if hasattr(self, '_loading_document') and self._loading_document:
                logger.warning("Recursive call to _load_document avoided")
                return False
                
            self._loading_document = True
            
            try:
                # Clear notification
                self.notification_area.object = ""
                
                # Clear existing UI elements
                self.main_content.clear()
                self.doc_info_area.clear()
                self.doc_actions_area.clear()
                self.tabs.clear()
                
                # Get document details using document_uid directly
                if self.document_uid:
                    # Only fetch document if we don't already have it
                    if not self.document:
                        document_data = get_document(document_uid=self.document_uid)
                        
                        if not document_data:
                            self.notification_area.object = "**Error:** Document not found"
                            return False
                            
                        # Store the document
                        self.document = document_data
                        
                        # Extract properties from document data
                        self._extract_document_properties()
                    
                    # Create header
                    doc_header = pn.Column(
                        pn.pane.Markdown(f"# {self.doc_title or 'Untitled Document'}"),
                        pn.pane.Markdown(f"**Document Number:** {self.doc_number or 'No number'} | " +
                                        f"**Revision:** {self.doc_revision or 'None'} | " +
                                        f"**Status:** {self.doc_status or 'Unknown'}"),
                        sizing_mode='stretch_width'
                    )
                    
                    # Add header to main content
                    self.main_content.append(doc_header)
                    
                    # Set up document info
                    self._setup_document_info()
                    
                    # Set up document actions
                    self._setup_document_actions()
                    
                    # Create and add tabs for document content
                    self._create_document_tabs()
                    
                    # Add document info and actions to main content (in a row to mimic sidebar)
                    info_actions = pn.Row(
                        self.doc_info_area,
                        pn.layout.HSpacer(width=20),  # Spacing
                        self.doc_actions_area
                    )
                    
                    # Create layout
                    layout = pn.Column(
                        doc_header,
                        info_actions,
                        self.tabs,
                        sizing_mode='stretch_width'
                    )
                    
                    # Update main content
                    self.main_content.clear()
                    self.main_content.append(layout)
                    
                    return True
                else:
                    self.notification_area.object = "**Error:** No document UID provided"
                    return False
                    
            finally:
                # Always clear the loading flag
                self._loading_document = False
                
        except Exception as e:
            self._loading_document = False  # Ensure flag is cleared
            self.notification_area.object = f"**Error:** {str(e)}"
            logger.error(f"Error loading document: {e}")
            import traceback
            logger.error(f"Traceback: {traceback.format_exc()}")
            return False
    
    def _extract_document_properties(self):
        """Extract document properties from document data."""
        if not self.document:
            return
            
        # Case-insensitive property extraction
        def get_property(data, possible_keys, default=''):
            if not data:
                return default
            
            for key in possible_keys:
                if hasattr(data, 'get'):
                    value = data.get(key)
                    if value:
                        return value
            return default
        
        # Extract document metadata with case-insensitive lookup
        self.document_uid = get_property(self.document, ['UID', 'uid'])
        self.doc_title = get_property(self.document, ['title', 'Title'])
        self.doc_number = get_property(self.document, ['docNumber', 'doc_number', 'documentNumber'])
        self.doc_revision = get_property(self.document, ['revision', 'Revision'])
        self.doc_status = get_property(self.document, ['status', 'Status'])
        self.doc_type = get_property(self.document, ['docType', 'doc_type', 'documentType'])
        self.doc_department = get_property(self.document, ['department', 'Department'])
        self.doc_owner = get_property(self.document, ['ownerName', 'owner_name', 'owner'])
        self.doc_owner_uid = get_property(self.document, ['ownerUID', 'owner_uid'])
        self.doc_creator = get_property(self.document, ['creatorName', 'creator_name', 'creator'])
        self.doc_creator_uid = get_property(self.document, ['creatorUID', 'creator_uid'])
        self.doc_created_date = get_property(self.document, ['createdDate', 'created_date', 'created'])
        self.doc_modified_date = get_property(self.document, ['modifiedDate', 'modified_date', 'modified'])
        
        # Get document content if available directly or fetch it if needed
        self.doc_content = get_property(self.document, ['content', 'text', 'doc_text'])
    
    def load_document_data(self, document_data):
        """
        Load document directly from document data.
        
        Parameters:
        -----------
        document_data : dict
            The document data to load
        """
        logger.debug(f"Loading document from data: {type(document_data)}")
        
        try:
            # Debug the document data keys
            if hasattr(document_data, 'keys'):
                logger.debug(f"Document data keys: {document_data.keys()}")
            
            # Store the document data
            self.document = document_data
            
            # Case-insensitive property extraction
            def get_property(data, possible_keys, default=''):
                if not data:
                    return default
                
                for key in possible_keys:
                    if hasattr(data, 'get'):
                        value = data.get(key)
                        if value:
                            return value
                return default
            
            # Extract document metadata with case-insensitive lookup
            self.document_uid = get_property(document_data, ['UID', 'uid'])  # This is correct
            self.doc_title = get_property(document_data, ['title', 'Title'])
            self.doc_number = get_property(document_data, ['docNumber', 'doc_number', 'documentNumber'])
            self.doc_revision = get_property(document_data, ['revision', 'Revision'])
            self.doc_status = get_property(document_data, ['status', 'Status'])
            self.doc_type = get_property(document_data, ['docType', 'doc_type', 'documentType'])
            self.doc_department = get_property(document_data, ['department', 'Department'])
            self.doc_owner = get_property(document_data, ['ownerName', 'owner_name', 'owner'])
            self.doc_owner_uid = get_property(document_data, ['ownerUID', 'owner_uid'])
            self.doc_creator = get_property(document_data, ['creatorName', 'creator_name', 'creator'])
            self.doc_creator_uid = get_property(document_data, ['creatorUID', 'creator_uid'])
            self.doc_created_date = get_property(document_data, ['createdDate', 'created_date', 'created'])
            self.doc_modified_date = get_property(document_data, ['modifiedDate', 'modified_date', 'modified'])
            
            # Get document content if available directly or fetch it if needed
            self.doc_content = get_property(document_data, ['content', 'text', 'doc_text'])
            
            # FIX: Use self.document_uid instead of self.doc_uid
            if not self.doc_content and self.document_uid:
                # Fetch content if not included in document data
                from CDocs.controllers.document_controller import get_document_content
                content_result = get_document_content(self.document_uid)
                if content_result and isinstance(content_result, dict):
                    self.doc_content = content_result.get('content', '')
            
            # Just return True - don't try to call _load_document() which will cause infinite recursion
            logger.debug("Document data loaded successfully")
            return True
            
        except Exception as e:
            logger.error(f"Error loading document data: {e}")
            import traceback
            logger.error(f"Traceback: {traceback.format_exc()}")
            
            # Display error message in UI
            if hasattr(self, 'notification_area'):
                self.notification_area.object = f"**Error:** {str(e)}"
            
            return False
    
    def _setup_document_info(self):
        """Set up document info panel in sidebar"""
        # Document status with appropriate styling
        status_code = self.doc_status or 'DRAFT'
        status_name = settings.get_document_status_name(status_code)
        status_color = settings.get_status_color(status_code)
        
        # Basic document info
        doc_info = pn.Column(
            pn.pane.Markdown(f"## {self.doc_number or 'No document number'}"),
            pn.pane.Markdown(f"**Title:** {self.doc_title or 'Untitled'}"),
            pn.pane.Markdown(f"**Type:** {settings.get_document_type_name(self.doc_type)}"),
            pn.pane.Markdown(f"**Department:** {settings.get_department_name(self.doc_department)}"),
            pn.pane.Markdown(f"**Status:** <span style='color:{status_color};font-weight:bold;'>{status_name}</span>"),
            pn.pane.Markdown(f"**Owner:** {self.doc_owner or 'Unassigned'}"),
            pn.pane.Markdown(f"**Created:** {self._format_date(self.doc_created_date)}"),
            styles={'background':'#f8f9fa'},
            css_classes=['p-3', 'border', 'rounded']
        )
        
        self.doc_info_area.append(doc_info)
        
        # Include version info code from the original method
        # Get current version
        self.current_version = self.document.get('current_version')
        
        # Version info if available
        if self.current_version:
            version_info = Column(
                Markdown(f"### Current Version"),
                Markdown(f"**Version:** {self.current_version.get('version_number', '')}"),
                Markdown(f"**Created by:** {self.current_version.get('created_by_name', '')}"),
                Markdown(f"**Date:** {self._format_date(self.current_version.get('created_date'))}"),
                Markdown(f"**File:** {self.current_version.get('file_name', '')}"),
                styles={'background':'#f8f9fa'},
                css_classes=['p-3', 'border', 'rounded', 'mt-3']
            )
            self.doc_info_area.append(doc_info)
            self.doc_info_area.append(version_info)
        else:
            self.doc_info_area.append(doc_info)
            self.doc_info_area.append(Markdown("*No versions available*"))
    
    def _setup_document_actions(self):
        """Set up document action buttons in sidebar"""
        # Create action button container
        self.doc_actions_area.append(Markdown("## Actions"))
        
        # Different actions based on document status and user permissions
        status = self.document.get('status', '')
        
        # View/download button always available
        if self.current_version:
            view_btn = Button(name="View Document", button_type="primary", width=200)
            view_btn.on_click(self._view_document)
            self.doc_actions_area.append(view_btn)
        
        # Edit metadata button - available if user has edit permission
        if permissions.user_has_permission(self.user, "EDIT_DOCUMENT"):
            edit_btn = Button(name="Edit Metadata", button_type="default", width=200)
            edit_btn.on_click(self._show_edit_form)
            self.doc_actions_area.append(edit_btn)
        
        # Upload new version - available if user has create version permission
        if permissions.user_has_permission(self.user, "CREATE_VERSION"):
            upload_btn = Button(name="Upload New Version", button_type="default", width=200)
            upload_btn.on_click(self._show_upload_form)
            self.doc_actions_area.append(upload_btn)
        
        # Review button - available for draft documents if user has review initiation permission
        if status in ['DRAFT'] and permissions.user_has_permission(self.user, "INITIATE_REVIEW"):
            review_btn = Button(name="Start Review", button_type="default", width=200)
            review_btn.on_click(self._show_review_form)
            self.doc_actions_area.append(review_btn)
        
        # Approval button - available for approved documents if user has approval initiation permission
        if status in ['APPROVED'] and permissions.user_has_permission(self.user, "INITIATE_APPROVAL"):
            approval_btn = Button(name="Start Approval", button_type="default", width=200)
            approval_btn.on_click(self._show_approval_form)
            self.doc_actions_area.append(approval_btn)
        
        # Publish button - available for approved documents if user has publish permission
        if status in ['APPROVED'] and permissions.user_has_permission(self.user, "PUBLISH_DOCUMENT"):
            publish_btn = Button(name="Publish Document", button_type="success", width=200)
            publish_btn.on_click(self._show_publish_form)
            self.doc_actions_area.append(publish_btn)
        
        # Archive button - available for published documents if user has archive permission
        if status in ['PUBLISHED', 'EFFECTIVE'] and permissions.user_has_permission(self.user, "ARCHIVE_DOCUMENT"):
            archive_btn = Button(name="Archive Document", button_type="danger", width=200)
            archive_btn.on_click(self._show_archive_form)
            self.doc_actions_area.append(archive_btn)
        
        # Clone button - always available if user has create document permission
        if permissions.user_has_permission(self.user, "CREATE_DOCUMENT"):
            clone_btn = Button(name="Clone Document", button_type="default", width=200)
            clone_btn.on_click(self._show_clone_form)
            self.doc_actions_area.append(clone_btn)
    
    def _create_document_tabs(self):
        """Create tabs for different document content sections"""
        # Overview tab
        overview_tab = self._create_overview_tab()
        
        # Versions tab
        versions_tab = self._create_versions_tab()
        
        # Reviews tab
        reviews_tab = self._create_reviews_tab()
        
        # Approvals tab
        approvals_tab = self._create_approvals_tab()
        
        # Audit trail tab
        audit_tab = self._create_audit_tab()
        
        # Add tabs to the tabs container
        self.tabs.extend([
            ('Overview', overview_tab),
            ('Versions', versions_tab),
            ('Reviews', reviews_tab),
            ('Approvals', approvals_tab),
            ('Audit Trail', audit_tab)
        ])
    
    def _create_overview_tab(self):
        """Create the overview tab content"""
        # Basic overview information
        description = self.document.get('description', 'No description available')
        
        # Key metadata from document
        created_date = self._format_date(self.document.get('createdDate'))
        modified_date = self._format_date(self.document.get('modifiedDate'))
        effective_date = self._format_date(self.document.get('effective_date'))
        expiry_date = self._format_date(self.document.get('expiry_date'))
        
        # Current version preview if available
        preview_pane = Column(
            sizing_mode='stretch_width',
            height=600
        )
        
        if self.current_version:
            try:
                # Add document viewer
                doc_viewer = self._create_document_viewer()
                preview_pane.append(doc_viewer)
            except Exception as e:
                logger.error(f"Error creating document preview: {e}")
                preview_pane.append(Markdown("*Error loading document preview*"))
        else:
            preview_pane.append(Markdown("*No document version available for preview*"))
        
        # Document lifecycle dates
        dates_section = Column(
            Markdown("### Document Timeline"),
            Markdown(f"**Created:** {created_date}"),
            Markdown(f"**Last Modified:** {modified_date}"),
            Markdown(f"**Effective Date:** {effective_date or 'Not set'}"),
            Markdown(f"**Expiry Date:** {expiry_date or 'Not set'}"),
            styles={'background':'#f8f9fa'},
            css_classes=['p-3', 'border', 'rounded']
        )
        
        # Create layout
        overview_layout = Column(
            Markdown(f"# {self.document.get('title')}"),
            Markdown(f"## Description"),
            Markdown(description),
            Row(
                dates_section,
                sizing_mode='stretch_width'
            ),
            Markdown("## Document Preview"),
            preview_pane,
            sizing_mode='stretch_width'
        )
        
        return overview_layout
    
    def _create_versions_tab(self):
        """Create the versions tab content"""
        logger = logging.getLogger('CDocs.ui.document_detail')
        logger.debug("Creating versions tab")
        
        # Get versions from document with error handling
        versions = []
        
        try:
            # Debug the document structure
            logger.debug(f"Document keys: {list(self.document.keys() if isinstance(self.document, dict) else [])}")
            
            # Try accessing versions from the document
            if isinstance(self.document, dict):
                if 'versions' in self.document and isinstance(self.document['versions'], list):
                    versions = self.document['versions']
                    logger.debug(f"Found {len(versions)} versions in document['versions']")
                elif 'document_versions' in self.document and isinstance(self.document['document_versions'], list):
                    versions = self.document['document_versions']
                    logger.debug(f"Found {len(versions)} versions in document['document_versions']")
                
            # If no versions found in document, fetch them directly
            if not versions and hasattr(self, 'document_uid') and self.document_uid:
                logger.debug(f"No versions found in document, fetching directly for {self.document_uid}")
                try:
                    from CDocs.controllers.document_controller import get_document_versions
                    version_result = get_document_versions(self.document_uid)
                    logger.debug(f"get_document_versions result: {version_result}")
                    
                    if version_result and version_result.get('success') and 'versions' in version_result:
                        versions = version_result['versions']
                        logger.debug(f"Loaded {len(versions)} versions from direct API call")
                except Exception as version_err:
                    logger.error(f"Error fetching versions: {version_err}")
                    import traceback
                    logger.error(traceback.format_exc())
        except Exception as e:
            logger.error(f"Error accessing document versions: {e}")
            import traceback
            logger.error(traceback.format_exc())
            versions = []
        
        # Debug the versions we found
        logger.debug(f"Final versions count: {len(versions)}")
        if versions and len(versions) > 0:
            logger.debug(f"First version keys: {list(versions[0].keys() if isinstance(versions[0], dict) else [])}")
        
        if not versions:
            # Create upload new version button if user has permission
            upload_btn = Button(
                name="Upload New Version", 
                button_type="primary", 
                width=150,
                disabled=not permissions.user_has_permission(self.user, "CREATE_VERSION")
            )
            upload_btn.on_click(self._show_upload_form)
            
            return pn.Column(
                pn.pane.Markdown("# Document Versions"),
                pn.pane.Markdown("*No versions available for this document*"),
                upload_btn,
                sizing_mode='stretch_width'
            )
        
        # Convert versions to DataFrame with flexible field names
        version_rows = []
        for version in versions:
            # Skip if not a dictionary
            if not isinstance(version, dict):
                continue
                
            # Helper function to get a field with multiple possible names
            def get_field(names):
                for name in names:
                    if name in version and version[name] is not None:
                        return version[name]
                return ""
            
            # Extract data with fallbacks for different field names
            version_row = {
                'UID': get_field(['UID', 'uid', 'version_uid']),
                'version_number': get_field(['version_number', 'versionNumber', 'number', 'revision']),
                'is_current': get_field(['is_current', 'isCurrent', 'current']) or False,
                'created_date': get_field(['created_date', 'createdDate', 'date']),
                'created_by_name': get_field(['created_by_name', 'createdByName', 'creatorName', 'creator']),
                'file_name': get_field(['file_name', 'fileName', 'name']),
                'comment': get_field(['comment', 'versionComment', 'notes'])
            }
            version_rows.append(version_row)
        
        # If no valid rows, show message
        if not version_rows:
            return pn.Column(
                pn.pane.Markdown("# Document Versions"),
                pn.pane.Markdown(f"*No valid version data found*"),
                sizing_mode='stretch_width'
            )
        
        # Create DataFrame
        versions_df = pd.DataFrame(version_rows)
        logger.debug(f"Created DataFrame with columns: {versions_df.columns.tolist()}")
        
        # Format dates
        if 'created_date' in versions_df.columns:
            # Convert dates safely
            try:
                versions_df['created_date'] = pd.to_datetime(versions_df['created_date']).dt.strftime('%Y-%m-%d %H:%M')
            except Exception as date_err:
                logger.warning(f"Error formatting dates: {date_err}")
        
        # Sort by version number if possible
        try:
            versions_df = versions_df.sort_values('version_number', ascending=False)
        except Exception as sort_err:
            logger.warning(f"Error sorting versions: {sort_err}")
        
        # Add hidden UID column for reference
        if 'UID' in versions_df.columns:
            versions_df['_uid'] = versions_df['UID']
        
        # Select columns for display
        display_columns = []
        column_names = {}
        
        # Include columns that exist in the dataframe
        if 'version_number' in versions_df.columns:
            display_columns.append('version_number')
            column_names['version_number'] = 'Version'
            
        if 'is_current' in versions_df.columns:
            display_columns.append('is_current')
            column_names['is_current'] = 'Current'
            
        if 'created_date' in versions_df.columns:
            display_columns.append('created_date')
            column_names['created_date'] = 'Created'
            
        if 'created_by_name' in versions_df.columns:
            display_columns.append('created_by_name')
            column_names['created_by_name'] = 'Created By'
            
        if 'file_name' in versions_df.columns:
            display_columns.append('file_name')
            column_names['file_name'] = 'File Name'
            
        if 'comment' in versions_df.columns:
            display_columns.append('comment')
            column_names['comment'] = 'Comment'
        
        # Add action column with button formatter
        versions_df['_action'] = 'Download'
        display_columns.append('_action')
        column_names['_action'] = 'Action'
        
        # Filter and rename columns
        filtered_cols = [col for col in display_columns if col in versions_df.columns]
        
        # Make sure to always include the UID columns even if they're not displayed
        display_df = versions_df[filtered_cols]

        # Create formatters for tabulator
        formatters = {
            'Current': {'type': 'tickCross'},
            'Action': {
                'type': 'button',
                'buttonTitle': 'Download',
                'buttonLabel': 'Download'
            }
        }

        # Make sure there's a hidden UID column for reference
        if 'uid' in versions_df.columns:
            display_df['__uid'] = versions_df['UID']  # Double underscore to avoid conflicts

        if '_uid' in versions_df.columns:
            display_df['__uid'] = versions_df['_uid']  # Double underscore to avoid conflicts

        # Create versions table
        versions_table = Tabulator(
            display_df,
            pagination='local',
            page_size=10,
            sizing_mode='stretch_width',
            selectable=1,
            height=400,
            formatters=formatters
        )
        
        # Add version selection handler
        versions_table.on_click(self._version_selected)
        
        # Create upload new version button if user has permission
        upload_btn = Button(
            name="Upload New Version", 
            button_type="primary", 
            width=150,
            disabled=not permissions.user_has_permission(self.user, "CREATE_VERSION")
        )
        upload_btn.on_click(self._show_upload_form)
        
        # Create version action area
        version_action_area = pn.Column(
            pn.pane.Markdown("## Select a version to view"),
            sizing_mode='stretch_width'
        )
        
        # Store this for later reference
        self._version_action_area = version_action_area
        
        # Layout
        versions_layout = pn.Column(
            pn.pane.Markdown("# Document Versions"),
            pn.pane.Markdown("The table below shows all versions of this document. Click on a version to view or download it."),
            pn.Row(
                pn.layout.HSpacer(),
                upload_btn,
                sizing_mode='stretch_width',
                align='end'
            ),
            versions_table,
            version_action_area,
            sizing_mode='stretch_width'
        )
        
        return versions_layout
    
    def _create_reviews_tab(self):
        """Create the reviews tab content"""
        # Get review cycles from document
        review_cycles = []
        
        try:
            # Call controller to get review cycles
            from CDocs.controllers.review_controller import get_document_review_cycles
            review_result = get_document_review_cycles(document_uid=self.document_uid)
            review_cycles = review_result.get('review_cycles', [])
            
            logger.debug(f"Loaded {len(review_cycles)} review cycles")
        except Exception as e:
            logger.error(f"Error loading review cycles: {e}")
            return pn.Column(
                pn.pane.Markdown("# Document Reviews"),
                pn.pane.Markdown(f"**Error loading review data:** {str(e)}"),
                sizing_mode='stretch_width'
            )
        
        if not review_cycles:
            # Create button to start review if appropriate
            if self.document.get('status') == 'DRAFT' and permissions.user_has_permission(self.user, "INITIATE_REVIEW"):
                start_review_btn = Button(
                    name="Start Review Cycle", 
                    button_type="primary", 
                    width=150
                )
                start_review_btn.on_click(self._show_review_form)
                
                return Column(
                    Markdown("# Document Reviews"),
                    Markdown("*No review cycles found for this document*"),
                    start_review_btn,
                    sizing_mode='stretch_width'
                )
            else:
                return Column(
                    Markdown("# Document Reviews"),
                    Markdown("*No review cycles found for this document*"),
                    sizing_mode='stretch_width'
                )
        
        # Rest of the function remains the same...
        
        # Convert to DataFrame for tabulator
        try:
            # Create a clean list of dictionaries for the DataFrame
            reviews_data = []
            for cycle in review_cycles:
                cycle_data = {
                    'uid': cycle.get('UID', ''),
                    'status': cycle.get('status', 'Unknown'),
                    'start_date': cycle.get('startDate', None),
                    'due_date': cycle.get('dueDate', None),
                    'completed_date': cycle.get('completionDate', None),
                    'reviewers_count': len(cycle.get('reviewers', [])),
                    'initiated_by_name': cycle.get('initiated_by_name', 'Unknown')
                }
                reviews_data.append(cycle_data)
            
            # Create DataFrame (safely handle empty data)
            if not reviews_data:
                return Markdown("*No review cycles found with valid data*")
            
            reviews_df = pd.DataFrame(reviews_data)
        except Exception as df_error:
            logger.error(f"Error creating reviews DataFrame: {df_error}")
            return Markdown("*Error formatting review cycle data*")
        
        # Rest of the function as before...
        
        # Format dates
        date_columns = ['start_date', 'due_date', 'completed_date']
        for col in date_columns:
            if col in reviews_df.columns:
                reviews_df[col] = pd.to_datetime(reviews_df[col]).dt.strftime('%Y-%m-%d')
        
        # Select and rename columns for display
        display_columns = ['UID', 'cycle_number', 'status', 'initiated_by_name', 'start_date', 'due_date', 'completed_date']
        column_names = {
            'cycle_number': 'Cycle #',
            'status': 'Status',
            'initiated_by_name': 'Initiated By',
            'start_date': 'Started',
            'due_date': 'Due',
            'completed_date': 'Completed'
        }
        
        # Filter columns that exist in the DataFrame
        exist_columns = [col for col in display_columns if col in reviews_df.columns]
        reviews_df = reviews_df[exist_columns]
        
        # Rename columns
        rename_dict = {col: column_names[col] for col in exist_columns if col in column_names}
        reviews_df = reviews_df.rename(columns=rename_dict)
        
        # Add action column
        reviews_df['Action'] = 'View'
        
        # Create reviews table
        reviews_table = Tabulator(
            reviews_df,
            pagination='local',
            page_size=5,
            sizing_mode='stretch_width',
            selectable=1,
            height=300
        )
        
        # Add review selection handler
        reviews_table.on_click(self._review_selected)
        
        # Create review details area
        review_details_area = Column(
            Markdown("## Review Details"),
            Markdown("*Select a review cycle to see details*"),
            sizing_mode='stretch_width',
            styles={'background':'#f8f9fa'},
            css_classes=['p-3', 'border', 'rounded']
        )
        
        # Create start review button if appropriate
        buttons = []
        if self.document.get('status') == 'DRAFT' and permissions.user_has_permission(self.user, "INITIATE_REVIEW"):
            start_review_btn = Button(
                name="Start New Review Cycle", 
                button_type="primary", 
                width=180
            )
            start_review_btn.on_click(self._show_review_form)
            buttons.append(start_review_btn)
        
        # Layout
        reviews_layout = Column(
            Markdown("# Document Reviews"),
            Row(*buttons, sizing_mode='stretch_width', align='end') if buttons else None,
            reviews_table,
            review_details_area,
            sizing_mode='stretch_width'
        )
        
        return reviews_layout
    
    def _create_approvals_tab(self):
        """Create the approvals tab content"""
        # Get approval workflows from document
        approval_workflows = []
        
        try:
            # Call controller to get approval workflows
            approval_result = get_document_approvals(document_uid=self.document_uid)
            approval_workflows = approval_result.get('approval_workflows', [])
        except Exception as e:
            logger.error(f"Error loading approval workflows: {e}")
            return Markdown("*Error loading approval workflows*")
        
        if not approval_workflows:
            # Create button to start approval if appropriate
            if self.document.get('status') == 'APPROVED' and permissions.user_has_permission(self.user, "INITIATE_APPROVAL"):
                start_approval_btn = Button(
                    name="Start Approval Workflow", 
                    button_type="primary", 
                    width=180
                )
                start_approval_btn.on_click(self._show_approval_form)
                
                return Column(
                    Markdown("# Document Approvals"),
                    Markdown("*No approval workflows found for this document*"),
                    start_approval_btn,
                    sizing_mode='stretch_width'
                )
            else:
                return Column(
                    Markdown("# Document Approvals"),
                    Markdown("*No approval workflows found for this document*"),
                    sizing_mode='stretch_width'
                )
        
        # Convert to DataFrame for tabulator
        approvals_df = pd.DataFrame(approval_workflows)
        
        # Format dates
        date_columns = ['initiated_date', 'due_date', 'completed_date']
        for col in date_columns:
            if col in approvals_df.columns:
                approvals_df[col] = pd.to_datetime(approvals_df[col]).dt.strftime('%Y-%m-%d')
        
        # Select and rename columns for display
        display_columns = ['UID', 'workflow_type', 'status', 'initiated_by_name', 'initiated_date', 'due_date', 'completed_date']
        column_names = {
            'workflow_type': 'Type',
            'status': 'Status',
            'initiated_by_name': 'Initiated By',
            'initiated_date': 'Started',
            'due_date': 'Due',
            'completed_date': 'Completed'
        }
        
        # Filter columns that exist in the DataFrame
        exist_columns = [col for col in display_columns if col in approvals_df.columns]
        approvals_df = approvals_df[exist_columns]
        
        # Rename columns
        rename_dict = {col: column_names[col] for col in exist_columns if col in column_names}
        approvals_df = approvals_df.rename(columns=rename_dict)
        
        # Add action column
        approvals_df['Action'] = 'View'
        
        # Create approvals table
        approvals_table = Tabulator(
            approvals_df,
            pagination='local',
            page_size=5,
            sizing_mode='stretch_width',
            selectable=1,
            height=300
        )
        
        # Add approval selection handler
        approvals_table.on_click(self._approval_selected)
        
        # Create approval details area
        approval_details_area = Column(
            Markdown("## Approval Details"),
            Markdown("*Select an approval workflow to see details*"),
            sizing_mode='stretch_width',
            styles={'background':'#f8f9fa'},
            css_classes=['p-3', 'border', 'rounded']
        )
        
        # Create start approval button if appropriate
        buttons = []
        if self.document.get('status') == 'APPROVED' and permissions.user_has_permission(self.user, "INITIATE_APPROVAL"):
            start_approval_btn = Button(
                name="Start New Approval Workflow", 
                button_type="primary", 
                width=220
            )
            start_approval_btn.on_click(self._show_approval_form)
            buttons.append(start_approval_btn)
        
        # Layout
        approvals_layout = Column(
            Markdown("# Document Approvals"),
            Row(*buttons, sizing_mode='stretch_width', align='end') if buttons else None,
            approvals_table,
            approval_details_area,
            sizing_mode='stretch_width'
        )
        
        return approvals_layout
    
    def _create_audit_tab(self):
        """Create the audit trail tab content"""
        # Get audit trail from document
        audit_trail = []
        
        try:
            # Import the utilities for audit trail
            from CDocs.utils.audit_trail import get_document_history
            
            # Fetch document history/audit trail
            audit_trail = get_document_history(self.document_uid)
            
            # If no audit trail events were found, check if it's in the document object
            if not audit_trail and self.document and isinstance(self.document, dict):
                audit_trail = self.document.get('audit_trail', [])
            
            logger.debug(f"Fetched {len(audit_trail)} audit trail events")
        except Exception as e:
            logger.error(f"Error fetching audit trail: {e}")
            import traceback
            logger.error(traceback.format_exc())
            return Column(
                Markdown("# Document Audit Trail"),
                Markdown(f"**Error loading audit trail:** {str(e)}"),
                sizing_mode='stretch_width'
            )
        
        if not audit_trail:
            return Column(
                Markdown("# Document Audit Trail"),
                Markdown("*No audit trail events found for this document*"),
                sizing_mode='stretch_width'
            )
        
        # Ensure audit_trail is a list of dictionaries
        if not isinstance(audit_trail, list):
            audit_trail = [audit_trail] if audit_trail else []
            
        # Convert to DataFrame for tabulator
        try:
            # Create a clean list of dictionaries for the DataFrame
            audit_data = []
            
            for event in audit_trail:
                if isinstance(event, dict):
                    # Ensure timestamp exists and is properly formatted
                    timestamp = event.get('timestamp', '')
                    if timestamp:
                        # Try to parse the timestamp to ensure it's valid
                        try:
                            if isinstance(timestamp, datetime):
                                formatted_time = timestamp.strftime('%Y-%m-%d %H:%M')
                            else:
                                # Try to parse it as ISO format
                                dt = pd.to_datetime(timestamp)
                                formatted_time = dt.strftime('%Y-%m-%d %H:%M')
                        except:
                            formatted_time = str(timestamp)  # Fallback to string representation
                    else:
                        formatted_time = ''
                    
                    # Extract key information with pre-formatted timestamp
                    event_data = {
                        'timestamp': formatted_time,  # Use pre-formatted timestamp
                        'eventType': event.get('eventType', event.get('event_type', 'Unknown')),
                        'userName': event.get('userName', event.get('user_name', '')),
                        'description': event.get('description', ''),
                        'details': str(event.get('details', ''))
                    }
                    audit_data.append(event_data)
            
            # Create DataFrame
            if audit_data:
                audit_df = pd.DataFrame(audit_data)
                
                # No need for timestamp conversion as we pre-formatted the timestamps
                
                # Select and rename columns for display
                display_columns = ['timestamp', 'eventType', 'userName', 'description', 'details']
                column_names = {
                    'timestamp': 'Time',
                    'eventType': 'Event Type',
                    'userName': 'User',
                    'description': 'Description',
                    'details': 'Details'
                }
                
                # Filter columns that exist in the DataFrame
                exist_columns = [col for col in display_columns if col in audit_df.columns]
                audit_df = audit_df[exist_columns]
                
                # Rename columns
                rename_dict = {col: column_names[col] for col in exist_columns if col in column_names}
                audit_df = audit_df.rename(columns=rename_dict)
                
                # Sort by timestamp if it exists
                if 'Time' in audit_df.columns:
                    audit_df = audit_df.sort_values('Time', ascending=False)
                
                # Create audit table
                audit_table = Tabulator(
                    audit_df,
                    pagination='local',
                    page_size=20,
                    sizing_mode='stretch_width',
                    height=600,
                    show_index=False
                )
                
                # Layout
                audit_layout = Column(
                    Markdown("# Document Audit Trail"),
                    Markdown("The table below shows the complete history of actions performed on this document."),
                    audit_table,
                    sizing_mode='stretch_width'
                )
                
                return audit_layout
            else:
                return Column(
                    Markdown("# Document Audit Trail"),
                    Markdown("*No valid audit trail events found for this document*"),
                    sizing_mode='stretch_width'
                )
        except Exception as df_error:
            logger.error(f"Error creating audit trail DataFrame: {df_error}")
            import traceback
            logger.error(traceback.format_exc())
            
            return Column(
                Markdown("# Document Audit Trail"),
                Markdown(f"**Error formatting audit trail data:** {str(df_error)}"),
                sizing_mode='stretch_width'
            )
    
    def _create_document_viewer(self):
        """Create a document viewer for the current version"""
        if not self.current_version:
            return Markdown("*No document version available*")
        
        # Document type
        file_name = self.current_version.get('file_name', '')
        file_type = file_name.split('.')[-1].lower() if '.' in file_name else ''
        
        # For PDF, use iframe
        if file_type == 'pdf':
            # Get document content
            try:
                version_uid = self.current_version.get('UID')
                doc_content = download_document_version(
                        user=self.user, 
                        document_uid=self.document_uid, 
                        version_uid=version_uid
                    )
                
                if (doc_content and 'content' in doc_content) or isinstance(doc_content, bytes):
                    # Convert content to base64
                    content_b64 = base64.b64encode(doc_content['content'] if 'content' in doc_content else doc_content).decode('utf-8')
                    
                    # Create data URL
                    data_url = f"data:application/pdf;base64,{content_b64}"
                    
                    # Create iframe HTML
                    iframe_html = f"""
                    <iframe src="{data_url}" width="100%" height="600px" style="border: 1px solid #ddd;"></iframe>
                    """
                    
                    return HTML(iframe_html)
                else:
                    return Markdown("*Error loading document content*")
            except Exception as e:
                logger.error(f"Error creating PDF viewer: {e}")
                return Markdown("*Error creating document viewer*")
        
        # For images
        elif file_type in ['png', 'jpg', 'jpeg', 'gif']:
            try:
                version_uid = self.current_version.get('UID')
                doc_content = download_document_version(
                        user=self.user, 
                        document_uid=self.document_uid, 
                        version_uid=version_uid
                    )
                
                if doc_content and 'content' in doc_content:
                    # Convert content to base64
                    content_b64 = base64.b64encode(doc_content['content']).decode('utf-8')
                    
                    # Create data URL
                    data_url = f"data:image/{file_type};base64,{content_b64}"
                    
                    # Create image HTML
                    img_html = f"""
                    <img src="{data_url}" style="max-width: 100%; max-height: 600px; border: 1px solid #ddd;">
                    """
                    
                    return HTML(img_html)
                else:
                    return Markdown("*Error loading image content*")
            except Exception as e:
                logger.error(f"Error creating image viewer: {e}")
                return Markdown("*Error creating document viewer*")
        
        # For other file types, just show download link
        else:
            download_btn = Button(
                name=f"Download {file_name}", 
                button_type="primary", 
                width=200
            )
            download_btn.on_click(self._download_current_version)
            
            return Column(
                Markdown(f"Document type **{file_type}** cannot be previewed in the browser."),
                download_btn
            )
    
    def _version_selected(self, event):
        """Handle version selection from table"""
        logger = logging.getLogger('CDocs.ui.document_detail')
        
        try:
            # Handle different event types
            row_index = None
            row_data = None
            
            # Debug the event type
            logger.debug(f"Event type: {type(event).__name__}")
            
            # Check if this is a CellClickEvent (from clicking on Action column)
            if hasattr(event, 'row') and event.row is not None:
                logger.debug(f"Cell click event detected for row: {event.row}")
                row_index = event.row
                
                # Store this early so we don't lose it
                if hasattr(event, 'column'):
                    logger.debug(f"Column: {event.column}")
                    
                # For CellClickEvent, extract row data directly from the event model
                if hasattr(event, 'model') and hasattr(event.model, 'source') and hasattr(event.model.source, 'data'):
                    source_data = event.model.source.data
                    logger.debug(f"Source data keys: {list(source_data.keys())}")
                    
                    # Extract the row data directly from source data
                    # Each key in source_data is a column, with a list of values
                    try:
                        # Create a dictionary with column name -> value for this row
                        row_data = {col: values[row_index] for col, values in source_data.items() if len(values) > row_index}
                        logger.debug(f"Extracted row data directly: {row_data}")
                        
                        # LOOK FOR UID SPECIFICALLY
                        # The UID might be in index or hidden columns not directly visible
                        for col, values in source_data.items():
                            if col.lower().endswith('UID') or col == '_uid' or col == 'UID' or col == '__uid':
                                if len(values) > row_index:
                                    logger.debug(f"Found potential UID column '{col}': {values[row_index]}")
                                    # Store this directly in case it's not included in the regular row data
                                    if values[row_index]:  # Only if not empty
                                        self._selected_version_uid = values[row_index]
                                        logger.debug(f"Directly stored version UID: {self._selected_version_uid}")
                        
                    except Exception as extract_err:
                        logger.error(f"Error extracting row data: {extract_err}")
                    
                # Even for CellClickEvent, try to find UID from the actual table
                # This is the part that needs safer handling
                if hasattr(self, 'tabs') and len(self.tabs) > 1:
                    try:
                        versions_tab = self.tabs[1][1]
                        
                        # Check if versions_tab is a container before iterating
                        if hasattr(versions_tab, 'objects'):
                            # Look for Tabulator in the versions tab
                            for obj in versions_tab.objects:
                                if isinstance(obj, pn.widgets.Tabulator):
                                    if hasattr(obj, 'value'):
                                        df = obj.value
                                        logger.debug(f"Found table in versions tab with {len(df)} rows")
                                        
                                        # If we have a valid row index and DataFrame, get the UID
                                        if row_index < len(df):
                                            for uid_col in ['_uid', 'UID', '__uid']:
                                                if uid_col in df.columns:
                                                    self._selected_version_uid = df.iloc[row_index][uid_col]
                                                    logger.debug(f"Found UID in table column '{uid_col}': {self._selected_version_uid}")
                                                    break
                        else:
                            logger.debug(f"Versions tab is not a container: {type(versions_tab).__name__}")
                    except Exception as tab_err:
                        logger.error(f"Error searching for table in versions tab: {tab_err}")
                    
            # Handle TabSelector selection event format (event.new)
            elif hasattr(event, 'new') and event.new:
                row_index = event.new[0] if isinstance(event.new, list) else event.new
                logger.debug(f"Selection event detected for row: {row_index}")
                
                # For regular events, the table is in event.obj
                if hasattr(event, 'obj') and hasattr(event.obj, 'value'):
                    df = event.obj.value
                    logger.debug(f"Using DataFrame from event.obj.value with {len(df)} rows")
                    
                    if row_index < len(df):
                        row_data = df.iloc[row_index].to_dict()
                        logger.debug(f"Row data from DataFrame: {row_data}")
                        
                        # Look for UID column
                        for uid_col in ['_uid', 'UID', '__uid']:
                            if uid_col in df.columns:
                                self._selected_version_uid = df.iloc[row_index][uid_col]
                                logger.debug(f"Found UID in DataFrame column '{uid_col}': {self._selected_version_uid}")
                                break
                
            # Exit if no row index found
            if row_index is None:
                logger.warning("No row index found in event")
                return
                
            # If we still don't have row_data, try to find it
            if row_data is None:
                logger.warning("No row data extracted, searching for DataFrame")
                df = None
                
                # First try to get the DataFrame from the event directly
                if hasattr(event, 'obj') and hasattr(event.obj, 'value'):
                    df = event.obj.value
                    logger.debug(f"Got DataFrame from event.obj.value")
                elif hasattr(event, 'model') and hasattr(event.model, 'data'):
                    # Try to convert model data to DataFrame
                    try:
                        import pandas as pd
                        source_data = event.model.source.data
                        df = pd.DataFrame(source_data)
                        logger.debug(f"Created DataFrame from model.source.data")
                    except Exception as df_err:
                        logger.error(f"Error creating DataFrame from model data: {df_err}")
                
                # If still no DataFrame, find the versions table in the versions tab
                if df is None and hasattr(self, 'tabs') and len(self.tabs) > 1:
                    try:
                        versions_tab = self.tabs[1][1]
                        
                        # Check if versions_tab is a container before iterating
                        if hasattr(versions_tab, 'objects'):
                            # Look for Tabulator in the versions tab objects
                            for obj in versions_tab.objects:
                                if isinstance(obj, pn.widgets.Tabulator):
                                    if hasattr(obj, 'value'):
                                        df = obj.value
                                        logger.debug(f"Found table in versions tab with {len(df)} rows and columns: {df.columns.tolist()}")
                                        break
                        elif isinstance(versions_tab, pn.widgets.Tabulator):
                            # The tab itself might be a Tabulator
                            if hasattr(versions_tab, 'value'):
                                df = versions_tab.value
                                logger.debug(f"Tab itself is a Tabulator with {len(df)} rows")
                        else:
                            logger.debug(f"Versions tab is not a container or Tabulator: {type(versions_tab).__name__}")
                    except Exception as tab_err:
                        logger.error(f"Error searching for table in versions tab: {tab_err}")
                    
                # If we found a DataFrame and the row index is valid, extract row data
                if df is not None and row_index < len(df):
                    row_data = df.iloc[row_index].to_dict()
                    logger.debug(f"Retrieved row data from DataFrame: {row_data}")
                    
                    # Look for UID column again
                    for uid_col in ['_uid', 'UID', '__uid']:
                        if uid_col in df.columns:
                            self._selected_version_uid = df.iloc[row_index][uid_col]
                            logger.debug(f"Found UID in DataFrame column '{uid_col}': {self._selected_version_uid}")
                            break
            
            # Get version UID from row_data if we still don't have it
            if not hasattr(self, '_selected_version_uid') or not self._selected_version_uid:
                if row_data:
                    # Look for UID in the row data with different possible keys
                    uid_keys = ['_uid', 'uid', 'UID', 'version_uid', 'versionUID', '__uid']
                    for key in uid_keys:
                        if key in row_data and row_data[key]:
                            self._selected_version_uid = row_data[key]
                            logger.debug(f"Found UID in row_data with key {key}: {self._selected_version_uid}")
                            break
                    
                    # If still not found, check if any key ends with 'uid'
                    if not hasattr(self, '_selected_version_uid') or not self._selected_version_uid:
                        for key in row_data.keys():
                            if key.lower().endswith('UID'):
                                self._selected_version_uid = row_data[key]
                                logger.debug(f"Found UID in row_data with key ending with 'uid': {self._selected_version_uid}")
                                break
            
            # Exit if no row data found
            if row_data is None:
                logger.error("Could not extract row data from event")
                self.notification_area.object = "**Error:** Could not access version data"
                return
            
            # Extract version information directly from row_data
            logger.debug(f"Final row data keys: {list(row_data.keys())}")
            
            # Extract version UID from row data - try different column names
            if not hasattr(self, '_selected_version_uid') or not self._selected_version_uid:
                version_uid = None
                
                # Try different possible locations for the version UID
                uid_variants = ['_uid', 'uid', 'UID', 'version_uid', 'versionUID', '__uid']
                for key in uid_variants:
                    if key in row_data and row_data[key]:
                        version_uid = row_data[key]
                        break
                        
                # If still not found, check if there's a key ending with 'uid' (case insensitive)
                if not version_uid:
                    for key in row_data.keys():
                        if key.lower().endswith('UID'):
                            version_uid = row_data[key]
                            break
                
                logger.debug(f"Selected version UID: {version_uid}")
                
                # IMPORTANT: Store the version UID in the class instance for action buttons
                if version_uid:
                    self._selected_version_uid = version_uid
                    logger.debug(f"Stored version UID (from row data): {self._selected_version_uid}")
            
            # Final check - do we have a valid UID?
            if hasattr(self, '_selected_version_uid') and self._selected_version_uid:
                logger.debug(f"Final selected version UID: {self._selected_version_uid}")
            else:
                logger.warning("No version UID found after exhaustive search")
                self.notification_area.object = "**Error:** Could not determine version UID"
                return
            
            # Extract other information from row data
            # Handle potential column name variations based on renaming
            version_number = row_data.get('Version', row_data.get('version_number', 'Unknown'))
            created_date = row_data.get('Created', row_data.get('created_date', 'Unknown'))
            file_name = row_data.get('File Name', row_data.get('file_name', 'Unknown'))
            
            # Create download button
            download_btn = Button(
                name="Download Version", 
                button_type="primary", 
                width=150
            )
            
            # Set up click handler for download button
            download_btn.on_click(self._download_selected_version)
            
            # Create "Set as Current" button if user has permission
            set_current_btn = None
            if hasattr(self.user, 'has_permission') and self.user.has_permission("MANAGE_VERSIONS"):
                set_current_btn = Button(
                    name="Set as Current Version", 
                    button_type="success", 
                    width=200
                )
                set_current_btn.on_click(self._set_as_current_version)
            
            # Create content for the version action area
            buttons_row = pn.Row(download_btn)
            if set_current_btn:
                buttons_row.append(set_current_btn)
            
            # Display the version UID in debug mode for verification
            version_info = [
                pn.pane.Markdown(f"## Version {version_number}"),
                pn.pane.Markdown(f"Created: {created_date}"),
                pn.pane.Markdown(f"File: {file_name}")
            ]
            
            # Always add UID for easier debugging but only show in debug mode
            try:
                from CDocs.config import settings
                if getattr(settings, 'DEBUG', False):
                    version_info.append(pn.pane.Markdown(f"UID: {self._selected_version_uid}"))
            except Exception as settings_err:
                logger.error(f"Error accessing settings: {settings_err}")
            
            version_action_area = pn.Column(
                *version_info,
                buttons_row,
                styles={'background':'#f8f9fa'},
                css_classes=['p-3', 'border', 'rounded'],
                sizing_mode='stretch_width'
            )
            
            # Safely update the version action area
            if hasattr(self, '_version_action_area'):
                try:
                    self._version_action_area.objects = version_action_area.objects
                except Exception as update_err:
                    logger.error(f"Error updating _version_action_area: {update_err}")
                    # Try a full replacement
                    if hasattr(self, 'tabs') and len(self.tabs) > 1:
                        versions_tab = self.tabs[1][1]
                        if isinstance(versions_tab, pn.Column) and len(versions_tab) > 0:
                            try:
                                # Replace the last element
                                versions_tab[-1] = version_action_area
                            except Exception as replace_err:
                                logger.error(f"Error replacing version action area: {replace_err}")
            else:
                # Try to find the action area in the versions tab
                if hasattr(self, 'tabs') and len(self.tabs) > 1:
                    versions_tab = self.tabs[1][1]
                    if isinstance(versions_tab, pn.Column) and len(versions_tab) > 0:
                        try:
                            # Replace the last element
                            versions_tab[-1] = version_action_area
                        except Exception as replace_err:
                            logger.error(f"Error replacing version action area: {replace_err}")
            
        except Exception as e:
            logger.error(f"Error selecting version: {str(e)}")
            import traceback
            logger.error(traceback.format_exc())
            self.notification_area.object = f"**Error:** {str(e)}"
    
    def _review_selected(self, event):
        """Handle review selection from table"""
        if not event.new:
            return
            
        try:
            # Get selected row index
            selected_idx = event.new[0]
            
            # Get data from the DataFrame
            df = event.obj.value
            
            # Create placeholder review details
            review_details = Column(
                Markdown("## Selected Review Cycle"),
                Markdown(f"Status: {df.iloc[selected_idx]['Status']}"),
                Markdown(f"Started: {df.iloc[selected_idx]['Started']}"),
                Markdown(f"Due: {df.iloc[selected_idx]['Due']}"),
                Markdown("### Reviewers"),
                Markdown("*Reviewer information would be displayed here*"),
                styles={'background':'#f8f9fa'},
                css_classes=['p-3', 'border', 'rounded'],
                sizing_mode='stretch_width'
            )
            
            # Replace existing details area
            self.tabs[2][1].objects[-1] = review_details
            
        except Exception as e:
            logger.error(f"Error selecting review: {e}")
            self.notification_area.object = f"**Error:** {str(e)}"
    
    def _approval_selected(self, event):
        """Handle approval selection from table"""
        if not event.new:
            return
            
        try:
            # Get selected row index
            selected_idx = event.new[0]
            
            # Get data from the DataFrame
            df = event.obj.value
            
            # Create placeholder approval details
            approval_details = Column(
                Markdown("## Selected Approval Workflow"),
                Markdown(f"Type: {df.iloc[selected_idx]['Type']}"),
                Markdown(f"Status: {df.iloc[selected_idx]['Status']}"),
                Markdown(f"Started: {df.iloc[selected_idx]['Started']}"),
                Markdown(f"Due: {df.iloc[selected_idx]['Due']}"),
                Markdown("### Approvers"),
                Markdown("*Approver information would be displayed here*"),
                styles={'background':'#f8f9fa'},
                css_classes=['p-3', 'border', 'rounded'],
                sizing_mode='stretch_width'
            )
            
            # Replace existing details area
            self.tabs[3][1].objects[-1] = approval_details
            
        except Exception as e:
            logger.error(f"Error selecting approval: {e}")
            self.notification_area.object = f"**Error:** {str(e)}"
    
    def _navigate_back(self, event=None):
        """Navigate back to dashboard"""
        return pn.state.execute("window.location.href = '/dashboard'")
    
    def _view_document(self, event=None):
        """View the current document version"""
        if not self.current_version:
            self.notification_area.object = "**Error:** No document version available"
            return
            
        # Get version UID
        version_uid = self.current_version.get('UID')
        
        # Navigate to document viewer
        try:
            # Use Panel's download method
            self._download_current_version(event)
        except Exception as e:
            logger.error(f"Error viewing document: {e}")
            self.notification_area.object = f"**Error:** {str(e)}"
    
    def _download_current_version(self, event=None):
        """Download the current document version"""
        if not self.current_version:
            self.notification_area.object = "**Error:** No document version available"
            return
            
        try:
            # Get version UID
            version_uid = self.current_version.get('UID')
            
            # Import required modules
            import io
            from panel.widgets import FileDownload
            from CDocs.controllers.document_controller import download_document_version
            
            # Create a callback function that will fetch the file when the download button is clicked
            def get_file_content():
                try:
                    # Get document content with all required parameters
                    doc_content = download_document_version(
                        user=self.user, 
                        document_uid=self.document_uid, 
                        version_uid=version_uid
                    )
                    
                    if isinstance(doc_content, dict) and 'content' in doc_content and doc_content['content']:
                        # Return the binary content and filename
                        file_name = doc_content.get('file_name', 'document.pdf')
                        return io.BytesIO(doc_content['content']), file_name
                    else:
                        # Handle error
                        self.notification_area.object = "**Error:** Could not download document"
                        return None, None
                except Exception as e:
                    logger.error(f"Error downloading document: {e}")
                    self.notification_area.object = f"**Error:** {str(e)}"
                    return None, None
            
            # Get file name from current version
            file_name = self.current_version.get('file_name', 'document.pdf')
            
            # Create download widget
            download_widget = FileDownload(
                callback=get_file_content,
                filename=file_name,
                button_type="success",
                label=f"Download {file_name}"
            )
            
            # Show the download widget in the notification area
            self.notification_area.object = pn.Column(
                "**Document ready for download:**", 
                download_widget
            )
            
        except Exception as e:
            logger.error(f"Error setting up download: {e}")
            self.notification_area.object = f"**Error:** {str(e)}"
    
    def _show_edit_form(self, event=None):
        """Show form to edit document metadata"""
        self.notification_area.object = "Loading edit form..."
        
        try:
            # Create edit form with current values
            title_input = TextInput(
                name="Title",
                value=self.doc_title or self.document.get('title', ''),
                width=400
            )
            
            description_input = TextAreaInput(
                name="Description",
                value=self.document.get('description', ''),
                width=400,
                height=150
            )
            
            # Create save button
            save_btn = Button(
                name="Save Changes",
                button_type="success",
                width=120
            )
            
            # Create cancel button
            cancel_btn = Button(
                name="Cancel",
                button_type="default",
                width=120
            )
            
            # Create form layout
            edit_form = Column(
                Markdown("# Edit Document Metadata"),
                title_input,
                description_input,
                Row(
                    cancel_btn,
                    save_btn,
                    align='end'
                ),
                styles={'background':'#f8f9fa'},
                css_classes=['p-3', 'border', 'rounded'],
                width=450
            )
            
            # Set up event handlers
            save_btn.on_click(lambda event: self._save_document_changes(
                title_input.value,
                description_input.value
            ))
            
            cancel_btn.on_click(self._load_document)
            
            # Clear display area and show form
            # FIX: Check if template exists, otherwise use main_content
            if self.template and hasattr(self.template, 'main'):
                # Standalone mode using template
                self.tabs.clear()
                self.template.main.clear()
                self.template.main.append(self.notification_area)
                self.template.main.append(edit_form)
            else:
                # Embedded mode using main_content
                self.main_content.clear()
                self.main_content.append(self.notification_area)
                self.main_content.append(edit_form)
            
            # Clear notification
            self.notification_area.object = ""
            
        except Exception as e:
            logger.error(f"Error showing edit form: {e}")
            self.notification_area.object = f"**Error:** {str(e)}"
    
    def _save_document_changes(self, title, description):
        """Save document metadata changes"""
        try:
            # Validate inputs
            if not title:
                self.notification_area.object = "**Error:** Title is required"
                return
                
            # Update document
            update_result = update_document(
                document_uid=self.document_uid,
                user=self.user,
                data={
                    'title': title,
                    'description': description
                }
            )
            
            if update_result and update_result.get('success'):
                self.notification_area.object = "Document updated successfully"
                # Reload document
                self._load_document()
            else:
                error_msg = update_result.get('message', 'An error occurred')
                self.notification_area.object = f"**Error:** {error_msg}"
                
        except Exception as e:
            logger.error(f"Error saving document changes: {e}")
            self.notification_area.object = f"**Error:** {str(e)}"
    
    def _show_upload_form(self, event=None):
        """Show form to upload a new document version"""
        self.notification_area.object = "Loading upload form..."
        
        try:
            # Create file input
            file_input = FileInput(accept='.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt')
            
            # Create comment input
            comment_input = TextAreaInput(
                name="Version Comment",
                placeholder="Enter a comment for this version",
                width=400,
                height=100
            )
            
            # Create upload button
            upload_btn = Button(
                name="Upload Version",
                button_type="success",
                width=120
            )
            
            # Create cancel button
            cancel_btn = Button(
                name="Cancel",
                button_type="default",
                width=120
            )
            
            # Create form layout
            upload_form = Column(
                Markdown("# Upload New Version"),
                Markdown("Select a file to upload as a new version of this document."),
                file_input,
                comment_input,
                Row(
                    cancel_btn,
                    upload_btn,
                    align='end'
                ),
                styles={'background':'#f8f9fa'},
                css_classes=['p-3', 'border', 'rounded'],
                width=450
            )
            
            # Set up event handlers
            upload_btn.on_click(lambda event: self._upload_new_version(
                file_input.value,
                file_input.filename,
                comment_input.value
            ))
            
            cancel_btn.on_click(self._load_document)
            
            # Clear display area and show form
            # FIX: Check if template exists, otherwise use main_content
            if self.template and hasattr(self.template, 'main'):
                # Standalone mode using template
                self.tabs.clear()
                self.template.main.clear()
                self.template.main.append(self.notification_area)
                self.template.main.append(upload_form)
            else:
                # Embedded mode using main_content
                self.main_content.clear()
                self.main_content.append(self.notification_area)
                self.main_content.append(upload_form)
        
            # Clear notification
            self.notification_area.object = ""
        
        except Exception as e:
            logger.error(f"Error showing upload form: {e}")
            self.notification_area.object = f"**Error:** {str(e)}"
    
    def _upload_new_version(self, file_content, file_name, comment):
        """Upload a new document version using FileCloud for storage"""
        try:
            # Validate file
            if not file_content:
                self.notification_area.object = "**Error:** Please select a file to upload"
                return
                
            # Show upload in progress message
            self.notification_area.object = "**Uploading new version...**"
            
            # First create the document version in Neo4j
            from CDocs.controllers.document_controller import create_document_version
            
            # Call create_document_version which will handle the Neo4j part
            result = create_document_version(
                user=self.user,
                document_uid=self.document_uid,
                file_content=file_content,
                file_name=file_name,
                comment=comment
            )
            
            if not result:
                error_msg = "Failed to create new document version"
                self.notification_area.object = f"**Error:** {error_msg}"
                return
            
            doc=ControlledDocument(uid=self.document_uid)
            doc.set_current_version(result.get('UID'))

                
            # Now upload the file to FileCloud
            from CDocs.controllers.filecloud_controller import upload_document_to_filecloud
            
            # Prepare metadata for FileCloud
            # metadata = {
            #     "doc_uid": self.document_uid,
            #     "doc_number": self.doc_number,
            #     "version_uid": result.get('UID'),
            #     "version_number": result.get('version_number'),
            #     "title": self.doc_title,
            #     "status": self.doc_status,
            #     "owner": self.doc_owner,
            #     "comment": comment
            # }
            
            # Upload to FileCloud
            filecloud_result = upload_document_to_filecloud(
                user=self.user,
                document=doc,
                file_content=file_content,
                version_comment=comment,
                metadata=None
            )
            
            if not filecloud_result or not filecloud_result.get('success'):
                error_msg = "Failed to upload file to FileCloud storage"
                self.notification_area.object = f"**Error:** {error_msg}"
                return
                
            # Success!
            self.notification_area.object = "New version uploaded successfully"
            
            # Reload document
            self._load_document()
                    
        except Exception as e:
            logger.error(f"Error uploading new version: {e}")
            import traceback
            logger.error(traceback.format_exc())
            self.notification_area.object = f"**Error uploading new version:** {str(e)}"
    
    # Helper methods
    def _format_date(self, date_str):
        """Format date string for display"""
        if not date_str:
            return None
            
        try:
            date = datetime.fromisoformat(date_str)
            return date.strftime('%Y-%m-%d')
        except Exception:
            return date_str
    
    def _get_status_color(self, status_code):
        """Get color for document status"""
        return settings.get_status_color(status_code)
    
    # Form placeholders for actions that would be implemented
    def _show_review_form(self, event=None):
        """Show form to start a review cycle"""
        self.notification_area.object = "Review form would be shown here"
    
    def _show_approval_form(self, event=None):
        """Show form to start an approval workflow"""
        self.notification_area.object = "Approval form would be shown here"
    
    def _show_publish_form(self, event=None):
        """Show form to publish the document"""
        self.notification_area.object = "Publish form would be shown here"
    
    def _show_archive_form(self, event=None):
        """Show form to archive the document"""
        self.notification_area.object = "Archive form would be shown here"
    
    def _show_clone_form(self, event=None):
        """Show form to clone the document"""
        self.notification_area.object = "Clone form would be shown here"
    
    def _set_as_current_version(self, event):
        """Set the selected version as the current version"""
        logger = logging.getLogger('CDocs.ui.document_detail')
        
        # Debug the stored version UID
        logger.debug(f"_set_as_current_version called, stored version UID: {getattr(self, '_selected_version_uid', 'None')}")
        
        if not hasattr(self, '_selected_version_uid') or not self._selected_version_uid:
            self.notification_area.object = "**Error:** No version selected"
            return
            
        try:
            # Get version UID
            version_uid = self._selected_version_uid
            logger.debug(f"Setting version {version_uid} as current")
            
            # Show in progress message
            self.notification_area.object = "**Setting as current version...**"
            
            # Use controller to set current version
            from CDocs.controllers.document_controller import set_current_version
            
            result = set_current_version(
                user=self.user,
                document_uid=self.document_uid,
                version_uid=version_uid
            )
            
            if result and result.get('success'):
                self.notification_area.object = "**Success:** Version set as current"
                
                # Reload document to reflect the changes
                self._load_document()
            else:
                error_msg = "Unknown error" 
                if result and 'message' in result:
                    error_msg = result['message']
                self.notification_area.object = f"**Error:** Could not set as current version: {error_msg}"
                
        except Exception as e:
            logger.error(f"Error setting current version: {e}")
            import traceback
            logger.error(traceback.format_exc())
            self.notification_area.object = f"**Error:** {str(e)}"

    def _download_selected_version(self, event):
        """Download the selected document version"""
        logger = logging.getLogger('CDocs.ui.document_detail')
        
        if not hasattr(self, '_selected_version_uid') or not self._selected_version_uid:
            self.notification_area.object = "**Error:** No version selected"
            return
            
        try:
            # Get version UID
            version_uid = self._selected_version_uid
            logger.debug(f"Downloading version: {version_uid}")
            
            # Show download in progress message
            self.notification_area.object = "**Preparing download...**"
            
            # Import the necessary functions
            from CDocs.controllers.document_controller import download_document_version
            from panel.widgets import FileDownload
            
            # Create a callback function that will fetch the file when the download button is clicked
            def get_file_content():
                try:
                    # Get document content with all required parameters
                    doc_content = download_document_version(
                        user=self.user, 
                        document_uid=self.document_uid, 
                        version_uid=version_uid
                    )
                    
                    if isinstance(doc_content, dict) and 'content' in doc_content and doc_content['content']:
                        # Return ONLY the BytesIO object, not a tuple
                        file_name = doc_content.get('file_name', 'document.pdf')
                        return io.BytesIO(doc_content['content'])
                    else:
                        # Handle error
                        self.notification_area.object = "**Error:** Could not download document"
                        return None
                except Exception as e:
                    logger.error(f"Error downloading version: {e}")
                    import traceback
                    logger.error(f"Traceback: {traceback.format_exc()}")
                    self.notification_area.object = f"**Error:** {str(e)}"
                    return None
            
            # Find the version details to get the filename
            file_name = "document.pdf"  # Default
            logger.info("version details: ", self.document)
            for version in self.document.get('versions', []):
                if version.get('UID') == version_uid:
                    file_name = version.get('file_name', file_name)
                    break
            
            # Create download widget with separate filename parameter
            download_widget = FileDownload(
                callback=get_file_content,
                filename=file_name,  # Set filename separately
                button_type="success",
                label=f"Download {file_name}"
            )
            
            # Update the version action area to show the download widget
            if hasattr(self, '_version_action_area'):
                # Find the button row in the action area
                for idx, obj in enumerate(self._version_action_area):
                    if isinstance(obj, pn.Row) and any(isinstance(child, pn.widgets.Button) for child in obj):
                        # Replace existing download button with our FileDownload widget
                        new_row = pn.Row(*[child for child in obj if not (isinstance(child, pn.widgets.Button) 
                                                   and child.name == "Download Version")])
                        new_row.append(download_widget)
                        self._version_action_area[idx] = new_row
                        break
                else:
                    # If no button row found, add the download widget at the end
                    self._version_action_area.append(download_widget)
                    
                self.notification_area.object = "**Ready for download.** Click the download button to save the file."
            else:
                # If no action area exists, show download widget in notification area
                self.notification_area.object = pn.Column(
                    "**Ready for download:**", 
                    download_widget
                )
            
        except Exception as e:
            logger.error(f"Error setting up download: {e}")
            import traceback
            logger.error(f"Traceback: {traceback.format_exc()}")
            self.notification_area.object = f"**Error:** {str(e)}"

Parameters

Name Type Default Kind
bases param.Parameterized -

Parameter Details

bases: Parameter of type param.Parameterized

Return Value

Returns unspecified type

Class Interface

Methods

__init__(self, parent_app)

Purpose: Internal method: init

Parameters:

  • parent_app: Parameter

Returns: None

set_user(self, user)

Purpose: Set the current user.

Parameters:

  • user: Parameter

Returns: None

load_document(self, document_uid, doc_number)

Purpose: Load document by UID or document number.

Parameters:

  • document_uid: Parameter
  • doc_number: Parameter

Returns: None

get_document_view(self)

Purpose: Get the document view for embedding in other panels.

Returns: None

_get_current_user(self) -> DocUser

Purpose: Get the current user from session

Returns: Returns DocUser

_setup_header(self)

Purpose: Set up the header with title and actions

Returns: None

_setup_sidebar(self)

Purpose: Set up the sidebar with document actions

Returns: None

_setup_main_area(self)

Purpose: Set up the main area with document content tabs

Returns: None

_load_document(self, event)

Purpose: Load document data and update the UI.

Parameters:

  • event: Parameter

Returns: None

_extract_document_properties(self)

Purpose: Extract document properties from document data.

Returns: None

load_document_data(self, document_data)

Purpose: Load document directly from document data. Parameters: ----------- document_data : dict The document data to load

Parameters:

  • document_data: Parameter

Returns: None

_setup_document_info(self)

Purpose: Set up document info panel in sidebar

Returns: None

_setup_document_actions(self)

Purpose: Set up document action buttons in sidebar

Returns: None

_create_document_tabs(self)

Purpose: Create tabs for different document content sections

Returns: None

_create_overview_tab(self)

Purpose: Create the overview tab content

Returns: None

_create_versions_tab(self)

Purpose: Create the versions tab content

Returns: None

_create_reviews_tab(self)

Purpose: Create the reviews tab content

Returns: None

_create_approvals_tab(self)

Purpose: Create the approvals tab content

Returns: None

_create_audit_tab(self)

Purpose: Create the audit trail tab content

Returns: None

_create_document_viewer(self)

Purpose: Create a document viewer for the current version

Returns: None

_version_selected(self, event)

Purpose: Handle version selection from table

Parameters:

  • event: Parameter

Returns: None

_review_selected(self, event)

Purpose: Handle review selection from table

Parameters:

  • event: Parameter

Returns: None

_approval_selected(self, event)

Purpose: Handle approval selection from table

Parameters:

  • event: Parameter

Returns: None

_navigate_back(self, event)

Purpose: Navigate back to dashboard

Parameters:

  • event: Parameter

Returns: None

_view_document(self, event)

Purpose: View the current document version

Parameters:

  • event: Parameter

Returns: None

_download_current_version(self, event)

Purpose: Download the current document version

Parameters:

  • event: Parameter

Returns: None

_show_edit_form(self, event)

Purpose: Show form to edit document metadata

Parameters:

  • event: Parameter

Returns: None

_save_document_changes(self, title, description)

Purpose: Save document metadata changes

Parameters:

  • title: Parameter
  • description: Parameter

Returns: None

_show_upload_form(self, event)

Purpose: Show form to upload a new document version

Parameters:

  • event: Parameter

Returns: None

_upload_new_version(self, file_content, file_name, comment)

Purpose: Upload a new document version using FileCloud for storage

Parameters:

  • file_content: Parameter
  • file_name: Parameter
  • comment: Parameter

Returns: None

_format_date(self, date_str)

Purpose: Format date string for display

Parameters:

  • date_str: Parameter

Returns: None

_get_status_color(self, status_code)

Purpose: Get color for document status

Parameters:

  • status_code: Parameter

Returns: None

_show_review_form(self, event)

Purpose: Show form to start a review cycle

Parameters:

  • event: Parameter

Returns: None

_show_approval_form(self, event)

Purpose: Show form to start an approval workflow

Parameters:

  • event: Parameter

Returns: None

_show_publish_form(self, event)

Purpose: Show form to publish the document

Parameters:

  • event: Parameter

Returns: None

_show_archive_form(self, event)

Purpose: Show form to archive the document

Parameters:

  • event: Parameter

Returns: None

_show_clone_form(self, event)

Purpose: Show form to clone the document

Parameters:

  • event: Parameter

Returns: None

_set_as_current_version(self, event)

Purpose: Set the selected version as the current version

Parameters:

  • event: Parameter

Returns: None

_download_selected_version(self, event)

Purpose: Download the selected document version

Parameters:

  • event: Parameter

Returns: None

Required Imports

import logging
import base64
from typing import Dict
from typing import List
from typing import Any

Usage Example

# Example usage:
# result = DocumentDetail(bases)

Similar Components

AI-powered semantic similarity - components with related functionality:

  • class DocumentDetail_v1 98.4% similar

    Document detail view component

    From: /tf/active/vicechatdev/document_detail_old.py
  • class DocumentDetail_v2 97.9% similar

    Document detail view component

    From: /tf/active/vicechatdev/CDocs/ui/document_detail.py
  • function create_document_detail 58.1% similar

    Factory function that creates and initializes a DocumentDetail panel component, optionally loading a specific document by its UID.

    From: /tf/active/vicechatdev/document_detail_backup.py
  • class DocumentDashboard 56.9% similar

    Dashboard for viewing and managing controlled documents.

    From: /tf/active/vicechatdev/CDocs/ui/document_dashboard.py
  • class TrainingManagement 54.1% similar

    UI component for managing document training.

    From: /tf/active/vicechatdev/CDocs/ui/training_management.py
← Back to Browse