🔍 Code Extractor

class DocumentDetail_v2

Maturity: 27

Document detail view component

File:
/tf/active/vicechatdev/CDocs/ui/document_detail.py
Lines:
77 - 5782
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: removed - otheriwse status updates are not happening
                    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
        from CDocs.models.user_extensions import DocUser
        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_uid = get_property(self.document, ['ownerUID', 'owner_uid'])
        self.doc_owner = DocUser(uid=self.doc_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 _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.name 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:
            # Determine which file path to show based on document status
            primary_file_path = None
            secondary_file_path = None
            
            # Get file paths from current version
            word_file_path = self.current_version.get('word_file_path') or self.current_version.get('fileCloudWordPath')
            pdf_file_path = self.current_version.get('pdf_file_path') or self.current_version.get('fileCloudPdfPath')
            
            # Set file paths based on document status
            if is_published_status(status_code):
                primary_file_path = pdf_file_path
                secondary_file_path = word_file_path
                primary_label = "PDF File"
                secondary_label = "Editable File"
            else:
                primary_file_path = word_file_path
                secondary_file_path = pdf_file_path
                primary_label = "Editable File"
                secondary_label = "PDF File"
            
            # Build version info panel
            version_info_items = [
                pn.pane.Markdown(f"### Current Version"),
                pn.pane.Markdown(f"**Version:** {self.current_version.get('version_number', '')}"),
                pn.pane.Markdown(f"**Created by:** {self.current_version.get('created_by_name', '')}"),
                pn.pane.Markdown(f"**Date:** {self._format_date(self.current_version.get('created_date'))}")
            ]
            
            # Add file path information if available
            if primary_file_path:
                # Truncate path for display if it's too long
                display_path = primary_file_path
                if len(display_path) > 40:
                    display_path = "..." + display_path[-40:]
                
                version_info_items.append(pn.pane.Markdown(f"**{primary_label}:** {display_path}"))
                
            if secondary_file_path:
                # Truncate path for display if it's too long
                display_path = secondary_file_path
                if len(display_path) > 40:
                    display_path = "..." + display_path[-40:]
                    
                version_info_items.append(pn.pane.Markdown(f"**{secondary_label}:** {display_path}"))
            
            version_info = pn.Column(
                *version_info_items,
                styles={'background':'#f8f9fa'},
                css_classes=['p-3', 'border', 'rounded', 'mt-3']
            )
            
            self.doc_info_area.append(version_info)
        else:
            self.doc_info_area.append(doc_info)
            self.doc_info_area.append(pn.pane.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', '')
        
        # Add document access controls with permission indicator
        if self.current_version:
            from CDocs.ui.components import DocumentAccessControls
            
            access_controls = DocumentAccessControls(
                document_uid=self.document_uid,
                user_uid=self.user.uid if self.user else None,
                show_access_indicator=True
            )
            
            self.doc_actions_area.append(access_controls.view())
            
            # Add legacy buttons for backward compatibility
            # View/download button always available
            view_btn = pn.widgets.Button(name="Download 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 = pn.widgets.Button(name="Edit Metadata", button_type="warning", 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 = pn.widgets.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)
        
        # Add PDF convert button for editable documents
        if is_editable_status(status) and permissions.user_has_permission(self.user, "CONVERT_DOCUMENT"):
            convert_btn = pn.widgets.Button(name="Convert to PDF", button_type="default", width=200)
            convert_btn.on_click(self._convert_to_pdf)
            self.doc_actions_area.append(convert_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 documents in allowed statuses if user has approval initiation permission
        if status in settings.APPROVAL_ALLOWED_STATUSES 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)
        
        # Training management button
        can_manage_training = (
            permissions.user_has_permission(self.user, "MANAGE_TRAINING") or
            permissions.user_has_permission(self.user, "MANAGE_ALL_TRAINING")
        )
        if can_manage_training:
            training_btn = Button(name="Manage Training", button_type="primary", width=200)
            training_btn.on_click(self._manage_training)
            self.doc_actions_area.append(training_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 _manage_training(self, event=None):
        """Navigate to training management for this document."""
        try:
            if not self.document_uid:
                self.notification_area.object = "**Error:** No document selected"
                return
                
            # Navigate to training management via parent app
            if self.parent_app and hasattr(self.parent_app, 'navigate_to_training_management'):
                self.parent_app.navigate_to_training_management(self.document_uid)
            else:
                self.notification_area.object = "**Error:** Training management not available"
                logger.error("Parent app does not have training management navigation method")
                
        except Exception as e:
            logger.error(f"Error navigating to training management: {e}")
            self.notification_area.object = f"**Error:** {str(e)}"
    
    def _create_document_tabs(self):
        """Create tabs for different document content sections"""
        # Overview tab
        self.overview_tab = self._create_overview_tab()
        
        # Versions tab
        self.versions_tab = self._create_versions_tab()
        
        # Reviews tab
        self.reviews_tab = self._create_reviews_tab()
        
        # Approvals tab
        self.approvals_tab = self._create_approvals_tab()
        
        # Audit trail tab
        self.audit_tab = self._create_audit_tab()
        
        # Add tabs to the tabs container
        self.tabs.extend([
            ('Overview', self.overview_tab),
            ('Versions', self.versions_tab),
            ('Reviews', self.reviews_tab),
            ('Approvals', self.approvals_tab),
            ('Audit Trail', self.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'))
        
        # Add training information
        training_section = None
        try:
            from CDocs.models.training import DocumentTraining
            doc_training = DocumentTraining(document_uid=self.document_uid)
            
            if doc_training._data.get('training_required', False):
                assigned_users = doc_training.get_assigned_users()
                training_info = [
                    "### Training Information",
                    f"**Training Required:** Yes",
                    f"**Validity Period:** {doc_training._data.get('validity_days', 365)} days",
                    f"**Quiz Required:** {'Yes' if doc_training._data.get('quiz_required', False) else 'No'}",
                    f"**Assigned Users:** {len(assigned_users)}",
                ]
                
                if doc_training._data.get('instructions'):
                    training_info.append(f"**Instructions:** {doc_training._data.get('instructions')}")
                
                training_section = Column(
                    Markdown("\n".join(training_info)),
                    styles={'background':'#e7f3ff'},
                    css_classes=['p-3', 'border', 'rounded']
                )
            else:
                training_section = Column(
                    Markdown("### Training Information\n**Training Required:** No"),
                    styles={'background':'#f8f9fa'},
                    css_classes=['p-3', 'border', 'rounded']
                )
                
        except Exception as training_error:
            logger.warning(f"Error loading training info: {training_error}")
            training_section = Column(
                Markdown("### Training Information\n**Training Status:** Error loading training information"),
                styles={'background':'#fff3cd'},
                css_classes=['p-3', 'border', 'rounded']
            )
        
        # 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'
            ),
            training_section if training_section else None,
            Markdown("## Document Preview"),
            preview_pane,
            sizing_mode='stretch_width'
        )
        
        return overview_layout
    
    def _compare_versions(self, versions_table, versions_df):
        """
        Compare two selected document versions using LLM.
        
        Args:
            versions_table: The tabulator widget containing the selections
            versions_df: The DataFrame with version data
        """
        # Get the selected rows
        selected_indices = versions_table.selection
        
        # Ensure we have exactly 2 versions selected
        if len(selected_indices) != 2:
            self.notification_area.object = "**Error:** Please select exactly 2 versions to compare"
            return
        
        # Get the UIDs of the selected versions
        selected_rows = versions_df.iloc[selected_indices]
        version_uids = selected_rows['UID'].tolist()
        version_numbers = selected_rows['Version'].tolist()
        
        # Sort versions by version number to ensure version A is always the older version
        version_pairs = list(zip(version_numbers, version_uids))
        
        # Convert version numbers to float for numeric comparison if possible
        try:
            # Try parsing as numeric versions (e.g., 1.0, 2.0)
            sorted_pairs = sorted(version_pairs, key=lambda x: float(x[0]))
        except ValueError:
            # Fallback to string comparison if not numeric
            sorted_pairs = sorted(version_pairs, key=lambda x: x[0])
        
        # Extract sorted version numbers and UIDs
        sorted_version_numbers = [pair[0] for pair in sorted_pairs]
        sorted_version_uids = [pair[1] for pair in sorted_pairs]
        
        self.notification_area.object = f"**Comparing versions:** {sorted_version_numbers[0]} and {sorted_version_numbers[1]}..."
        
        try:
            # Create comparison service
            from CDocs.utils.version_comparison import VersionComparisonService
            comparison_service = VersionComparisonService()
            
            # Get document contents for both versions - in chronological order
            version1_content = self._get_version_content(sorted_version_uids[0])  # Older version
            version2_content = self._get_version_content(sorted_version_uids[1])  # Newer version
            
            if not version1_content or not version2_content:
                self.notification_area.object = "**Error:** Failed to retrieve document content for comparison"
                return
            
            # Run the comparison with ordered versions
            comparison_result = comparison_service.compare_documents(
                version1_content, 
                version2_content,
                sorted_version_numbers[0],  # Older version number as version A
                sorted_version_numbers[1],  # Newer version number as version B
                document_title=self.document.get('title', 'Document')
            )
            
            # Display results in a modal dialog
            self._show_comparison_results(comparison_result, sorted_version_numbers)
            
        except Exception as e:
            logger.error(f"Error comparing versions: {e}")
            logger.error(traceback.format_exc())
            self.notification_area.object = f"**Error comparing versions:** {str(e)}"
    
    def _get_version_content(self, version_uid):
        """
        Get the content of a specific document version as text.
        
        Args:
            version_uid: UID of the version
            
        Returns:
            Document content as text or None if retrieval fails
        """
        try:
            # Get document and version information
            from CDocs.models.document import DocumentVersion
            from CDocs.controllers.filecloud_controller import download_document_from_filecloud
            
            # Get version details
            version = DocumentVersion(uid=version_uid)
            if not version:
                logger.error(f"Version not found: {version_uid}")
                return None
                
            document_uid = version.document_uid
            if not document_uid:
                logger.error(f"Could not determine document UID for version: {version_uid}")
                return None
            
            # Download the document content
            file_content = download_document_from_filecloud(
                document_uid=document_uid,
                version=version.version_number
            )
            
            # Check if the download was successful
            if not isinstance(file_content, bytes):
                logger.error(f"Failed to download file content for version: {version_uid}")
                return None
                
            # Get file extension to determine processing method
            file_name = version.file_name
            file_ext = file_name.split('.')[-1].lower() if '.' in file_name else ''
            
            # Process based on file type
            if file_ext in ['pdf']:
                # For PDFs, extract text using llmsherpa if available
                return self._extract_text_from_pdf(file_content)
                
            elif file_ext in ['doc', 'docx', 'rtf', 'odt']:
                # Word documents
                return self._extract_text_from_word(file_content, file_ext)
                
            elif file_ext in ['ppt', 'pptx']:
                # PowerPoint documents
                return self._extract_text_from_powerpoint(file_content, file_ext)
                
            elif file_ext in ['xls', 'xlsx']:
                # Excel documents
                return self._extract_text_from_excel(file_content, file_ext)
                
            else:
                # For other file types or fallback, try direct text conversion
                try:
                    # Try UTF-8 first
                    return file_content.decode('utf-8')
                except UnicodeDecodeError:
                    # Fallback to Latin-1 which never fails
                    return file_content.decode('latin-1')
                
        except Exception as e:
            logger.error(f"Error getting version content: {e}")
            logger.error(traceback.format_exc())
            return None
            
        return None
    
    def _extract_text_from_pdf(self, file_content):
        """Extract text from PDF content"""
        try:
            # Save to a temporary file
            with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as temp_file:
                temp_path = temp_file.name
                temp_file.write(file_content)
            
            try:
                # Try using llmsherpa for PDF extraction if available
                from llmsherpa.readers import LayoutPDFReader
                
                # Use the llmsherpa API to extract content
                llmsherpa_api_url = "http://llmsherpa:5001/api/parseDocument?renderFormat=all&useNewIndentParser=yes"
                pdf_reader = LayoutPDFReader(llmsherpa_api_url)
                
                try:
                    doc = pdf_reader.read_pdf(temp_path)
                    text_chunks = []
                    
                    for chunk in doc.chunks():
                        # Add text content from paragraphs
                        if hasattr(chunk, 'to_text'):
                            clean_text = chunk.to_text().replace("- ", "").replace("\n", " ")
                            text_chunks.append(clean_text)
                    
                    # Clean up temp file
                    os.remove(temp_path)
                    
                    # Return combined text
                    return "\n\n".join(text_chunks)
                except:
                    # If llmsherpa fails, fall back to PyPDF2
                    raise ImportError("LLMSherpa failed, falling back to PyPDF2")
                    
            except ImportError:
                # Fallback to PyPDF2 if llmsherpa is not available
                import PyPDF2
                
                with open(temp_path, 'rb') as pdf_file:
                    pdf_reader = PyPDF2.PdfReader(pdf_file)
                    text = []
                    
                    for page_num in range(len(pdf_reader.pages)):
                        page = pdf_reader.pages[page_num]
                        text.append(page.extract_text())
                
                # Clean up temp file
                os.remove(temp_path)
                
                # Return combined text
                return "\n\n".join(text)
                    
        except Exception as e:
            logger.error(f"Error extracting text from PDF: {e}")
            return "Error extracting PDF text: " + str(e)
    
    def _extract_text_from_word(self, file_content, file_ext):
        """Extract text from Word document content"""
        try:
            # Save to a temporary file
            with tempfile.NamedTemporaryFile(suffix=f'.{file_ext}', delete=False) as temp_file:
                temp_path = temp_file.name
                temp_file.write(file_content)
            
            try:
                # Try using the python-docx library for .docx files
                if file_ext == 'docx':
                    import docx
                    doc = docx.Document(temp_path)
                    text = []
                    
                    # Extract text from paragraphs
                    for para in doc.paragraphs:
                        text.append(para.text)
                    
                    # Extract text from tables
                    for table in doc.tables:
                        for row in table.rows:
                            row_text = []
                            for cell in row.cells:
                                row_text.append(cell.text)
                            text.append(" | ".join(row_text))
                    
                    # Clean up temp file
                    os.remove(temp_path)
                    
                    # Return combined text
                    return "\n\n".join(text)
                else:
                    # For other Word formats, convert to PDF first, then extract text
                    pdf_path = self._convert_to_pdf(temp_path)
                    if pdf_path:
                        with open(pdf_path, 'rb') as pdf_file:
                            pdf_content = pdf_file.read()
                        
                        # Clean up temporary files
                        os.remove(temp_path)
                        os.remove(pdf_path)
                        
                        # Extract text from the PDF
                        return self._extract_text_from_pdf(pdf_content)
                    else:
                        raise Exception(f"Failed to convert {file_ext} file to PDF")
                    
            except Exception as doc_err:
                logger.error(f"Error with python-docx: {doc_err}")
                # Fall back to conversion to PDF
                pdf_path = self._convert_to_pdf(temp_path)
                if pdf_path:
                    with open(pdf_path, 'rb') as pdf_file:
                        pdf_content = pdf_file.read()
                    
                    # Clean up temporary files
                    os.remove(temp_path)
                    os.remove(pdf_path)
                    
                    # Extract text from the PDF
                    return self._extract_text_from_pdf(pdf_content)
                else:
                    raise Exception(f"Failed to convert {file_ext} file to PDF")
                    
        except Exception as e:
            logger.error(f"Error extracting text from Word document: {e}")
            return "Error extracting Word document text: " + str(e)
    
    def _extract_text_from_powerpoint(self, file_content, file_ext):
        """Extract text from PowerPoint content"""
        try:
            # Save to a temporary file
            with tempfile.NamedTemporaryFile(suffix=f'.{file_ext}', delete=False) as temp_file:
                temp_path = temp_file.name
                temp_file.write(file_content)
            
            try:
                # Try using the python-pptx library for .pptx files
                if file_ext == 'pptx':
                    import pptx
                    presentation = pptx.Presentation(temp_path)
                    text = []
                    
                    # Process each slide
                    for i, slide in enumerate(presentation.slides):
                        slide_text = []
                        slide_text.append(f"Slide {i+1}")
                        
                        # Get title if available
                        if slide.shapes.title and slide.shapes.title.text:
                            slide_text.append(slide.shapes.title.text)
                        
                        # Extract text from all shapes
                        for shape in slide.shapes:
                            if hasattr(shape, "text") and shape.text:
                                slide_text.append(shape.text)
                        
                        text.append("\n".join(slide_text))
                    
                    # Clean up temp file
                    os.remove(temp_path)
                    
                    # Return combined text
                    return "\n\n".join(text)
                else:
                    # For other PowerPoint formats, convert to PDF first, then extract text
                    pdf_path = self._convert_to_pdf(temp_path)
                    if pdf_path:
                        with open(pdf_path, 'rb') as pdf_file:
                            pdf_content = pdf_file.read()
                        
                        # Clean up temporary files
                        os.remove(temp_path)
                        os.remove(pdf_path)
                        
                        # Extract text from the PDF
                        return self._extract_text_from_pdf(pdf_content)
                    else:
                        raise Exception(f"Failed to convert {file_ext} file to PDF")
                    
            except Exception as ppt_err:
                logger.error(f"Error with python-pptx: {ppt_err}")
                # Fall back to conversion to PDF
                pdf_path = self._convert_to_pdf(temp_path)
                if pdf_path:
                    with open(pdf_path, 'rb') as pdf_file:
                        pdf_content = pdf_file.read()
                    
                    # Clean up temporary files
                    os.remove(temp_path)
                    os.remove(pdf_path)
                    
                    # Extract text from the PDF
                    return self._extract_text_from_pdf(pdf_content)
                else:
                    raise Exception(f"Failed to convert {file_ext} file to PDF")
                    
        except Exception as e:
            logger.error(f"Error extracting text from PowerPoint: {e}")
            return "Error extracting PowerPoint text: " + str(e)
    
    def _extract_text_from_excel(self, file_content, file_ext):
        """Extract text from Excel content"""
        try:
            # Save to a temporary file
            with tempfile.NamedTemporaryFile(suffix=f'.{file_ext}', delete=False) as temp_file:
                temp_path = temp_file.name
                temp_file.write(file_content)
            
            try:
                # Try using pandas for Excel files
                import pandas as pd
                
                excel_file = pd.ExcelFile(temp_path)
                text = []
                
                # Process each sheet
                for sheet_name in excel_file.sheet_names:
                    df = pd.read_excel(excel_file, sheet_name=sheet_name)
                    
                    # Skip empty sheets
                    if df.empty:
                        continue
                    
                    # Add sheet name as header
                    text.append(f"Sheet: {sheet_name}")
                    
                    # Convert to markdown table
                    markdown_table = df.to_markdown(index=False)
                    text.append(markdown_table)
                
                # Clean up temp file
                os.remove(temp_path)
                
                # Return combined text
                return "\n\n".join(text)
                    
            except Exception as excel_err:
                logger.error(f"Error with pandas: {excel_err}")
                
                # Fall back to conversion to PDF
                pdf_path = self._convert_to_pdf(temp_path)
                if pdf_path:
                    with open(pdf_path, 'rb') as pdf_file:
                        pdf_content = pdf_file.read()
                    
                    # Clean up temporary files
                    os.remove(temp_path)
                    os.remove(pdf_path)
                    
                    # Extract text from the PDF
                    return self._extract_text_from_pdf(pdf_content)
                else:
                    raise Exception(f"Failed to convert {file_ext} file to PDF")
                    
        except Exception as e:
            logger.error(f"Error extracting text from Excel: {e}")
            return "Error extracting Excel text: " + str(e)
    
    def _convert_to_pdf(self, input_file):
        """Convert a document to PDF using LibreOffice"""
        try:
            output_pdf = os.path.splitext(input_file)[0] + ".pdf"
            
            # Use LibreOffice for conversion
            cmd = [
                'libreoffice',
                '--headless',
                '--norestore',
                '--convert-to', 'pdf',
                '--outdir', os.path.dirname(input_file),
                input_file
            ]
            
            # Run with timeout
            process = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=60  # 1 minute timeout
            )
            
            # Check if there was an error
            if process.returncode != 0:
                logger.error(f"LibreOffice error: {process.stderr}")
                return None
            
            # Verify the file was created
            if os.path.exists(output_pdf):
                return output_pdf
            else:
                # Check for other PDFs that might have been created
                potential_pdfs = list(Path(os.path.dirname(input_file)).glob(f"{os.path.basename(os.path.splitext(input_file)[0])}*.pdf"))
                if potential_pdfs:
                    return str(potential_pdfs[0])
            
            return None
        except Exception as e:
            logger.error(f"Error converting to PDF: {e}")
            return None
    
    def _show_comparison_results(self, comparison_result, version_numbers):
        """
        Display comparison results in a custom modal dialog using FloatPanel for Panel 1.6.1 compatibility.
        
        Args:
            comparison_result: Results from the comparison
            version_numbers: Version numbers that were compared
        """
        try:
            # First check if there's already a floatpanel open and clear it
            try:
                self.floatpanel_comparison.clear()
                self.main_content.pop(-1)  # Remove the existing floatpanel from workspace
            except:
                # Create a new floatpanel if it doesn't exist or couldn't be cleared
                config = {"headerControls": {"maximize": "remove", "close": "remove"}}
                self.floatpanel_comparison = pn.layout.FloatPanel(
                    name='Version Comparison', 
                    contained=False, 
                    margin=20,
                    height=800,
                    width=1000,
                    position="center",
                    config=config
                )
            
            # Create the title text
            title_text = f"Version Comparison: {version_numbers[0]} vs {version_numbers[1]}"
            
            # Create close button 
            close_btn = pn.widgets.Button(name="Close", button_type="default")
            close_btn.window = "self.main_content"
            close_btn.floater = "self.floatpanel_comparison"
            close_btn.on_click(self.floater_close)
            
            # Create the content for the comparison dialog
            comparison_content = pn.Column(
                pn.pane.Markdown(f"# {title_text}"),
                pn.pane.Markdown(comparison_result),
                #pn.Row(pn.layout.HSpacer(), close_btn, align='end'),
                sizing_mode='stretch_width',
                scroll=True,
                height=700,
                css_classes=['p-4']
            )
            
            # Add content to the floatpanel
            self.floatpanel_comparison.append(close_btn)
            self.floatpanel_comparison.append(comparison_content)
            
            # Add the panel to the workspace
            self.main_content.append(self.floatpanel_comparison)
            
            # Update notification
            self.notification_area.object = f"**Comparison complete** between versions {version_numbers[0]} and {version_numbers[1]}"
            
        except Exception as e:
            logger.error(f"Error showing comparison results: {e}")
            logger.error(traceback.format_exc())
            
            # Fallback: Display results inline
            self.notification_area.object = f"""
            **Comparison complete** between versions {version_numbers[0]} and {version_numbers[1]}
            
            Unable to display comparison dialog. Results shown below:
            
            <details>
            <summary>Click to view comparison results</summary>
            <div style="max-height: 400px; overflow-y: auto; border: 1px solid #ddd; padding: 10px;">
            {comparison_result}
            </div>
            </details>
            """
    
    def floater_close(self, event):
        """
        Close a floating panel
        
        Args:
            event: The event object containing the window and floater properties
        """
        try:
            # Get the window and floater from the event object
            window = event.obj.window
            floater = event.obj.floater
            
            # Remove the floater from the window
            eval(window+".pop(-1)")
        except Exception as e:
            logger.error(f"Error closing float panel: {e}")
            logger.error(traceback.format_exc())
    
    
    def _create_versions_tab(self):
        """Create a tab for document versions, with version selection and comparison functionality"""
        
        # Get all versions
        versions = self.document.get('versions', [])
        
        # If no versions, show message
        if not versions:
            return pn.Column(
                pn.pane.Markdown("# Document Versions"),
                pn.pane.Markdown("No versions available for this document."),
                sizing_mode='stretch_width'
            )
        
        # Filter out archived versions except for the current version
        current_version_uid = None
        if self.current_version:
            current_version_uid = self.current_version.get('UID')
        
        filtered_versions = []
        for version in versions:
            # Include if not archived or if it's the current version
            if version.get('status') != 'ARCHIVED' or version.get('UID') == current_version_uid:
                filtered_versions.append(version)
        
        # Create DataFrame for tabulator
        try:
            versions_df = pd.DataFrame(filtered_versions)
            
            # Handle different column name cases
            version_col = next((col for col in versions_df.columns if col.lower() in ['version_number', 'versionnumber']), None)
            if version_col:
                versions_df = versions_df.rename(columns={version_col: 'Version'})
            
            created_col = next((col for col in versions_df.columns if col.lower() in ['created_date', 'createddate']), None)
            if created_col:
                versions_df = versions_df.rename(columns={created_col: 'Created'})
                # Format date
                try:
                    versions_df['Created'] = pd.to_datetime(versions_df['Created']).dt.strftime('%Y-%m-%d')
                except:
                    pass
            
            # Add file name if available
            file_name_col = next((col for col in versions_df.columns if col.lower() in ['file_name', 'filename']), None)
            if file_name_col:
                versions_df = versions_df.rename(columns={file_name_col: 'File Name'})
            
            # Add status column if available
            status_col = next((col for col in versions_df.columns if col.lower() == 'status'), None)
            if status_col:
                versions_df = versions_df.rename(columns={status_col: 'Status'})
            
            # Add 'Current' column
            versions_df['Current'] = ''
            if current_version_uid:
                versions_df.loc[versions_df['UID'] == current_version_uid, 'Current'] = '✓'
            
            # Add selection column
            versions_df['Selected'] = False
            
            # Add action column
            versions_df['Action'] = 'View'
            
            # Select and reorder columns - keep UID but don't display it
            columns_to_use = ['UID', 'Selected', 'Version', 'Created', 'Current']
            
            # Add File Name if available
            if 'File Name' in versions_df.columns:
                columns_to_use.append('File Name')
            
            # Add Status if available
            if 'Status' in versions_df.columns:
                columns_to_use.append('Status')
            
            # Add Action column last
            columns_to_use.append('Action')
            
            # Use only columns that exist in the DataFrame
            columns_to_use = [col for col in columns_to_use if col in versions_df.columns]
            
            # Create versions table with selection column
            versions_table = pn.widgets.Tabulator(
                versions_df[columns_to_use],
                pagination='local',
                page_size=10,
                sizing_mode='stretch_width',
                hidden_columns=['UID'],
                selectable='checkbox',  # Use checkbox for clearer multi-selection
                show_index=False
            )
            
            # Add version selection handler
            versions_table.on_click(self._version_selected)
            
            # Create compare button (initially disabled)
            compare_btn = pn.widgets.Button(
                name="Compare Selected Versions",
                button_type="primary",
                width=200,
                disabled=True
            )
            
            # Handler to enable/disable compare button based on selection
            def update_compare_button(event):
                # Get the selected indices - in Panel 1.6.1, this is available directly
                # Make sure we handle both older and newer Panel versions
                selected_indices = []
                
                if hasattr(versions_table, 'selection') and versions_table.selection is not None:
                    if isinstance(versions_table.selection, list):
                        selected_indices = versions_table.selection
                    else:
                        # Handle case where selection is a string or other type
                        try:
                            selected_indices = [int(idx) for idx in str(versions_table.selection).split(',') if idx.strip()]
                        except:
                            selected_indices = []
                
                # Enable button only when exactly 2 versions are selected
                compare_btn.disabled = len(selected_indices) != 2
                
            # Use the param.watch to monitor the selection parameter
            versions_table.param.watch(update_compare_button, 'selection')
            
            # Add compare button handler
            compare_btn.on_click(lambda event: self._compare_versions(versions_table, versions_df))
            
            # Create version action area where we'll show details of selected version
            self._version_action_area = pn.Column(
                pn.pane.Markdown("## Version Details"),
                pn.pane.Markdown("*Select a version to view details*"),
                sizing_mode='stretch_width',
                styles={'background':'#f8f9fa'},
                css_classes=['p-3', 'border', 'rounded']
            )
            
            # Layout
            versions_layout = pn.Column(
                pn.pane.Markdown("# Document Versions"),
                pn.Row(compare_btn, align='end', sizing_mode='stretch_width'),
                versions_table,
                self._version_action_area,
                sizing_mode='stretch_width'
            )
            
            return versions_layout
            
        except Exception as df_error:
            logger.error(f"Error creating versions DataFrame: {df_error}")
            return pn.Column(
                pn.pane.Markdown("# Document Versions"),
                pn.pane.Markdown(f"Error creating versions table: {str(df_error)}"),
                sizing_mode='stretch_width'
            )

    def _edit_document_online(self, event=None):
        """Get edit URL from FileCloud and open it"""
        # Show loading message
        self.notification_area.object = "**Getting edit URL...**"
        
        try:
            # Import the document controller function
            from CDocs.controllers.document_controller import get_document_edit_url
            
            # Call API to get edit URL
            result = get_document_edit_url(
                user=self.user,
                document_uid=self.document_uid
            )
            
            if result.get('success'):
                edit_url = result.get('edit_url')
                # Create a clickable link to open the edit URL
                self.notification_area.object = f"""
                **Document ready for editing!**
                
                Click this link to edit the document online: 
                [Open in FileCloud Editor]({edit_url})
                
                """
            else:
                self.notification_area.object = f"**Error:** {result.get('message', 'Unable to get edit URL')}"
        
        except Exception as e:
            import traceback
            logger.error(f"Error getting edit URL: {e}")
            logger.error(traceback.format_exc())
            self.notification_area.object = f"**Error getting edit URL:** {str(e)}"

    def _convert_to_pdf(self, event=None):
        """Convert the current document version to PDF"""
        # Show loading message
        self.notification_area.object = "**Converting document to PDF...**"
        
        try:
            # Import the document controller function
            from CDocs.controllers.document_controller import convert_document_to_pdf
            
            # Call API to convert document - FIX: Pass the user object
            result = convert_document_to_pdf(
                user=self.user,  # Pass the user object
                document_uid=self.document_uid
            )
            
            if result.get('success'):
                # Show success message with PDF path
                message = f"""
                **Document converted to PDF successfully!**
                
                PDF path: {result.get('pdf_path')}
                
                Reload this page to see the updated document.
                """
                
                # Create a new notification with both message and button
                notification_column = pn.Column(
                    pn.pane.Markdown(message),
                    pn.widgets.Button(name="Reload Document", button_type="primary", on_click=self._load_document)
                )
                
                # Replace the notification area with our column
                for i, item in enumerate(self.main_content):
                    if item is self.notification_area:
                        self.main_content[i] = notification_column
                        break
                
                if not any(item is notification_column for item in self.main_content):
                    # If we couldn't find and replace, just update the message
                    self.notification_area.object = message
            else:
                self.notification_area.object = f"**Error:** {result.get('message', 'Failed to convert document')}"
        
        except Exception as e:
            import traceback
            logger.error(f"Error converting document to PDF: {e}")
            logger.error(traceback.format_exc())
            self.notification_area.object = f"**Error converting document:** {str(e)}"
    
    def _create_reviews_tab(self):
        """Create the reviews tab content, hiding reviews for archived versions except current one"""
        # 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 = pn.widgets.Button(
                    name="Start Review", 
                    button_type="primary", 
                    width=150
                )
                start_review_btn.on_click(self._show_review_form)
                
                return pn.Column(
                    pn.pane.Markdown("# Document Reviews"),
                    pn.pane.Markdown("*No review cycles found for this document*"),
                    start_review_btn,
                    sizing_mode='stretch_width'
                )
            else:
                return pn.Column(
                    pn.pane.Markdown("# Document Reviews"),
                    pn.pane.Markdown("*No review cycles found for this document*"),
                    sizing_mode='stretch_width'
                )
        
        # Get current version UID for filtering
        current_version_uid = None
        if self.current_version:
            current_version_uid = self.current_version.get('UID')
        
        # Filter review cycles to exclude those for archived versions (except current version)
        filtered_review_cycles = []
        for cycle in review_cycles:
            version_uid = cycle.get('version_uid')
            # Include if it's for the current version or a non-archived version
            if version_uid == current_version_uid or not self._is_archived_version(version_uid):
                filtered_review_cycles.append(cycle)
        
        # Convert to DataFrame for tabulator
        try:
            # Create a clean list of dictionaries for the DataFrame
            reviews_data = []
            for cycle in filtered_review_cycles:
                # Convert Neo4j DateTime objects to Python datetime objects or strings
                cycle_dict = {}
                for key, value in cycle.items():
                    # Convert Neo4j DateTime objects to strings
                    if hasattr(value, '__class__') and value.__class__.__name__ == 'DateTime':
                        # Convert Neo4j DateTime to string in ISO format
                        try:
                            cycle_dict[key] = value.iso_format()[:10]  # Just get the YYYY-MM-DD part
                        except (AttributeError, TypeError):
                            cycle_dict[key] = str(value)
                    else:
                        cycle_dict[key] = value
                
                reviews_data.append(cycle_dict)
            
            # Create DataFrame (safely handle empty data)
            if not reviews_data:
                return pn.Column(
                    pn.pane.Markdown("# Document Reviews"),
                    pn.pane.Markdown("*No reviews found for non-archived versions*"),
                    sizing_mode='stretch_width'
                )
            
            reviews_df = pd.DataFrame(reviews_data)
        except Exception as df_error:
            logger.error(f"Error creating reviews DataFrame: {df_error}")
            logger.error(f"Traceback: {traceback.format_exc()}")
            return pn.Column(
                pn.pane.Markdown("# Document Reviews"),
                pn.pane.Markdown(f"*Error formatting review cycle data: {str(df_error)}*"),
                sizing_mode='stretch_width'
            )
        
        # Format dates - no need to use pd.to_datetime since we already formatted them
        date_columns = ['start_date', 'startDate', 'due_date', 'dueDate', 'completed_date', 'completionDate']
        for col in date_columns:
            if col in reviews_df.columns:
                # Instead of using pd.to_datetime, ensure the column contains strings
                reviews_df[col] = reviews_df[col].astype(str)
        
        # Select and rename columns for display
        display_columns = ['UID', 'cycle_number', 'status', 'initiated_by_name', 'startDate', 'dueDate', 'completionDate']
        column_names = {
            'UID': 'UID',
            'cycle_number': 'Cycle #',
            'status': 'Status',
            'initiated_by_name': 'Initiated By',
            'startDate': 'Started',
            'dueDate': 'Due',
            'completionDate': '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,
            hidden_columns=['UID']
        )
        
        # 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 _is_archived_version(self, version_uid):
        """Check if a version is archived"""
        if not version_uid:
            return False
            
        versions = self.document.get('versions', [])
        for version in versions:
            if version.get('UID') == version_uid and version.get('status') == 'ARCHIVED':
                return True
                
        return False
    
    def _review_selected(self, event):
        """Handle review selection from table with support for both selection and cell click events"""
        logger = logging.getLogger('CDocs.ui.document_detail')
        
        try:
            # Clear any previous notifications
            self.notification_area.object = ""
            
            # Debug the event type
            logger.debug(f"Review selection event type: {type(event).__name__}")
            
            # Handle different event types
            row_index = None
            review_uid = None
            row_data = None
            
            # Check if this is a CellClickEvent
            if hasattr(event, 'row') and event.row is not None:
                # This is a CellClickEvent
                row_index = event.row
                logger.debug(f"Cell click event on row {row_index}")
                
                # For CellClickEvent, extract data from event.model.source
                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())}")
                    
                    # Try to find review_uid directly in source data
                    uid_keys = ['review_uid', '_review_uid', 'UID', 'uid']
                    for key in uid_keys:
                        if key in source_data and len(source_data[key]) > row_index:
                            review_uid = source_data[key][row_index]
                            logger.debug(f"Found review_uid using key '{key}': {review_uid}")
                            break
                            
                    # Create row data dictionary
                    row_data = {col: values[row_index] for col, values in source_data.items() if len(values) > row_index}
                    
                    # If still no UID found, check through all columns that might contain UID
                    if not review_uid:
                        for key in source_data.keys():
                            if '_uid' in key.lower() and len(source_data[key]) > row_index:
                                review_uid = source_data[key][row_index]
                                logger.debug(f"Found review_uid from column '{key}': {review_uid}")
                                break
                
            # Handle TabSelector selection event
            elif hasattr(event, 'new') and event.new is not None:
                row_index = event.new[0] if isinstance(event.new, list) else event.new
                logger.debug(f"Selection event with index {row_index}")
                
                # Get DataFrame
                if hasattr(event, 'obj') and hasattr(event.obj, 'value'):
                    df = event.obj.value
                    logger.debug(f"DataFrame columns: {list(df.columns)}")
                    
                    if row_index < len(df):
                        row_data = df.iloc[row_index].to_dict()
                        
                        # Try different common column names for the UID
                        uid_columns = ['review_uid', '_review_uid', 'UID', 'uid']
                        for col in uid_columns:
                            if col in df.columns:
                                review_uid = df.iloc[row_index][col]
                                logger.debug(f"Found review_uid in column '{col}': {review_uid}")
                                break
                                
                        # If still no UID found, check through all columns that might contain UID
                        if not review_uid:
                            for col in df.columns:
                                if '_uid' in col.lower():
                                    review_uid = df.iloc[row_index][col]
                                    logger.debug(f"Found review_uid from column '{col}': {review_uid}")
                                    break
            
            # Last resort - check if the event itself has a UID attribute or value
            if not review_uid:
                if hasattr(event, 'uid'):
                    review_uid = event.uid
                elif hasattr(event, 'value') and isinstance(event.value, str) and len(event.value) > 20:
                    # Possibly a UID string
                    review_uid = event.value
            
            if not review_uid:
                logger.warning("Could not determine review UID from event")
                self.notification_area.object = "**Error:** Couldn't determine review ID"
                return
            
            logger.info(f"Loading review with UID: {review_uid}")
            
            # Load review details
            from CDocs.controllers.review_controller import get_review_cycle
            review_data = get_review_cycle(review_uid, include_document=True, include_comments=True)
            
            if not review_data:
                self.notification_area.object = "**Error:** Review not found"
                return
            
            review_data = self._convert_neo4j_datetimes(review_data)
                
            # Extract data
            status = review_data.get('status', '')
            review_type = review_data.get('review_type', '')
            initiated_date = self._format_date(review_data.get('initiated_date', ''))
            due_date = self._format_date(review_data.get('due_date', ''))
            initiated_by = review_data.get('initiated_by_name', '')
            instructions = review_data.get('instructions', '')
            
            # Get reviewer data
            reviewer_assignments = review_data.get('reviewer_assignments', [])
            
            # Convert to DataFrame for tabulator
            reviewer_data = []
            for r in reviewer_assignments:
                reviewer_data.append({
                    'reviewer': r.get('reviewer_name', ''),
                    'status': r.get('status', ''),
                    'decision': r.get('decision', ''),
                    'assigned_date': self._format_date(r.get('assigned_date', '')),
                    'decision_date': self._format_date(r.get('decision_date', ''))
                })
            
            reviewers_df = pd.DataFrame(reviewer_data)
            
            # Get comments
            comments = review_data.get('comments', [])
            
            # Create details panel
            details_panel = pn.Column(
                pn.pane.Markdown(f"## Review Cycle Details"),
                pn.pane.Markdown(f"**Status:** {status}"),
                pn.pane.Markdown(f"**Type:** {review_type}"),
                pn.pane.Markdown(f"**Started:** {initiated_date}"),
                pn.pane.Markdown(f"**Due Date:** {due_date}"),
                pn.pane.Markdown(f"**Initiated By:** {initiated_by}"),
                sizing_mode='stretch_width',
                styles={'background':'#f8f9fa'},
                css_classes=['p-3', 'border', 'rounded']
            )
            
            # Add instructions if available
            if instructions:
                details_panel.append(pn.pane.Markdown("### Instructions"))
                details_panel.append(pn.pane.Markdown(instructions))
            
            # Add reviewers table
            details_panel.append(pn.pane.Markdown("### Reviewers"))
            details_panel.append(pn.widgets.Tabulator(
                reviewers_df,
                pagination='local',
                page_size=5,
                sizing_mode='stretch_width',
                height=200
            ))
            
            # Add comments section
            details_panel.append(pn.pane.Markdown("### Comments"))
            if comments:
                comments_html = "<div class='comments-container'>"
                for comment in comments:
                    user_name = comment.get('user_name', 'Unknown')
                    timestamp = self._format_date(comment.get('timestamp', ''))
                    text = comment.get('text', '')
                    section = comment.get('section', '')
                    
                    comments_html += f"""
                    <div class='comment p-2 mb-2 border rounded'>
                        <div><strong>{user_name}</strong> <span class='text-muted'>on {timestamp}</span></div>
                        {f"<div><strong>Section:</strong> {section}</div>" if section else ""}
                        <div>{text}</div>
                    </div>
                    """
                comments_html += "</div>"
                details_panel.append(pn.pane.HTML(comments_html))
            else:
                details_panel.append(pn.pane.Markdown("*No comments yet*"))
            
            # Add actions section for document owner or review initiator
            if status == 'COMPLETED' and self.user:
                # Check if user is document owner, review initiator, or has manage permission
                is_document_owner = self.doc_owner_uid == self.user.uid
                is_review_initiator = review_data.get('initiated_by_uid') == self.user.uid
                has_manage_permission = permissions.user_has_permission(self.user, "MANAGE_REVIEWS")
                document_status = review_data.get("document", {}).get("status", "")
                
                # Only show the close review section if the document is still IN_REVIEW
                if document_status == "IN_REVIEW" and (is_document_owner or is_review_initiator or has_manage_permission):
                    logger.info("adding close review section")
                    # Create close review section
                    close_review_section = pn.Column(
                        pn.pane.Markdown("## Close Review Cycle"),
                        pn.pane.Markdown("The review cycle is completed. You can now close it and update the document status."),
                        sizing_mode='stretch_width'
                    )
                    
                    # Create status dropdown
                    status_select = pn.widgets.Select(
                        name="Update Document Status To",
                        options=['DRAFT', 'APPROVED'],
                        value='DRAFT',
                        width=200
                    )
                    
                    # Create checkbox for updating status
                    update_status_checkbox = pn.widgets.Checkbox(
                        name="Update Document Status",
                        value=True,
                        width=200
                    )
                    
                    # Create close button
                    close_btn = pn.widgets.Button(
                        name="Close Review Cycle",
                        button_type="primary",
                        width=150
                    )
                    
                    # Create form layout
                    close_form = pn.Column(
                        pn.Row(
                            pn.Column(update_status_checkbox, status_select),
                            pn.layout.HSpacer(),
                            pn.Column(close_btn, align='end')
                        ),
                        styles={'background':'#f8f9fa'},
                        css_classes=['p-3', 'border', 'rounded'],
                        sizing_mode='stretch_width'
                    )
                    
                    # Add to section
                    close_review_section.append(close_form)
                    
                    # Add handler
                    from functools import partial
                    close_btn.on_click(partial(self._close_review_cycle, 
                                            review_uid=review_uid, 
                                            update_status_checkbox=update_status_checkbox, 
                                            status_select=status_select))
                    
                    # Add section to details panel
                    details_panel.append(close_review_section)
            
            # Check if this is an active review and user has permission to manage reviews
            if status in ['PENDING', 'IN_PROGRESS'] and permissions.user_has_permission(self.user, "MANAGE_REVIEWS"):
                # Add admin actions section
                admin_actions = pn.Column(
                    pn.pane.Markdown("## Review Management"),
                    pn.Row(
                        pn.widgets.Button(name="Add Reviewer", button_type="default", width=120,
                                        on_click=lambda e: self._show_add_reviewer_form(review_uid)),
                        pn.widgets.Button(name="Extend Deadline", button_type="default", width=120,
                                        on_click=lambda e: self._show_extend_review_deadline_form(review_uid)),
                        pn.widgets.Button(name="Cancel Review", button_type="danger", width=120,
                                        on_click=lambda e: self._show_cancel_review_form(review_uid)),
                        sizing_mode='stretch_width'
                    ),
                    sizing_mode='stretch_width',
                    styles={'background':'#f8f9fa'},
                    css_classes=['p-3', 'border', 'rounded']
                )
                
                details_panel.append(admin_actions)
            
            #logger.info(f"ready to update review details with {details_panel}")
            # Update the review details area
            #logger.info("available tabs: %s", self.tabs)
            details_area = self.reviews_tab
            logger.info(f"Details area found: {details_area}")
            if details_area:
                # Find if there's an existing review details area
                review_details_found = False
                for i, item in enumerate(details_area):
                    if isinstance(item, pn.Column) and (
                        (hasattr(item, 'name') and item.name == 'review_details') or
                        (len(item) >= 1 and isinstance(item[0], pn.pane.Markdown) and "Review Details" in item[0].object)
                    ):
                        # Replace existing review details area
                        details_area[i] = details_panel
                        #details_panel.name = 'review_details'
                        review_details_found = True
                        break
                
                # If not found, append it
                if not review_details_found:
                    #details_panel.name = 'review_details'
                    details_area.append(details_panel)
            else:
                # Log error when Reviews tab not found
                logger.error("Could not find Reviews tab to update review details")
        
        except Exception as e:
            self.notification_area.object = f"**Error:** {str(e)}"
            logger.error(f"Error in _review_selected: {e}")
            logger.error(f"Traceback: {traceback.format_exc()}")
    
    def _close_review_cycle(self, event, review_uid, update_status_checkbox, status_select):
        """Close a review cycle and optionally update document status"""
        try:
            self.notification_area.object = "Closing review cycle..."
            
            # Call controller to close review cycle
            from CDocs.controllers.review_controller import close_review_cycle
            result = close_review_cycle(
                user=self.user,
                review_uid=review_uid,
                update_document_status=update_status_checkbox.value,
                target_status=status_select.value
            )
            
            if result['success']:
                self.notification_area.object = "**Success:** Review cycle closed successfully"
                
                # If status was updated, show additional message
                if update_status_checkbox.value:
                    self.notification_area.object += f"<br>Document status updated to {status_select.value}"
                    
                # Reload the document after a short delay to reflect changes
                time.sleep(1)
                self._load_document()
            else:
                self.notification_area.object = f"**Error:** {result.get('message', 'Unknown error')}"
        
        except ResourceNotFoundError as e:
            self.notification_area.object = f"**Error:** {str(e)}"
        except ValidationError as e:
            self.notification_area.object = f"**Validation Error:** {str(e)}"
        except PermissionError as e:
            self.notification_area.object = f"**Permission Error:** {str(e)}"
        except BusinessRuleError as e:
            self.notification_area.object = f"**Business Rule Error:** {str(e)}"
        except Exception as e:
            logger.error(f"Error closing review cycle: {e}")
            logger.error(traceback.format_exc())
            self.notification_area.object = f"**Error:** An unexpected error occurred"

    def _close_approval_cycle(self, event, approval_uid, update_status_checkbox, status_select):
        """Close a review cycle and optionally update document status"""
        try:
            self.notification_area.object = "Closing approval cycle..."
            
            # Call controller to close approval cycle
            from CDocs.controllers.approval_controller import close_approval_cycle
            result = close_approval_cycle(
                user=self.user,
                approval_uid=approval_uid,
                update_document_status=update_status_checkbox.value,
                target_status=status_select.value
            )
            
            if result['success']:
                self.notification_area.object = "**Success:** Approval cycle closed successfully"
                
                # If status was updated, show additional message
                if update_status_checkbox.value:
                    self.notification_area.object += f"<br>Document status updated to {status_select.value}"
                    
                # Reload the document after a short delay to reflect changes
                time.sleep(1)
                self._load_document()
            else:
                self.notification_area.object = f"**Error:** {result.get('message', 'Unknown error')}"
        
        except ResourceNotFoundError as e:
            self.notification_area.object = f"**Error:** {str(e)}"
        except ValidationError as e:
            self.notification_area.object = f"**Validation Error:** {str(e)}"
        except PermissionError as e:
            self.notification_area.object = f"**Permission Error:** {str(e)}"
        except BusinessRuleError as e:
            self.notification_area.object = f"**Business Rule Error:** {str(e)}"
        except Exception as e:
            logger.error(f"Error closing approval cycle: {e}")
            logger.error(traceback.format_exc())
            self.notification_area.object = f"**Error:** An unexpected error occurred"

    def _show_add_approver_form(self, approval_uid):
        """Show form to add an approver to an active approval cycle"""
        try:
            # Store the approval UID for later use
            self._selected_approval_uid = approval_uid
            # Get current approval cycle
            from CDocs.controllers.approval_controller import get_approval_cycle
            approval_data = get_approval_cycle(approval_uid)
            
            if not approval_data:
                self.notification_area.object = "**Error:** Could not retrieve approval cycle details"
                return
                
            # Get potential approvers (exclude current approvers)
            from CDocs.models.user_extensions import DocUser
            try:
                potential_approvers = DocUser.get_users_by_role(role="APPROVER")
                
                if not potential_approvers:
                    # If no users with explicit APPROVER role, get all users who can approve
                    potential_approvers = DocUser.get_users_with_permission("COMPLETE_APPROVAL")
    
                # Get current approver UIDs to exclude
                current_approver_uids = set()
                for approver in approval_data.get('approver_assignments', []):
                    approver_uid = approver.get('approver_uid')
                    if approver_uid:
                        current_approver_uids.add(approver_uid)
                
                # Filter out current approvers
                potential_approvers = [u for u in potential_approvers if u.uid not in current_approver_uids]
                
                # Create options dictionary for select widget
                user_options = {f"{u.name} ({u.username})" : u.uid for u in potential_approvers}
                
                # If no potential approvers, show message
                if not user_options:
                    self.notification_area.object = "**Error:** No additional users available to be assigned as approvers"
                    return
                    
            except Exception as users_err:
                # Fallback - get all users
                logger.error(f"Error getting potential approvers: {users_err}")
                user_options = {"user1": "User 1", "user2": "User 2"}  # Placeholder
            
            # Create form elements
            approver_select = pn.widgets.Select(
                name="Select Approver",
                options=user_options,
                width=300
            )
            
            # For sequential approvals, add sequence selector
            sequence_input = None
            if approval_data.get('sequential', False):
                sequence_input = pn.widgets.IntInput(
                    name="Approval Sequence Order",
                    value=len(approval_data.get('approver_assignments', [])) + 1,
                    start=1,
                    width=150
                )
            
            # Add instructions field for approver
            instructions_input = pn.widgets.TextAreaInput(
                name="Instructions for Approver",
                placeholder="Enter specific instructions for this approver (optional)",
                rows=3,
                width=300
            )
            
            submit_btn = pn.widgets.Button(
                name="Add Approver",
                button_type="primary",
                width=150
            )
            
            cancel_btn = pn.widgets.Button(
                name="Cancel",
                button_type="default",
                width=100
            )
            
            # Create form container
            form_components = [
                pn.pane.Markdown("## Add Approver to Approval Cycle"),
                pn.pane.Markdown(f"Approval: {approval_data.get('status', 'Unknown')}"),
                pn.Row(approver_select, sizing_mode='stretch_width'),
                pn.Row(instructions_input, sizing_mode='stretch_width')
            ]
            
            if sequence_input:
                form_components.append(pn.Row(sequence_input, sizing_mode='stretch_width'))
            
            form_components.append(
                pn.Row(
                    pn.layout.HSpacer(),
                    cancel_btn,
                    submit_btn,
                    sizing_mode='stretch_width'
                )
            )
            
            form = pn.Column(
                *form_components,
                width=400,
                styles={'background':'#f8f9fa'},
                css_classes=['p-3', 'border', 'rounded', 'shadow']
            )
            
            # Define submission handler
            def submit_add_approver(event):
                # Validate inputs
                if not approver_select.value:
                    self.notification_area.object = "**Error:** You must select an approver"
                    return
                    
                # Call controller function
                from CDocs.controllers.approval_controller import add_approver_to_active_approval
                try:
                    result = add_approver_to_active_approval(
                        user=self.user,
                        approval_uid=approval_uid,
                        approver_uid=approver_select.value,
                        sequence_order=sequence_input.value if sequence_input else None,
                        instructions=instructions_input.value if instructions_input.value else None
                    )
                    
                    if result.get('success', False):
                        self.notification_area.object = "**Success:** Approver added successfully"
                        # Reload approval details
                        self._load_document()
                    else:
                        self.notification_area.object = f"**Error:** {result.get('message', 'Failed to add approver')}"
                except Exception as e:
                    self.notification_area.object = f"**Error:** {str(e)}"
            
            # Define cancel handler
            def cancel_action(event):
                self.notification_area.object = "Approver addition canceled"
                # Remove the form from view
                if hasattr(self, 'main_content'):
                    for i, item in enumerate(self.main_content):
                        if item is form:
                            self.main_content.pop(i)
                            break
            
            # Add handlers
            submit_btn.on_click(submit_add_approver)
            cancel_btn.on_click(cancel_action)
            
            # Clear display area and show form
            # 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(form)
            else:
                # Embedded mode using main_content
                self.main_content.clear()
                self.main_content.append(self.notification_area)
                self.main_content.append(form)
                
        except Exception as e:
            logger.error(f"Error showing add approver form: {e}")
            logger.error(traceback.format_exc())
            self.notification_area.object = f"**Error:** {str(e)}"
    
    def _show_extend_approval_deadline_form(self, approval_uid):
        """Show form to extend approval deadline"""
        try:
            # Store the approval UID for later use
            self._selected_approval_uid = approval_uid
            # Get current approval cycle to show current deadline
            from CDocs.controllers.approval_controller import get_approval_cycle
            approval_data = get_approval_cycle(approval_uid)
            
            if not approval_data:
                self.notification_area.object = "**Error:** Could not retrieve approval cycle details"
                return
                
            # Extract current deadline for display
            current_deadline = None
            if 'dueDate' in approval_data:
                try:
                    current_deadline = datetime.fromisoformat(approval_data['dueDate'])
                    current_deadline_str = self._format_date(approval_data['dueDate'])
                except Exception:
                    current_deadline_str = approval_data['dueDate']
            else:
                current_deadline_str = "Not set"
    
            # Create form elements
            date_picker = pn.widgets.DatePicker(
                name="New Due Date",
                value=None,
                start=datetime.now().date() + timedelta(days=1),  # Must be at least tomorrow
                end=datetime.now().date() + timedelta(days=90),   # Maximum 90 days in future
                width=200
            )
            
            reason_input = pn.widgets.TextAreaInput(
                name="Reason for Extension",
                placeholder="Enter reason for deadline extension",
                rows=3,
                width=300
            )
            
            submit_btn = pn.widgets.Button(
                name="Extend Deadline",
                button_type="primary",
                width=150
            )
            
            cancel_btn = pn.widgets.Button(
                name="Cancel",
                button_type="default",
                width=100
            )
            
            # Create form container
            form = pn.Column(
                pn.pane.Markdown("## Extend Approval Deadline"),
                pn.pane.Markdown(f"Current deadline: **{current_deadline_str}**"),
                pn.Row(date_picker, sizing_mode='stretch_width'),
                pn.Row(reason_input, sizing_mode='stretch_width'),
                pn.Row(
                    pn.layout.HSpacer(),
                    cancel_btn,
                    submit_btn,
                    sizing_mode='stretch_width'
                ),
                width=400,
                styles={'background':'#f8f9fa'},
                css_classes=['p-3', 'border', 'rounded', 'shadow']
            )
            
            # Define submission handler
            def submit_extension(event):
                # Validate inputs
                if not date_picker.value:
                    self.notification_area.object = "**Error:** Please select a new due date"
                    return
                    
                # Convert to datetime with end of day
                new_deadline = datetime.combine(date_picker.value, datetime.max.time())
                
                # Call controller function
                from CDocs.controllers.approval_controller import extend_approval_deadline
                try:
                    result = extend_approval_deadline(
                        user=self.user,
                        approval_uid=approval_uid,
                        new_due_date=new_deadline,
                        reason=reason_input.value
                    )
                    
                    if result.get('success', False):
                        self.notification_area.object = "**Success:** Approval deadline extended successfully"
                        # Reload approval details
                        self._load_document()
                    else:
                        self.notification_area.object = f"**Error:** {result.get('message', 'Failed to extend deadline')}"
                except Exception as e:
                    self.notification_area.object = f"**Error:** {str(e)}"
            
            # Define cancel handler
            def cancel_action(event):
                self.notification_area.object = "Deadline extension canceled"
                # Remove the form from view
                if hasattr(self, 'main_content'):
                    for i, item in enumerate(self.main_content):
                        if item is form:
                            self.main_content.pop(i)
                            break
            
            # Add handlers
            submit_btn.on_click(submit_extension)
            cancel_btn.on_click(cancel_action)
            
            # Clear display area and show form
            # 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(form)
            else:
                # Embedded mode using main_content
                self.main_content.clear()
                self.main_content.append(self.notification_area)
                self.main_content.append(form)
                
        except Exception as e:
            logger.error(f"Error showing extend deadline form: {e}")
            logger.error(traceback.format_exc())
            self.notification_area.object = f"**Error:** {str(e)}"

    def _show_extend_review_deadline_form(self, review_uid):
        """Show form to extend review deadline"""
        try:
            # Store the review UID for later use
            self._selected_review_uid = review_uid
            # Get current review cycle to show current deadline
            from CDocs.controllers.review_controller import get_review_cycle
            review_data = get_review_cycle(review_uid)
            
            if not review_data:
                self.notification_area.object = "**Error:** Could not retrieve review cycle details"
                return
                
            # Extract current deadline for display
            current_deadline = None
            if 'dueDate' in review_data:
                try:
                    current_deadline = self._convert_neo4j_datetimes(review_data['dueDate'])
                    current_deadline_str = current_deadline.strftime("%Y-%m-%d") if current_deadline else "Not set"
                except Exception:
                    current_deadline_str = str(review_data['dueDate'])
            else:
                current_deadline_str = "Not set"
    
            # Create form elements
            date_picker = pn.widgets.DatePicker(
                name="New Due Date",
                value=None,
                start=datetime.now().date() + timedelta(days=1),  # Must be at least tomorrow
                end=datetime.now().date() + timedelta(days=90),   # Maximum 90 days in future
                width=200
            )
            
            reason_input = pn.widgets.TextAreaInput(
                name="Reason for Extension",
                placeholder="Enter reason for deadline extension",
                rows=3,
                width=300
            )
            
            submit_btn = pn.widgets.Button(
                name="Extend Deadline",
                button_type="primary",
                width=150
            )
            
            cancel_btn = pn.widgets.Button(
                name="Cancel",
                button_type="default",
                width=100
            )
            
            # Create form container
            form = pn.Column(
                pn.pane.Markdown("## Extend Review Deadline"),
                pn.pane.Markdown(f"Current deadline: **{current_deadline_str}**"),
                pn.Row(date_picker, sizing_mode='stretch_width'),
                pn.Row(reason_input, sizing_mode='stretch_width'),
                pn.Row(
                    pn.layout.HSpacer(),
                    cancel_btn,
                    submit_btn,
                    sizing_mode='stretch_width'
                ),
                width=400,
                styles={'background':'#f8f9fa'},
                css_classes=['p-3', 'border', 'rounded', 'shadow']
            )
            
            # Define submission handler
            def submit_extension(event):
                # Validate inputs
                if not date_picker.value:
                    self.notification_area.object = "**Error:** Please select a new deadline date"
                    return
                    
                # Convert to datetime with end of day
                new_deadline = datetime.combine(date_picker.value, datetime.max.time())
                
                # Call controller function
                from CDocs.controllers.review_controller import extend_review_deadline
                try:
                    result = extend_review_deadline(
                        user=self.user,
                        review_uid=review_uid,
                        new_due_date=new_deadline,
                        reason=reason_input.value
                    )
                    
                    if result.get('success', False):
                        self.notification_area.object = "**Success:** Review deadline extended"
                        # Close form and refresh review details
                        # Close form and refresh document details
                        self._load_document()
                    else:
                        self.notification_area.object = f"**Error:** {result.get('message', 'Unknown error')}"
                except Exception as e:
                    self.notification_area.object = f"**Error:** {str(e)}"
            
            # Define cancel handler
            def cancel_action(event):
                self.notification_area.object = "Deadline extension canceled"
                # Remove the form from view
                if hasattr(self, 'main_content'):
                    for i, item in enumerate(self.main_content):
                        if item is form:
                            self.main_content.pop(i)
                            break
            
            # Add handlers
            submit_btn.on_click(submit_extension)
            cancel_btn.on_click(cancel_action)
            
            # 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(form)
            else:
                # Embedded mode using main_content
                self.main_content.clear()
                self.main_content.append(self.notification_area)
                self.main_content.append(form)
                
        except Exception as e:
            logger.error(f"Error showing extend deadline form: {e}")
            logger.error(traceback.format_exc())
            self.notification_area.object = f"**Error:** {str(e)}"
    
    def _show_add_reviewer_form(self, review_uid):
        """Show form to add a reviewer to an active review cycle"""
        try:
            # Store the review UID for later use
            self._selected_review_uid = review_uid
            # Get current review cycle
            from CDocs.controllers.review_controller import get_review_cycle
            review_data = get_review_cycle(review_uid)
            
            if not review_data:
                self.notification_area.object = "**Error:** Could not retrieve review cycle details"
                return
                
            # Get potential reviewers (exclude current reviewers)
            from CDocs.models.user_extensions import DocUser
            try:
                potential_reviewers = DocUser.get_users_by_role(role="REVIEWER")
                

                # Get current reviewer UIDs to exclude
                current_reviewer_uids = set()
                for reviewer in review_data.get('reviewers', []):
                    current_reviewer_uids.add(reviewer.get('UID'))
                
                # Filter out current reviewers
                potential_reviewers = [u for u in potential_reviewers if u.uid not in current_reviewer_uids]
                
                # Create options dictionary for select widget
                user_options = {f"{u.name} ({u.username})" : u.uid for u in potential_reviewers}
                
                # If no potential reviewers, show message
                if not user_options:
                    self.notification_area.object = "**Info:** No additional reviewers available"
                    return
                    
            except Exception as users_err:
                # Fallback - get all users
                logger.error(f"Error getting potential reviewers: {users_err}")
                user_options = {"user1": "User 1", "user2": "User 2"}  # Placeholder
            
            # Create form elements
            reviewer_select = pn.widgets.Select(
                name="Select Reviewer",
                options=user_options,
                width=300
            )
            
            # For sequential reviews, add sequence selector
            sequence_input = None
            if review_data.get('sequential', False):
                sequence_input = pn.widgets.IntInput(
                    name="Review Sequence Order",
                    value=len(review_data.get('reviewers', [])) + 1,  # Default to next in sequence
                    start=1,
                    width=150
                )
            
            submit_btn = pn.widgets.Button(
                name="Add Reviewer",
                button_type="primary",
                width=150
            )
            
            cancel_btn = pn.widgets.Button(
                name="Cancel",
                button_type="default",
                width=100
            )
            
            # Create form container
            form_components = [
                pn.pane.Markdown("## Add Reviewer to Review Cycle"),
                pn.pane.Markdown(f"Review: {review_data.get('status', 'Unknown')}"),
                pn.Row(reviewer_select, sizing_mode='stretch_width')
            ]
            
            if sequence_input:
                form_components.append(pn.Row(sequence_input, sizing_mode='stretch_width'))
            
            form_components.append(
                pn.Row(
                    pn.layout.HSpacer(),
                    cancel_btn,
                    submit_btn,
                    sizing_mode='stretch_width'
                )
            )
            
            form = pn.Column(
                *form_components,
                width=400,
                styles={'background':'#f8f9fa'},
                css_classes=['p-3', 'border', 'rounded', 'shadow']
            )
            
            # Define submission handler
            def submit_add_reviewer(event):
                # Validate inputs
                if not reviewer_select.value:
                    self.notification_area.object = "**Error:** Please select a reviewer"
                    return
                    
                # Call controller function
                from CDocs.controllers.review_controller import add_reviewer_to_active_review
                try:
                    result = add_reviewer_to_active_review(
                        user=self.user,
                        review_uid=review_uid,
                        reviewer_uid=reviewer_select.value,
                        sequence_order=sequence_input.value if sequence_input else None
                    )
                    
                    if result.get('success', False):
                        self.notification_area.object = "**Success:** Reviewer added to review cycle"
                        # Close form and refresh review details
                        # Close form and refresh document details
                        self._load_document()
                    else:
                        self.notification_area.object = f"**Error:** {result.get('message', 'Unknown error')}"
                except Exception as e:
                    self.notification_area.object = f"**Error:** {str(e)}"
            
            # Define cancel handler
            def cancel_action(event):
                self.notification_area.object = "Reviewer addition canceled"
                # Remove the form from view
                if hasattr(self, 'main_content'):
                    for i, item in enumerate(self.main_content):
                        if item is form:
                            self.main_content.pop(i)
                            break
            
            # Add handlers
            submit_btn.on_click(submit_add_reviewer)
            cancel_btn.on_click(cancel_action)
            
            # 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(form)
            else:
                # Embedded mode using main_content
                self.main_content.clear()
                self.main_content.append(self.notification_area)
                self.main_content.append(form)
                
        except Exception as e:
            logger.error(f"Error showing add reviewer form: {e}")
            logger.error(traceback.format_exc())
            self.notification_area.object = f"**Error:** {str(e)}"
    
    def _show_cancel_review_form(self, review_uid):
        """Show form to cancel an active review cycle"""
        try:
            # Store the review UID for later use
            self._selected_review_uid = review_uid
            # Get current review cycle
            from CDocs.controllers.review_controller import get_review_cycle
            review_data = get_review_cycle(review_uid, include_document=True)
            
            if not review_data:
                self.notification_area.object = "**Error:** Could not retrieve review cycle details"
                return
                
            # Create form elements
            reason_input = pn.widgets.TextAreaInput(
                name="Reason for Cancellation",
                placeholder="Enter reason for canceling the review cycle",
                rows=3,
                width=300
            )
            
            confirm_checkbox = pn.widgets.Checkbox(
                name="I understand this action cannot be undone",
                value=False
            )
            
            submit_btn = pn.widgets.Button(
                name="Cancel Review",
                button_type="danger",
                width=150,
                disabled=True  # Disabled until checkbox is checked
            )
            
            cancel_btn = pn.widgets.Button(
                name="Go Back",
                button_type="default",
                width=100
            )
            
            # Create warning with document info
            document_info = review_data.get('document', {})
            doc_number = document_info.get('doc_number', 'Unknown')
            doc_title = document_info.get('title', 'Unknown')
            
            warning_md = pn.pane.Markdown(f"""
            ## ⚠️ Cancel Review Cycle
            
            **Warning:** You are about to cancel the active review cycle for document:
            
            **{doc_number}**: {doc_title}
            
            This action cannot be undone. All reviewer assignments and comments will remain,
            but the review cycle will be marked as canceled.
            """)
            
            # Create form container
            form = pn.Column(
                warning_md,
                pn.Row(reason_input, sizing_mode='stretch_width'),
                pn.Row(confirm_checkbox, sizing_mode='stretch_width'),
                pn.Row(
                    pn.layout.HSpacer(),
                    cancel_btn,
                    submit_btn,
                    sizing_mode='stretch_width'
                ),
                width=500,
                styles={'background':'#f8f9fa'},
                css_classes=['p-3', 'border', 'rounded', 'shadow']
            )
            
            # Enable/disable submit button based on checkbox
            def update_submit_btn(event):
                submit_btn.disabled = not confirm_checkbox.value
            
            confirm_checkbox.param.watch(update_submit_btn, 'value')
            
            # Define submission handler
            def submit_cancel_review(event):
                # Validate inputs
                if not reason_input.value or len(reason_input.value.strip()) < 10:
                    self.notification_area.object = "**Error:** Please provide a detailed reason for cancellation"
                    return
                    
                if not confirm_checkbox.value:
                    self.notification_area.object = "**Error:** You must confirm the action"
                    return
                    
                # Call controller function
                from CDocs.controllers.review_controller import cancel_review_cycle
                try:
                    result = cancel_review_cycle(
                        user=self.user,
                        review_uid=review_uid,
                        reason=reason_input.value
                    )
                    
                    if result.get('success', False):
                        self.notification_area.object = "**Success:** Review cycle canceled successfully"
                        # Close form and refresh document details
                        self._load_document()
                    else:
                        self.notification_area.object = f"**Error:** {result.get('message', 'Unknown error')}"
                except Exception as e:
                    self.notification_area.object = f"**Error:** {str(e)}"
            
            # Define cancel handler
            def cancel_action(event):
                self.notification_area.object = "Cancellation aborted"
                # Remove the form from view
                if hasattr(self, 'main_content'):
                    for i, item in enumerate(self.main_content):
                        if item is form:
                            self.main_content.pop(i)
                            break
            
            # Add handlers
            submit_btn.on_click(submit_cancel_review)
            cancel_btn.on_click(cancel_action)
            
            # 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(form)
            else:
                # Embedded mode using main_content
                self.main_content.clear()
                self.main_content.append(self.notification_area)
                self.main_content.append(form)
                
        except Exception as e:
            logger.error(f"Error showing cancel review form: {e}")
            logger.error(traceback.format_exc())
            self.notification_area.object = f"**Error:** {str(e)}"

    def _show_cancel_approval_form(self, approval_uid):
        """Show form to cancel an active approval cycle"""
        try:
            # Store the approval UID for later use
            self._selected_approval_uid = approval_uid
            # Get current approval cycle
            from CDocs.controllers.approval_controller import get_approval_cycle
            approval_data = get_approval_cycle(approval_uid, include_document=True)
            
            if not approval_data:
                self.notification_area.object = "**Error:** Could not retrieve approval cycle details"
                return
                
            # Create form elements
            reason_input = pn.widgets.TextAreaInput(
                name="Reason for Cancellation",
                placeholder="Enter reason for canceling the approval cycle",
                rows=3,
                width=300
            )
            
            confirm_checkbox = pn.widgets.Checkbox(
                name="I understand this action cannot be undone",
                value=False
            )
            
            submit_btn = pn.widgets.Button(
                name="Cancel Approval",
                button_type="danger",
                width=150,
                disabled=True  # Disabled until checkbox is checked
            )
            
            cancel_btn = pn.widgets.Button(
                name="Go Back",
                button_type="default",
                width=100
            )
            
            # Create warning with document info
            document_info = approval_data.get('document', {})
            doc_number = document_info.get('doc_number', 'Unknown')
            doc_title = document_info.get('title', 'Unknown')
            
            warning_md = pn.pane.Markdown(f"""
            ## ⚠️ Cancel Approval Cycle
            
            **Warning:** You are about to cancel the active approval cycle for document:
            
            **{doc_number}**: {doc_title}
            
            This action cannot be undone. All approver assignments and comments will remain,
            but the approval cycle will be marked as canceled.
            """)
            
            # Create form container
            form = pn.Column(
                warning_md,
                pn.Row(reason_input, sizing_mode='stretch_width'),
                pn.Row(confirm_checkbox, sizing_mode='stretch_width'),
                pn.Row(
                    pn.layout.HSpacer(),
                    cancel_btn,
                    submit_btn,
                    sizing_mode='stretch_width'
                ),
                width=500,
                styles={'background':'#f8f9fa'},
                css_classes=['p-3', 'border', 'rounded', 'shadow']
            )
            
            # Enable/disable submit button based on checkbox
            def update_submit_btn(event):
                submit_btn.disabled = not confirm_checkbox.value
            
            confirm_checkbox.param.watch(update_submit_btn, 'value')
            
            # Define submission handler
            def submit_cancel_approval(event):
                # Validate inputs
                if not reason_input.value or len(reason_input.value.strip()) < 10:
                    self.notification_area.object = "**Error:** Please provide a detailed reason for cancellation"
                    return
                    
                if not confirm_checkbox.value:
                    self.notification_area.object = "**Error:** You must confirm the action"
                    return
                    
                # Call controller function
                from CDocs.controllers.approval_controller import cancel_approval_cycle
                try:
                    result = cancel_approval_cycle(
                        user=self.user,
                        approval_uid=approval_uid,
                        reason=reason_input.value
                    )
                    
                    if result.get('success', False):
                        self.notification_area.object = "**Success:** Approval cycle canceled successfully"
                        # Close form and refresh document details
                        self._load_document()
                    else:
                        self.notification_area.object = f"**Error:** {result.get('message', 'Unknown error')}"
                except Exception as e:
                    self.notification_area.object = f"**Error:** {str(e)}"
            
            # Define cancel handler
            def cancel_action(event):
                self.notification_area.object = "Cancellation aborted"
                # Remove the form from view
                if hasattr(self, 'main_content'):
                    for i, item in enumerate(self.main_content):
                        if item is form:
                            self.main_content.pop(i)
                            break
            
            # Add handlers
            submit_btn.on_click(submit_cancel_approval)
            cancel_btn.on_click(cancel_action)
            
            # Clear display area and show form
            # 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(form)
            else:
                # Embedded mode using main_content
                self.main_content.clear()
                self.main_content.append(self.notification_area)
                self.main_content.append(form)
                
        except Exception as e:
            logger.error(f"Error showing cancel approval form: {e}")
            logger.error(traceback.format_exc())
            self.notification_area.object = f"**Error:** {str(e)}"
    
    def _load_review_details_by_uid(self, review_uid):
        """Helper method to refresh review details after an action"""
        try:
            # Just re-trigger the review selection with the same review UID
            # This will fetch fresh data and update the UI
            class DummyEvent:
                pass
                
            dummy_event = DummyEvent()
            dummy_event.new = [0]  # Simulate first row selection
            
            # Store review UID so _review_selected can find it
            self._selected_review_uid = review_uid
            
            # Re-trigger the review selection handler
            self._review_selected(dummy_event)
            
        except Exception as e:
            logger.error(f"Error refreshing review details: {e}")
            self.notification_area.object = f"**Error refreshing review details:** {str(e)}"

    def _convert_neo4j_datetimes(self, data):
        """
        Recursively convert all Neo4j DateTime objects to Python datetime objects or strings.
        
        Args:
            data: Any data structure potentially containing Neo4j DateTime objects
            
        Returns:
            Same data structure with Neo4j DateTime objects converted to Python datetime
        """
        if data is None:
            return None
            
        # Handle Neo4j DateTime objects
        if hasattr(data, '__class__') and data.__class__.__name__ == 'DateTime':
            try:
                # Try to convert to Python datetime
                import datetime
                py_datetime = datetime.datetime(
                    year=data.year, 
                    month=data.month, 
                    day=data.day,
                    hour=data.hour, 
                    minute=data.minute, 
                    second=data.second,
                    microsecond=data.nanosecond // 1000
                )
                return py_datetime
            except (AttributeError, ValueError):
                # If conversion fails, return as string
                return str(data)
                
        # Handle dictionaries
        elif isinstance(data, dict):
            return {k: self._convert_neo4j_datetimes(v) for k, v in data.items()}
            
        # Handle lists
        elif isinstance(data, list):
            return [self._convert_neo4j_datetimes(item) for item in data]
            
        # Handle tuples
        elif isinstance(data, tuple):
            return tuple(self._convert_neo4j_datetimes(item) for item in data)
            
        # Return other types unchanged
        return data
    
    def _create_approvals_tab(self):
        """Create the approvals tab content, hiding approvals for archived versions except current one"""
        # Get approval cycles from document
        approval_cycles = []
        
        try:
            # Call controller to get approval cycles
            from CDocs.controllers.approval_controller import get_document_approval_cycles
            approval_result = get_document_approval_cycles(document_uid=self.document_uid)
            approval_cycles = approval_result.get('approval_cycles', [])
            
            logger.debug(f"Loaded {len(approval_cycles)} approval cycles")
        except Exception as e:
            logger.error(f"Error loading approval cycles: {e}")
            return pn.Column(
                pn.pane.Markdown("# Document Approvals"),
                pn.pane.Markdown(f"**Error loading approval data:** {str(e)}"),
                sizing_mode='stretch_width'
            )
        
        if not approval_cycles:
            # Create button to start approval if appropriate
            if self.document.get('status') == 'DRAFT' and permissions.user_has_permission(self.user, "INITIATE_APPROVAL"):
                start_approval_btn = pn.widgets.Button(
                    name="Start Approval", 
                    button_type="primary", 
                    width=150
                )
                start_approval_btn.on_click(self._show_approval_form)
                
                return pn.Column(
                    pn.pane.Markdown("# Document Approvals"),
                    pn.pane.Markdown("*No approval cycles found for this document*"),
                    start_approval_btn,
                    sizing_mode='stretch_width'
                )
            else:
                return pn.Column(
                    pn.pane.Markdown("# Document Approvals"),
                    pn.pane.Markdown("*No approval cycles found for this document*"),
                    sizing_mode='stretch_width'
                )
        
        # Get current version UID for filtering
        current_version_uid = None
        if self.current_version:
            current_version_uid = self.current_version.get('UID')
        
        # Filter approval cycles to exclude those for archived versions (except current version)
        filtered_approval_cycles = []
        for cycle in approval_cycles:
            version_uid = cycle.get('version_uid')
            # Include if it's for the current version or a non-archived version
            if version_uid == current_version_uid or not self._is_archived_version(version_uid):
                filtered_approval_cycles.append(cycle)
        
        # Convert to DataFrame for tabulator
        try:
            # Create a clean list of dictionaries for the DataFrame
            approvals_data = []
            for cycle in filtered_approval_cycles:
                # Convert Neo4j DateTime objects to Python datetime objects or strings
                cycle_dict = {}
                for key, value in cycle.items():
                    # Convert Neo4j DateTime objects to strings
                    if hasattr(value, '__class__') and value.__class__.__name__ == 'DateTime':
                        # Convert Neo4j DateTime to string in ISO format
                        try:
                            cycle_dict[key] = value.iso_format()[:10]  # Just get the YYYY-MM-DD part
                        except (AttributeError, TypeError):
                            cycle_dict[key] = str(value)
                    else:
                        cycle_dict[key] = value
                
                approvals_data.append(cycle_dict)
            
            # Create DataFrame (safely handle empty data)
            if not approvals_data:
                return pn.Column(
                    pn.pane.Markdown("# Document Approvals"),
                    pn.pane.Markdown("*No approvals found for non-archived versions*"),
                    sizing_mode='stretch_width'
                )
            
            approvals_df = pd.DataFrame(approvals_data)
        except Exception as df_error:
            logger.error(f"Error creating approvals DataFrame: {df_error}")
            logger.error(f"Traceback: {traceback.format_exc()}")
            return pn.Column(
                pn.pane.Markdown("# Document Approvals"),
                pn.pane.Markdown(f"*Error formatting approval cycle data: {str(df_error)}*"),
                sizing_mode='stretch_width'
            )
        
        # Format dates - no need to use pd.to_datetime since we already formatted them
        date_columns = ['start_date', 'startDate', 'due_date', 'dueDate', 'completed_date', 'completionDate']
        for col in date_columns:
            if col in approvals_df.columns:
                # Instead of using pd.to_datetime, ensure the column contains strings
                approvals_df[col] = approvals_df[col].astype(str)
        
        # Select and rename columns for display
        display_columns = ['UID', 'cycle_number', 'status', 'initiated_by_name', 'startDate', 'dueDate', 'completionDate']
        column_names = {
            'UID': 'UID',
            'cycle_number': 'Cycle #',
            'status': 'Status',
            'initiated_by_name': 'Initiated By',
            'startDate': 'Started',
            'dueDate': 'Due',
            'completionDate': '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,
            hidden_columns=['UID']
        )
        
        # 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 cycle 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') in settings.APPROVAL_ALLOWED_STATUSES and permissions.user_has_permission(self.user, "INITIATE_APPROVAL"):
            start_approval_btn = Button(
                name="Start New Approval Cycle", 
                button_type="primary", 
                width=180
            )
            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 _get_status_color(self, status):
        """Get color for a status value"""
        # Default colors for various statuses
        status_colors = {
            'PENDING': '#6c757d',    # Gray
            'IN_PROGRESS': '#ffc107', # Yellow
            'COMPLETED': '#28a745',  # Green
            'APPROVED': '#28a745',   # Green
            'REJECTED': '#dc3545',   # Red
            'CANCELED': '#6c757d'    # Gray
        }
        
        # Look up in settings if available
        if hasattr(settings, 'APPROVAL_STATUS_CONFIG'):
            status_config = getattr(settings, 'APPROVAL_STATUS_CONFIG', {}).get(status, {})
            if status_config and 'color' in status_config:
                return status_config['color']
        
        # Fallback to our local mapping
        return status_colors.get(status, '#6c757d')  # Default gray if not found
    
    
    
    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 _approval_selected(self, event):
        """Handle approval selection from table with support for both selection and cell click events"""
        logger = logging.getLogger('CDocs.ui.document_detail')
        
        try:
            # Clear any previous notifications
            self.notification_area.object = ""
            
            # Debug the event type
            logger.debug(f"Review selection event type: {type(event).__name__}")
            
            # Handle different event types
            row_index = None
            approval_uid = None
            row_data = None
            
            # Check if this is a CellClickEvent
            if hasattr(event, 'row') and event.row is not None:
                # This is a CellClickEvent
                row_index = event.row
                logger.debug(f"Cell click event on row {row_index}")
                
                # For CellClickEvent, extract data from event.model.source
                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())}")
                    
                    # Try to find approval_uid directly in source data
                    uid_keys = ['approval_uid', '_approval_uid', 'UID', 'uid']
                    for key in uid_keys:
                        if key in source_data and len(source_data[key]) > row_index:
                            approval_uid = source_data[key][row_index]
                            logger.debug(f"Found approval_uid using key '{key}': {approval_uid}")
                            break
                            
                    # Create row data dictionary
                    row_data = {col: values[row_index] for col, values in source_data.items() if len(values) > row_index}
                    
                    # If still no UID found, check through all columns that might contain UID
                    if not approval_uid:
                        for key in source_data.keys():
                            if '_uid' in key.lower() and len(source_data[key]) > row_index:
                                approval_uid = source_data[key][row_index]
                                logger.debug(f"Found approval_uid from column '{key}': {approval_uid}")
                                break
                
            # Handle TabSelector selection event
            elif hasattr(event, 'new') and event.new is not None:
                row_index = event.new[0] if isinstance(event.new, list) else event.new
                logger.debug(f"Selection event with index {row_index}")
                
                # Get DataFrame
                if hasattr(event, 'obj') and hasattr(event.obj, 'value'):
                    df = event.obj.value
                    logger.debug(f"DataFrame columns: {list(df.columns)}")
                    
                    if row_index < len(df):
                        row_data = df.iloc[row_index].to_dict()
                        
                        # Try different common column names for the UID
                        uid_columns = ['approval_uid', '_approval_uid', 'UID', 'uid']
                        for col in uid_columns:
                            if col in df.columns:
                                approval_uid = df.iloc[row_index][col]
                                logger.debug(f"Found approval_uid in column '{col}': {approval_uid}")
                                break
                                
                        # If still no UID found, check through all columns that might contain UID
                        if not approval_uid:
                            for col in df.columns:
                                if '_uid' in col.lower():
                                    approval_uid = df.iloc[row_index][col]
                                    logger.debug(f"Found approval_uid from column '{col}': {approval_uid}")
                                    break
            
            # Last resort - check if the event itself has a UID attribute or value
            if not approval_uid:
                if hasattr(event, 'uid'):
                    approval_uid = event.uid
                elif hasattr(event, 'value') and isinstance(event.value, str) and len(event.value) > 20:
                    # Possibly a UID string
                    approval_uid = event.value
            
            if not approval_uid:
                logger.warning("Could not determine approval UID from event")
                self.notification_area.object = "**Error:** Couldn't determine approval ID"
                return
            
            logger.info(f"Loading approval with UID: {approval_uid}")
            
            # Load approval details
            from CDocs.controllers.approval_controller import get_approval_cycle
            approval_data = get_approval_cycle(approval_uid, include_document=True, include_comments=True)
            
            if not approval_data:
                self.notification_area.object = "**Error:** Approval not found"
                return
            
            approval_data = self._convert_neo4j_datetimes(approval_data)
                
            # Extract data
            status = approval_data.get('status', '')
            approval_type = approval_data.get('approval_type', '')
            initiated_date = self._format_date(approval_data.get('initiated_date', ''))
            due_date = self._format_date(approval_data.get('due_date', ''))
            initiated_by = approval_data.get('initiated_by_name', '')
            instructions = approval_data.get('instructions', '')
            
            # Get approvaler data
            approver_assignments = approval_data.get('approver_assignments', [])
            
            # Convert to DataFrame for tabulator
            approver_data = []
            for r in approver_assignments:
                approver_data.append({
                    'approver': r.get('approver_name', ''),
                    'status': r.get('status', ''),
                    'decision': r.get('decision', ''),
                    'assigned_date': self._format_date(r.get('assigned_date', '')),
                    'decision_date': self._format_date(r.get('decision_date', ''))
                })
            
            approvers_df = pd.DataFrame(approver_data)
            
            # Get comments
            comments = approval_data.get('comments', [])
            
            # Create details panel
            details_panel = pn.Column(
                pn.pane.Markdown(f"## Approval Cycle Details"),
                pn.pane.Markdown(f"**Status:** {status}"),
                pn.pane.Markdown(f"**Type:** {approval_type}"),
                pn.pane.Markdown(f"**Started:** {initiated_date}"),
                pn.pane.Markdown(f"**Due Date:** {due_date}"),
                pn.pane.Markdown(f"**Initiated By:** {initiated_by}"),
                sizing_mode='stretch_width',
                styles={'background':'#f8f9fa'},
                css_classes=['p-3', 'border', 'rounded']
            )
            
            # Add instructions if available
            if instructions:
                details_panel.append(pn.pane.Markdown("### Instructions"))
                details_panel.append(pn.pane.Markdown(instructions))
            
            # Add approvers table
            details_panel.append(pn.pane.Markdown("### Approvers"))
            details_panel.append(pn.widgets.Tabulator(
                approvers_df,
                pagination='local',
                page_size=5,
                sizing_mode='stretch_width',
                height=200
            ))
            
            # Add comments section
            details_panel.append(pn.pane.Markdown("### Comments"))
            if comments:
                comments_html = "<div class='comments-container'>"
                for comment in comments:
                    user_name = comment.get('user_name', 'Unknown')
                    timestamp = self._format_date(comment.get('timestamp', ''))
                    text = comment.get('text', '')
                    section = comment.get('section', '')
                    
                    comments_html += f"""
                    <div class='comment p-2 mb-2 border rounded'>
                        <div><strong>{user_name}</strong> <span class='text-muted'>on {timestamp}</span></div>
                        {f"<div><strong>Section:</strong> {section}</div>" if section else ""}
                        <div>{text}</div>
                    </div>
                    """
                comments_html += "</div>"
                details_panel.append(pn.pane.HTML(comments_html))
            else:
                details_panel.append(pn.pane.Markdown("*No comments yet*"))
            
            # Add actions section for document owner or approval initiator
            if status == 'COMPLETED' and self.user:
                # Check if user is document owner, approval initiator, or has manage permission
                is_document_owner = self.doc_owner_uid == self.user.uid
                is_approval_initiator = approval_data.get('initiated_by_uid') == self.user.uid
                has_manage_permission = permissions.user_has_permission(self.user, "MANAGE_APPROVALS")
                document_status = approval_data.get("document", {}).get("status", "")
                
                # Only show the close approval section if the document is still IN_REVIEW
                if document_status == "IN_APPROVAL" and (is_document_owner or is_approval_initiator or has_manage_permission):
                    logger.info("adding close approval section")
                    # Create close approval section
                    close_approval_section = pn.Column(
                        pn.pane.Markdown("## Close Approval Cycle"),
                        pn.pane.Markdown("The approval cycle is completed. You can now close it and update the document status."),
                        sizing_mode='stretch_width'
                    )
                    
                    # Create status dropdown
                    status_select = pn.widgets.Select(
                        name="Update Document Status To",
                        options=['DRAFT', 'APPROVED'],
                        value='DRAFT',
                        width=200
                    )
                    
                    # Create checkbox for updating status
                    update_status_checkbox = pn.widgets.Checkbox(
                        name="Update Document Status",
                        value=True,
                        width=200
                    )
                    
                    # Create close button
                    close_btn = pn.widgets.Button(
                        name="Close Approval Cycle",
                        button_type="primary",
                        width=150
                    )
                    
                    # Create form layout
                    close_form = pn.Column(
                        pn.Row(
                            pn.Column(update_status_checkbox, status_select),
                            pn.layout.HSpacer(),
                            pn.Column(close_btn, align='end')
                        ),
                        styles={'background':'#f8f9fa'},
                        css_classes=['p-3', 'border', 'rounded'],
                        sizing_mode='stretch_width'
                    )
                    
                    # Add to section
                    close_approval_section.append(close_form)
                    
                    # Add handler
                    from functools import partial
                    close_btn.on_click(partial(self._close_approval_cycle, 
                                            approval_uid=approval_uid, 
                                            update_status_checkbox=update_status_checkbox, 
                                            status_select=status_select))
                    
                    # Add section to details panel
                    details_panel.append(close_approval_section)
            
            # Check if this is an active approval and user has permission to manage approvals
            if status in ['PENDING', 'IN_PROGRESS'] and permissions.user_has_permission(self.user, "MANAGE_APPROVALS"):
                # Add admin actions section
                admin_actions = pn.Column(
                    pn.pane.Markdown("## Approval Management"),
                    pn.Row(
                        pn.widgets.Button(name="Add Approver", button_type="default", width=120,
                                        on_click=lambda e: self._show_add_approver_form(approval_uid)),
                        pn.widgets.Button(name="Extend Deadline", button_type="default", width=120,
                                        on_click=lambda e: self._show_extend_approval_deadline_form(approval_uid)),
                        pn.widgets.Button(name="Cancel Review", button_type="danger", width=120,
                                        on_click=lambda e: self._show_cancel_approval_form(approval_uid)),
                        sizing_mode='stretch_width'
                    ),
                    sizing_mode='stretch_width',
                    styles={'background':'#f8f9fa'},
                    css_classes=['p-3', 'border', 'rounded']
                )
                
                details_panel.append(admin_actions)
            
            #logger.info(f"ready to update approval details with {details_panel}")
            # Update the approval details area
            logger.info("available tabs: %s", self.tabs)
            details_area = self.approvals_tab
            logger.info(f"Details area found: {details_area}")
            if details_area:
                # Find if there's an existing approval details area
                approval_details_found = False
                for i, item in enumerate(details_area):
                    if isinstance(item, pn.Column) and (
                        (hasattr(item, 'name') and item.name == 'approval_details') or
                        (len(item) >= 1 and isinstance(item[0], pn.pane.Markdown) and "Approval Details" in item[0].object)
                    ):
                        # Replace existing approval details area
                        details_area[i] = details_panel
                        #details_panel.name = 'approval_details'
                        approval_details_found = True
                        break
                
                # If not found, append it
                if not approval_details_found:
                    #details_panel.name = 'approval_details'
                    details_area.append(details_panel)
            else:
                # Log error when Reviews tab not found
                logger.error("Could not find Approvals tab to update approval details")
        
        except Exception as e:
            self.notification_area.object = f"**Error:** {str(e)}"
            logger.error(f"Error in _approval_selected: {e}")
            logger.error(f"Traceback: {traceback.format_exc()}")
    
    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
        
        # Show loading message
        self.notification_area.object = "**Getting document URL...**"
        
        try:
            # Import the document controller function
            from CDocs.controllers.document_controller import get_document_edit_url
            
            # Call API to get download URL
            result = get_document_edit_url(
                user=self.user,
                document_uid=self.document_uid
            )
            
            if result.get('success'):
                download_url = result.get('edit_url')
                file_type = result.get('file_type', 'Document')
                
                # Create a clickable link to open the document
                self.notification_area.object = f"""
                **Document ready for viewing!**
                

                Click this link to view the {file_type}: 
                <a href="{download_url}" target="_blank">Open in FileCloud Viewer</a>
                
                This link will expire in 24 hours.
                """
            else:
                self.notification_area.object = f"**Error:** {result.get('message', 'Unable to get document URL')}"
        
        except Exception as e:
            import traceback
            logger.error(f"Error viewing document: {e}")
            logger.error(traceback.format_exc())
            self.notification_area.object = f"**Error viewing document:** {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
            
            # Update sharing permissions for document based on new review cycle
            from CDocs.controllers.share_controller import manage_document_permissions
            doc = ControlledDocument(uid=doc.uid)
            permission_result = manage_document_permissions(doc)

            # 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_input):
        """
        Format date string or object for display, handling multiple input formats.
        
        Args:
            date_input: Date input which could be:
                - String in various formats
                - datetime.datetime object
                - Neo4j DateTime object
                - None
                
        Returns:
            Formatted date string (YYYY-MM-DD) or None if input is invalid
        """
        if not date_input:
            return None
            
        try:
            # Handle Neo4j DateTime objects
            if hasattr(date_input, '__class__') and date_input.__class__.__name__ == 'DateTime':
                # Convert Neo4j DateTime to Python datetime
                try:
                    date_obj = datetime(
                        year=date_input.year, 
                        month=date_input.month, 
                        day=date_input.day,
                        hour=getattr(date_input, 'hour', 0), 
                        minute=getattr(date_input, 'minute', 0), 
                        second=getattr(date_input, 'second', 0)
                    )
                    return date_obj.strftime('%Y-%m-%d')
                except (AttributeError, ValueError):
                    # Fall back to string representation if conversion fails
                    return str(date_input)
                    
            # If it's already a datetime object
            if isinstance(date_input, datetime):
                return date_input.strftime('%Y-%m-%d')
                
            # If it's a date object
            if isinstance(date_input, date) and not isinstance(date_input, datetime):
                return date_input.strftime('%Y-%m-%d')
                
            # For string inputs, try multiple formats
            if isinstance(date_input, str):
                # Try ISO format first (most common in the application)
                try:
                    date_obj = datetime.fromisoformat(date_input)
                    return date_obj.strftime('%Y-%m-%d')
                except ValueError:
                    pass
                    
                # Try parsing with pandas (handles many formats)
                try:
                    import pandas as pd
                    date_obj = pd.to_datetime(date_input)
                    return date_obj.strftime('%Y-%m-%d')
                except (ValueError, ImportError):
                    pass
                    
                # Try some common formats explicitly
                formats_to_try = [
                    '%Y-%m-%d',           # 2023-05-20
                    '%d/%m/%Y',           # 20/05/2023
                    '%m/%d/%Y',           # 05/20/2023
                    '%Y/%m/%d',           # 2023/05/20
                    '%d-%m-%Y',           # 20-05-2023
                    '%m-%d-%Y',           # 05-20-2023
                    '%b %d, %Y',          # May 20, 2023
                    '%d %b %Y',           # 20 May 2023
                    '%Y-%m-%dT%H:%M:%S',  # 2023-05-20T14:30:45
                    '%Y-%m-%dT%H:%M:%SZ'  # 2023-05-20T14:30:45Z
                ]
                
                for date_format in formats_to_try:
                    try:
                        date_obj = datetime.strptime(date_input, date_format)
                        return date_obj.strftime('%Y-%m-%d')
                    except ValueError:
                        continue
                
                # If all parsing attempts fail, return the original string
                return date_input
                
            # For unexpected types, convert to string
            return str(date_input)
            
        except Exception as e:
            logger.debug(f"Error formatting date '{date_input}': {e}")
            return str(date_input) if date_input else None
    
    def _get_status_color(self, status_code):
        """Get color for document status"""
        return settings.get_status_color(status_code)
    
    def _show_review_form(self, event=None):
        """Show form to start a review cycle"""
        self.notification_area.object = "Loading review form..."
    
        try:
            # Create form elements
            from CDocs.models.user_extensions import DocUser
            
            # Get all users who can be reviewers
            reviewers = []
            try:
                # Directly query users since get_potential_reviewers doesn't exist
                from CDocs.db.db_operations import run_query  
                
                reviewers_result = run_query(
                    """
                    MATCH (u:User)
                    WHERE (u.UID <> $current_user_uid) and not('Template' in labels(u))
                    RETURN u.UID as uid, u.Name as name, u.Role as role, u.Department as department
                    ORDER BY u.Name
                    """, 
                    {"current_user_uid": self.user.uid}
                )
                
                reviewers = [{"uid": r["uid"], "name": r["name"], "role": r.get("role", ""), 
                            "department": r.get("department", "")} for r in reviewers_result]
                            
            except Exception as e:
                # If db query fails, use a simple fallback with an empty list
                logger.warning(f"Error fetching reviewers: {e}")
                reviewers = []
            
            # Modified: Create a DataFrame with reviewer data including instructions column
            reviewer_data = []
            for r in reviewers:
                # Handle different possible data structures
                if isinstance(r, dict):
                    uid = r.get("uid", "")
                    name = r.get("name", "Unknown")
                    role = r.get("role", "")
                    display_text = f"{name} ({role})" if role else name
                    reviewer_data.append({
                        "Reviewer": display_text,
                        "UID": uid,
                        "instructions": ""  # Add empty instructions field for each reviewer
                    })
                else:
                    # Fallback for non-dictionary items
                    logger.warning(f"Unexpected reviewer data format: {type(r)}")
            
            # Create DataFrame for reviewer table
            df = pd.DataFrame(reviewer_data)
            
            # Create tabulator with selection and instructions column
            reviewer_table = pn.widgets.Tabulator(
                df,
                selection=[],  # Empty list for selection
                pagination='local',
                page_size=6,
                height=250,
                width=400,
                selectable='checkbox',
                formatters={"instructions": {"type": "input"}},
                hidden_columns=['UID']
            )
    
            # Create sequential checkbox with callback to show/hide order controls
            sequential_check = pn.widgets.Checkbox(
                name="Sequential Review",
                value=False
            )
            
            # Container for sequence order controls
            sequence_controls = pn.Column(
                pn.pane.Markdown("### Set Review Sequence"),
                pn.pane.Markdown("Select reviewers and use buttons to arrange them in order:"),
                pn.Row(
                    pn.widgets.Button(name="⬆️ Move Up", width=120, button_type="default"),
                    pn.widgets.Button(name="⬇️ Move Down", width=120, button_type="default"),
                    align='center'
                ),
                visible=False,
                styles={'background': '#f5f5f5'},
                css_classes=['p-2', 'border', 'rounded']
            )
            
            # Show/hide sequence controls based on sequential checkbox
            def toggle_sequence_controls(event):
                sequence_controls.visible = event.new
                
            sequential_check.param.watch(toggle_sequence_controls, 'value')
            
            # Create due date picker (default to 2 weeks from now)
            default_due_date = (datetime.now() + timedelta(days=14)).date()
            due_date_picker = pn.widgets.DatePicker(
                name="Due Date",
                value=default_due_date,
                width=200
            )
            
            # Create review type dropdown
            review_type_select = pn.widgets.Select(
                name="Review Type",
                options=settings.REVIEW_TYPES,
                value="STANDARD",
                width=200
            )
            
            # Required approval percentage (default 100%)
            approval_pct = pn.widgets.IntSlider(
                name="Required Approval Percentage",
                start=1,
                end=100,
                value=100,
                step=1,
                width=200
            )
            
            # Instructions textarea
            instructions_input = pn.widgets.TextAreaInput(
                name="Instructions for Reviewers",
                placeholder="Enter general instructions for all reviewers",
                width=400,
                height=100
            )
            
            # Create instruction help text
            instruction_help = pn.pane.Markdown("""
            **Note:** You can add specific instructions for each reviewer by:
            1. Clicking on a cell in the Instructions column
            2. Typing the specific instructions for that reviewer
            3. Pressing Enter when done
            """)
            
            # Create start button
            start_btn = pn.widgets.Button(
                name="Start Review Cycle",
                button_type="success",
                width=150
            )
            
            # Create cancel button
            cancel_btn = pn.widgets.Button(
                name="Cancel",
                button_type="default",
                width=120
            )
            
            # Function to move reviewers up/down in sequence
            def move_reviewer(event):
                selected = reviewer_table.selection
                if not selected or len(selected) == 0:  # Additional check for empty list
                    return
                    
                df = reviewer_table.value
                row_idx = selected[0]
                
                if event.obj.name == "⬆️ Move Up" and row_idx > 0:
                    # Swap with row above
                    df.iloc[row_idx-1:row_idx+1] = df.iloc[row_idx-1:row_idx+1].iloc[::-1].reset_index(drop=True)
                elif event.obj.name == "⬇️ Move Down" and row_idx < len(df)-1:
                    # Swap with row below
                    df.iloc[row_idx:row_idx+2] = df.iloc[row_idx:row_idx+2].iloc[::-1].reset_index(drop=True)
                    
                reviewer_table.selection = []  # Clear selection
                reviewer_table.value = df  # Update the table values
            
            # Add event handlers to move buttons
            sequence_controls.objects[2][0].on_click(move_reviewer)
            sequence_controls.objects[2][1].on_click(move_reviewer)
            
            # Create form layout with sequence controls and instructions help
            review_form = pn.Column(
                pn.pane.Markdown("# Start Review Cycle"),
                pn.pane.Markdown(f"## {self.doc_number}: {self.doc_title}"),
                pn.pane.Markdown("### Select Reviewers"),
                reviewer_table,
                instruction_help,  # Add help text for reviewer-specific instructions
                sequential_check,
                sequence_controls,
                pn.layout.Divider(),
                pn.Row(
                    review_type_select,
                    due_date_picker
                ),
                pn.Row(
                    pn.Column(
                        pn.pane.Markdown("### Required Approval"),
                        approval_pct
                    )
                ),
                pn.pane.Markdown("### General Instructions"),
                instructions_input,
                pn.Row(
                    cancel_btn,
                    start_btn,
                    align='end'
                ),
                styles={'background':'#f8f9fa'},
                css_classes=['p-3', 'border', 'rounded'],
                width=500
            )
            
            # Set up event handlers
            start_btn.on_click(lambda event: self._start_review_cycle(
                reviewer_table=reviewer_table,
                due_date=due_date_picker.value,
                review_type=review_type_select.value,
                sequential=sequential_check.value,
                required_approval_percentage=approval_pct.value,
                instructions=instructions_input.value
            ))
            
            cancel_btn.on_click(self._load_document)
            
            # Show the form
            if self.template and hasattr(self.template, 'main'):
                self.tabs.clear()
                self.template.main.clear()
                self.template.main.append(self.notification_area)
                self.template.main.append(review_form)
            else:
                self.main_content.clear()
                self.main_content.append(self.notification_area)
                self.main_content.append(review_form)
            
            # Clear notification
            self.notification_area.object = ""
            
        except Exception as e:
            logger.error(f"Error showing review form: {e}")
            logger.error(traceback.format_exc())
            self.notification_area.object = f"**Error showing review form:** {str(e)}"

    def _start_review_cycle(self, reviewer_table, due_date, review_type, sequential, 
                          required_approval_percentage, instructions):
        """Start a new review cycle for the current document"""
        # Get selected reviewers from table with their sequence
        selected_rows = reviewer_table.selection
        if not selected_rows:
            self.notification_area.object = "**Error:** At least one reviewer must be selected"
            return
            
        # Extract reviewer UIDs in order from the table
        df = reviewer_table.value
        
        # Extract reviewer UIDs and instructions
        reviewer_uids = []
        reviewer_instructions = {}
        
        for idx in selected_rows:
            row = df.iloc[idx]
            uid = row['UID']
            reviewer_uids.append(uid)
            
            # Get specific instructions for this reviewer if available
            if 'instructions' in row and row['instructions']:
                reviewer_instructions[uid] = row['instructions']
        
        if not reviewer_uids or len(reviewer_uids) == 0:
            self.notification_area.object = "**Error:** At least one reviewer must be selected"
            return
        
        # Convert due_date string to datetime if needed
        if isinstance(due_date, str):
            try:
                due_date = datetime.fromisoformat(due_date)
            except ValueError:
                self.notification_area.object = "**Error:** Invalid date format"
                return
        elif isinstance(due_date, date):
            # Convert date to datetime at end of day
            due_date = datetime.combine(due_date, datetime.max.time())
        
        # Show processing message
        self.notification_area.object = "**Starting review cycle...**"
        
        try:
            # Call controller to create review cycle with reviewer-specific instructions
            result = create_review_cycle(
                user=self.user,
                document_uid=self.document_uid,
                reviewer_uids=reviewer_uids,  # Already in sequence order from table
                reviewer_instructions=reviewer_instructions,  # Added parameter with per-reviewer instructions
                due_date=due_date,
                instructions=instructions,
                review_type=review_type,
                sequential=sequential,
                required_approval_percentage=required_approval_percentage
            )
            
            if result['success']:
                self.notification_area.object = "**Success:** Review cycle started successfully"
                # Reload document after a short delay
                self._load_document()
            else:
                self.notification_area.object = f"**Error:** {result.get('message', 'Unknown error')}"
        
        except ResourceNotFoundError as e:
            self.notification_area.object = f"**Error:** {str(e)}"
        except ValidationError as e:
            self.notification_area.object = f"**Validation Error:** {str(e)}"
        except PermissionError as e:
            self.notification_area.object = f"**Permission Error:** {str(e)}"
        except BusinessRuleError as e:
            self.notification_area.object = f"**Business Rule Error:** {str(e)}"
        except Exception as e:
            logger.error(f"Error starting review cycle: {e}")
            logger.error(traceback.format_exc())
            self.notification_area.object = f"**Error:** An unexpected error occurred"

    def _start_approval_cycle(self, approver_table, due_date, approval_type, sequential, 
                          required_approval_percentage, instructions):
        """Start a new approval cycle for the current document"""
        # Get selected approvers from table with their sequence
        selected_rows = approver_table.selection
        if not selected_rows:
            self.notification_area.object = "**Error:** At least one approver must be selected"
            return
            
        # Extract approver UIDs in order from the table
        df = approver_table.value
        
        # Extract approver UIDs and instructions
        approver_uids = []
        approver_instructions = {}
        
        for idx in selected_rows:
            row = df.iloc[idx]
            uid = row['UID']
            approver_uids.append(uid)
            
            # Get specific instructions for this approver if available
            if 'instructions' in row and row['instructions']:
                approver_instructions[uid] = row['instructions']
        
        if not approver_uids or len(approver_uids) == 0:
            self.notification_area.object = "**Error:** At least one approver must be selected"
            return
        
        # Convert due_date string to datetime if needed
        if isinstance(due_date, str):
            try:
                due_date = datetime.fromisoformat(due_date)
            except ValueError:
                self.notification_area.object = "**Error:** Invalid date format"
                return
        elif isinstance(due_date, date):
            # Convert date to datetime at end of day
            due_date = datetime.combine(due_date, datetime.max.time())
        
        # Show processing message
        self.notification_area.object = "**Starting approval cycle...**"
        
        try:
            # Call controller to create approval cycle with approver-specific instructions
            result = create_approval_cycle(
                user=self.user,
                document_uid=self.document_uid,
                approver_uids=approver_uids,  # Already in sequence order from table
                approver_instructions=approver_instructions,  # Added parameter with per-approver instructions
                due_date=due_date,
                instructions=instructions,
                approval_type=approval_type,
                sequential=sequential,
                required_approval_percentage=required_approval_percentage
            )
            
            if result['success']:
                self.notification_area.object = "**Success:** Review cycle started successfully"
                # Reload document after a short delay
                self._load_document()
            else:
                self.notification_area.object = f"**Error:** {result.get('message', 'Unknown error')}"
        
        except ResourceNotFoundError as e:
            self.notification_area.object = f"**Error:** {str(e)}"
        except ValidationError as e:
            self.notification_area.object = f"**Validation Error:** {str(e)}"
        except PermissionError as e:
            self.notification_area.object = f"**Permission Error:** {str(e)}"
        except BusinessRuleError as e:
            self.notification_area.object = f"**Business Rule Error:** {str(e)}"
        except Exception as e:
            logger.error(f"Error starting approval cycle: {e}")
            logger.error(traceback.format_exc())
            self.notification_area.object = f"**Error:** An unexpected error occurred"

    
    def _show_approval_form(self, event=None):
        """Show form to start a approval cycle"""
        self.notification_area.object = "Loading approval form..."
    
        try:
            # Create form elements
            from CDocs.models.user_extensions import DocUser
            
            # Get all users who can be approvers
            approvers = []
            try:
                # Directly query users since get_potential_approvers doesn't exist
                from CDocs.db.db_operations import run_query  
                
                approvers_result = run_query(
                    """
                    MATCH (u:User)
                    WHERE (u.UID <> $current_user_uid) and not('Template' in labels(u))
                    RETURN u.UID as uid, u.Name as name, u.Role as role, u.Department as department
                    ORDER BY u.Name
                    """, 
                    {"current_user_uid": self.user.uid}
                )
                
                approvers = [{"uid": r["uid"], "name": r["name"], "role": r.get("role", ""), 
                            "department": r.get("department", "")} for r in approvers_result]
                            
            except Exception as e:
                # If db query fails, use a simple fallback with an empty list
                logger.warning(f"Error fetching approvers: {e}")
                approvers = []
            
            # Modified: Create a DataFrame with approver data including instructions column
            approver_data = []
            for r in approvers:
                # Handle different possible data structures
                if isinstance(r, dict):
                    uid = r.get("uid", "")
                    name = r.get("name", "Unknown")
                    role = r.get("role", "")
                    display_text = f"{name} ({role})" if role else name
                    approver_data.append({
                        "Approver": display_text,
                        "UID": uid,
                        "instructions": ""  # Add empty instructions field for each approver
                    })
                else:
                    # Fallback for non-dictionary items
                    logger.warning(f"Unexpected approver data format: {type(r)}")
            
            # Create DataFrame for approver table
            df = pd.DataFrame(approver_data)
            
            # Create tabulator with selection and instructions column
            approver_table = pn.widgets.Tabulator(
                df,
                selection=[],  # Empty list for selection
                pagination='local',
                page_size=6,
                height=250,
                width=400,
                selectable='checkbox',
                formatters={"instructions": {"type": "input"}},
                hidden_columns=['UID']
            )
    
            # Create sequential checkbox with callback to show/hide order controls
            sequential_check = pn.widgets.Checkbox(
                name="Sequential Approval",
                value=False
            )
            
            # Container for sequence order controls
            sequence_controls = pn.Column(
                pn.pane.Markdown("### Set Approval Sequence"),
                pn.pane.Markdown("Select approvers and use buttons to arrange them in order:"),
                pn.Row(
                    pn.widgets.Button(name="⬆️ Move Up", width=120, button_type="default"),
                    pn.widgets.Button(name="⬇️ Move Down", width=120, button_type="default"),
                    align='center'
                ),
                visible=False,
                styles={'background': '#f5f5f5'},
                css_classes=['p-2', 'border', 'rounded']
            )
            
            # Show/hide sequence controls based on sequential checkbox
            def toggle_sequence_controls(event):
                sequence_controls.visible = event.new
                
            sequential_check.param.watch(toggle_sequence_controls, 'value')
            
            # Create due date picker (default to 2 weeks from now)
            default_due_date = (datetime.now() + timedelta(days=14)).date()
            due_date_picker = pn.widgets.DatePicker(
                name="Due Date",
                value=default_due_date,
                width=200
            )
            
            # Create approval type dropdown
            approval_type_select = pn.widgets.Select(
                name="Approval Type",
                options=settings.APPROVAL_TYPES,
                value="STANDARD",
                width=200
            )
            
            # Required approval percentage (default 100%)
            approval_pct = pn.widgets.IntSlider(
                name="Required Approval Percentage",
                start=1,
                end=100,
                value=100,
                step=1,
                width=200
            )
            
            # Instructions textarea
            instructions_input = pn.widgets.TextAreaInput(
                name="Instructions for Approvers",
                placeholder="Enter general instructions for all approvers",
                width=400,
                height=100
            )
            
            # Create instruction help text
            instruction_help = pn.pane.Markdown("""
            **Note:** You can add specific instructions for each approver by:
            1. Clicking on a cell in the Instructions column
            2. Typing the specific instructions for that approver
            3. Pressing Enter when done
            """)
            
            # Create start button
            start_btn = pn.widgets.Button(
                name="Start Approval Cycle",
                button_type="success",
                width=150
            )
            
            # Create cancel button
            cancel_btn = pn.widgets.Button(
                name="Cancel",
                button_type="default",
                width=120
            )
            
            # Function to move approvers up/down in sequence
            def move_approver(event):
                selected = approver_table.selection
                if not selected or len(selected) == 0:  # Additional check for empty list
                    return
                    
                df = approver_table.value
                row_idx = selected[0]
                
                if event.obj.name == "⬆️ Move Up" and row_idx > 0:
                    # Swap with row above
                    df.iloc[row_idx-1:row_idx+1] = df.iloc[row_idx-1:row_idx+1].iloc[::-1].reset_index(drop=True)
                elif event.obj.name == "⬇️ Move Down" and row_idx < len(df)-1:
                    # Swap with row below
                    df.iloc[row_idx:row_idx+2] = df.iloc[row_idx:row_idx+2].iloc[::-1].reset_index(drop=True)
                    
                approver_table.selection = []  # Clear selection
                approver_table.value = df  # Update the table values
            
            # Add event handlers to move buttons
            sequence_controls.objects[2][0].on_click(move_approver)
            sequence_controls.objects[2][1].on_click(move_approver)
            
            # Create form layout with sequence controls and instructions help
            approval_form = pn.Column(
                pn.pane.Markdown("# Start Approval Cycle"),
                pn.pane.Markdown(f"## {self.doc_number}: {self.doc_title}"),
                pn.pane.Markdown("### Select Approvers"),
                approver_table,
                instruction_help,  # Add help text for approver-specific instructions
                sequential_check,
                sequence_controls,
                pn.layout.Divider(),
                pn.Row(
                    approval_type_select,
                    due_date_picker
                ),
                pn.Row(
                    pn.Column(
                        pn.pane.Markdown("### Required Approval"),
                        approval_pct
                    )
                ),
                pn.pane.Markdown("### General Instructions"),
                instructions_input,
                pn.Row(
                    cancel_btn,
                    start_btn,
                    align='end'
                ),
                styles={'background':'#f8f9fa'},
                css_classes=['p-3', 'border', 'rounded'],
                width=500
            )
            
            # Set up event handlers
            start_btn.on_click(lambda event: self._start_approval_cycle(
                approver_table=approver_table,
                due_date=due_date_picker.value,
                approval_type=approval_type_select.value,
                sequential=sequential_check.value,
                required_approval_percentage=approval_pct.value,
                instructions=instructions_input.value
            ))
            
            cancel_btn.on_click(self._load_document)
            
            # Show the form
            if self.template and hasattr(self.template, 'main'):
                self.tabs.clear()
                self.template.main.clear()
                self.template.main.append(self.notification_area)
                self.template.main.append(approval_form)
            else:
                self.main_content.clear()
                self.main_content.append(self.notification_area)
                self.main_content.append(approval_form)
            
            # Clear notification
            self.notification_area.object = ""
            
        except Exception as e:
            logger.error(f"Error showing approval form: {e}")
            logger.error(traceback.format_exc())
            self.notification_area.object = f"**Error showing approval form:** {str(e)}"
    
    def _create_approver_step(self, title,user_options):
        """Helper to create a step card for approval workflow"""
       
        
        # Create a multi-select for approvers
        approvers_select = pn.widgets.MultiSelect(
            name='Approvers',
            options=user_options,
            size=5
        )
        
        # Step options
        all_approve_check = pn.widgets.Checkbox(
            name='All must approve',
            value=True
        )
        
        options_row = pn.Row(
            all_approve_check,
            name='Step Options'
        )
        
        # Create step card
        step_card = pn.Column(
            pn.pane.Markdown(f"### {title}"),
            approvers_select,
            options_row,
            styles={'background': '#f8f9fa'},
            css_classes=['p-3', 'border', 'rounded', 'mb-3'],
            sizing_mode='stretch_width'
        )
        
        # Store references directly as attributes on the object instead of using param
        step_card.approvers_select = approvers_select
        step_card.all_approve_check = all_approve_check
        
        return step_card
    
    def _get_user_options(self):
        """Get user options for approver selection"""

        from CDocs.models.user_extensions import DocUser
        try:
            potential_approvers = DocUser.get_users_by_role(role="APPROVER")
            user_options = {f"{u.name} ({u.username})" : u.uid for u in potential_approvers}
            return user_options
        except Exception as e:
            logger.error(f"Error getting users: {e}")
            return {}  # Return empty dict on error
    
    def _show_publish_form(self, event=None):
        """Show form to publish a document"""
        try:
            if not self.document:
                self.notification_area.object = "**Error:** No document loaded"
                return
                
            # Clear notification area
            self.notification_area.object = ""
            
            # Create form elements
            publish_comment = pn.widgets.TextAreaInput(
                name="Publication Comment",
                placeholder="Enter comment about this publication (optional)",
                rows=3,
                width=400
            )
            
            # Calculate next major version number from current version
            current_version = self.document.get('revision', '1.0')
            try:
                major_version = int(current_version.split('.')[0])
                next_version = f"{major_version + 1}.0"
            except (ValueError, IndexError):
                next_version = "1.0"  # Default if parsing fails
            
            version_info = pn.pane.Markdown(f"**Note:** This will create version **{next_version}** as the published version.")
            
            # Create checkbox for confirmation
            confirmation_checkbox = pn.widgets.Checkbox(
                name="I confirm this document is ready for publication",
                value=False
            )
            
            # Create submit and cancel buttons
            submit_btn = pn.widgets.Button(
                name="Publish Document",
                button_type="success",
                disabled=True,
                width=150
            )
            
            cancel_btn = pn.widgets.Button(
                name="Cancel",
                button_type="default",
                width=100
            )
            
            # Enable submit button only when confirmation checked
            def toggle_submit(event):
                submit_btn.disabled = not event.new
                
            confirmation_checkbox.param.watch(toggle_submit, 'value')
            
            # Set up callbacks
            cancel_btn.on_click(lambda event: self._load_document())  # Changed to reload the document
            submit_btn.on_click(lambda event: self._publish_document(
                publish_comment=publish_comment.value,
                form_panel=form_panel
            ))
            
            # Create form layout
            form_panel = pn.Column(
                pn.pane.Markdown("## Publish Document"),
                pn.pane.Markdown(f"You are about to publish **{self.document.get('title')}** (Document Number: {self.doc_number})"),
                version_info,
                pn.pane.Markdown("Publishing this document will:"),
                pn.pane.Markdown("1. Create a finalized PDF with signatures and audit trail"),
                pn.pane.Markdown("2. Mark the document as PUBLISHED"),
                pn.pane.Markdown("3. Make it available to all authorized users"),
                publish_comment,
                confirmation_checkbox,
                pn.Row(
                    submit_btn,
                    cancel_btn,
                    align='end'
                ),
                styles={'background': '#f8f9fa'},
                css_classes=['p-3', 'border', 'rounded'],
                width=500
            )
            
            # Clear display area and show form - THIS IS THE KEY FIX
            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(form_panel)
            else:
                # Embedded mode using main_content
                self.main_content.clear()
                self.main_content.append(self.notification_area)
                self.main_content.append(form_panel)
            
        except Exception as e:
            self.notification_area.object = f"**Error preparing publication form:** {str(e)}"
            logger.error(f"Error in _show_publish_form: {e}")
            logger.error(traceback.format_exc())

    def _publish_document(self, publish_comment, form_panel):
        """Handle document publication process"""
        try:
            # Show processing message
            self.notification_area.object = "**Processing:** Publishing document..."
            
            # Remove the form
            self.main_content.remove(form_panel)
            
            # Import publication controller
            from CDocs.controllers.document_controller import publish_document
            
            # Call controller function to publish the document
            result = publish_document(
                user=self.user,
                document_uid=self.document_uid,
                publish_comment=publish_comment
            )
            
            if result.get('success'):
                # Display success message with details
                self.notification_area.object = f"""
                **Success:** Document published successfully!
                
                The document is now published as version {result.get('new_version_number')}.
                
                A finalized PDF version has been created and all links will now point to this published version.
                """
                
                # Add reload button
                reload_btn = pn.widgets.Button(
                    name="Reload Document",
                    button_type="primary",
                    width=150
                )
                reload_btn.on_click(self._load_document)
                
                # Add button below notification
                button_row = pn.Row(reload_btn, align='center')
                self.main_content.insert(1, button_row)  # Insert after notification
            else:
                # Display error message
                self.notification_area.object = f"**Error:** {result.get('message', 'Unknown error during publication')}"
        
        except Exception as e:
            self.notification_area.object = f"**Error publishing document:** {str(e)}"
            logger.error(f"Error in _publish_document: {e}")
            logger.error(traceback.format_exc())
    
    def _show_archive_form(self, event=None):
        """Show form to archive the document"""
        try:
            if not self.document:
                self.notification_area.object = "**Error:** No document loaded"
                return
                
            # Clear notification area
            self.notification_area.object = ""
            
            # Create form elements
            archive_reason = pn.widgets.Select(
                name="Archive Reason",
                options=["Obsolete", "Superseded", "No Longer Required", "Regulatory Change", "Other"],
                width=400
            )
            
            archive_comment = pn.widgets.TextAreaInput(
                name="Archive Comment",
                placeholder="Enter additional details about archiving this document",
                rows=3,
                width=400
            )
            
            # Create checkbox for confirmation
            confirmation_checkbox = pn.widgets.Checkbox(
                name="I confirm this document should be archived",
                value=False
            )
            
            # Create submit and cancel buttons
            submit_btn = pn.widgets.Button(
                name="Archive Document",
                button_type="danger",
                disabled=True,
                width=150
            )
            
            cancel_btn = pn.widgets.Button(
                name="Cancel",
                button_type="default",
                width=100
            )
            
            # Enable submit button only when confirmation checked
            def toggle_submit(event):
                submit_btn.disabled = not event.new
                
            confirmation_checkbox.param.watch(toggle_submit, 'value')
            
            # Set up callbacks
            cancel_btn.on_click(lambda event: self._load_document())
            submit_btn.on_click(lambda event: self._archive_document(
                archive_reason=archive_reason.value,
                archive_comment=archive_comment.value,
                form_panel=form_panel
            ))
            
            # Create form layout
            form_panel = pn.Column(
                pn.pane.Markdown("## Archive Document"),
                pn.pane.Markdown(f"You are about to archive **{self.document.get('title')}** (Document Number: {self.doc_number})"),
                pn.pane.Markdown("**Warning:** This will archive all versions of the document."),
                pn.pane.Markdown("Archiving this document will:"),
                pn.pane.Markdown("1. Change the status of all versions to ARCHIVED"),
                pn.pane.Markdown("2. Move the published PDF to an archive location"),
                pn.pane.Markdown("3. Make the document unavailable for normal use"),
                archive_reason,
                archive_comment,
                confirmation_checkbox,
                pn.Row(
                    submit_btn,
                    cancel_btn,
                    align='end'
                ),
                styles={'background': '#f8f9fa'},
                css_classes=['p-3', 'border', 'rounded'],
                width=500
            )
            
            # Clear display area and show form
            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(form_panel)
            else:
                # Embedded mode using main_content
                self.main_content.clear()
                self.main_content.append(self.notification_area)
                self.main_content.append(form_panel)
            
        except Exception as e:
            self.notification_area.object = f"**Error preparing archive form:** {str(e)}"
            logger.error(f"Error in _show_archive_form: {e}")
            logger.error(traceback.format_exc())

    def _archive_document(self, archive_reason, archive_comment, form_panel):
        """Handle document archiving process"""
        try:
            # Validate input
            if not archive_reason:
                self.notification_area.object = "**Error:** Please select an archive reason"
                return
                
            # Show processing message
            self.notification_area.object = "**Processing:** Archiving document..."
            
            # Remove the form
            if self.main_content and form_panel in self.main_content:
                self.main_content.remove(form_panel)
            
            # Import archive controller
            from CDocs.controllers.document_controller import archive_document
            
            # Call controller function to archive the document
            result = archive_document(
                user=self.user,
                document_uid=self.document_uid,
                archive_reason=archive_reason,
                archive_comment=archive_comment
            )
            
            if result.get('success'):
                # Display success message with details
                self.notification_area.object = f"""
                **Success:** Document archived successfully!
                
                The document and all its versions have been archived.
                The published PDF has been moved to the archive location.
                """
                
                # Add reload button
                reload_btn = pn.widgets.Button(
                    name="Reload Document",
                    button_type="primary",
                    width=150
                )
                reload_btn.on_click(self._load_document)
                
                # Add button below notification
                button_row = pn.Row(reload_btn, align='center')
                self.main_content.insert(1, button_row)  # Insert after notification
            else:
                # Display error message
                self.notification_area.object = f"**Error:** {result.get('message', 'Unknown error during archiving')}"
        
        except Exception as e:
            self.notification_area.object = f"**Error archiving document:** {str(e)}"
            logger.error(f"Error in _archive_document: {e}")
            logger.error(traceback.format_exc())
    
    def _show_clone_form(self, event=None):
        """Show form to clone the document with custom number option"""
        try:
            # Get current document information
            document_uid = getattr(self, 'document_uid', None)
            doc_title = getattr(self, 'doc_title', 'Unknown Document')
            doc_type = getattr(self, 'doc_type', '')
            department = getattr(self, 'department', '')
            
            if not document_uid:
                self.notification_area.object = "**Error:** No document selected for cloning"
                return
            
            # Clear main content and show clone form
            self.main_content.clear()
            self.notification_area.object = "Preparing clone form..."
            
            # Create form widgets
            title_input = pn.widgets.TextInput(
                name="New Title", 
                value=f"Copy of {doc_title}",
                width=400
            )
            
            # Get document types and departments from settings
            from CDocs.config import settings
            
            # Document type selector - use full names as options
            doc_type_options = list(settings.DOCUMENT_TYPES.keys())
            doc_type_select = pn.widgets.Select(
                name="Document Type", 
                options=doc_type_options,
                value=settings.get_document_type_name(doc_type) if doc_type else doc_type_options[0],
                width=200
            )
            
            # Department selector - use full names as options
            dept_options = list(settings.DEPARTMENTS.keys())
            department_select = pn.widgets.Select(
                name="Department", 
                options=dept_options,
                value=settings.get_department_name(department) if department else dept_options[0],
                width=200
            )
            
            # Custom document number option
            custom_number_toggle = pn.widgets.Switch(name="Use Custom Document Number", value=False)
            
            custom_number_input = pn.widgets.TextInput(
                name="Document Number", 
                placeholder="e.g., CUSTOM-DOC-001",
                width=300,
                visible=False
            )
            
            validation_status = pn.pane.Markdown("", width=400, visible=False)
            
            # Toggle visibility handler
            def toggle_custom_number(event):
                custom_number_input.visible = event.new
                validation_status.visible = event.new
                if not event.new:
                    custom_number_input.value = ""
                    validation_status.object = ""
            
            custom_number_toggle.param.watch(toggle_custom_number, 'value')
            
            # Real-time validation handler
            def validate_number(event):
                if event.new and len(event.new.strip()) > 0:
                    try:
                        from CDocs.controllers.document_controller import validate_document_number
                        result = validate_document_number(event.new.strip().upper())
                        
                        if result.get('valid', False):
                            validation_status.object = f"✅ {result.get('message', 'Valid')}"
                            validation_status.styles = {'color': 'green'}
                        else:
                            validation_status.object = f"❌ {result.get('message', 'Invalid')}"
                            validation_status.styles = {'color': 'red'}
                    except Exception as e:
                        validation_status.object = f"❌ Error validating: {str(e)}"
                        validation_status.styles = {'color': 'red'}
                else:
                    validation_status.object = ""
            
            custom_number_input.param.watch(validate_number, 'value')
            
            # Clone type selector - FIXED: Remove the 'inline' parameter
            clone_type = pn.widgets.RadioButtonGroup(
                name="Clone Type",
                options=[
                    ("New Document", "new_document"),
                    ("New Revision", "new_revision")
                ],
                value="new_document"
            )
            
            # Content options
            include_content = pn.widgets.Checkbox(name="Include Current Content", value=True)
            include_comments = pn.widgets.Checkbox(name="Include Comments/Reviews", value=False)
            include_metadata = pn.widgets.Checkbox(name="Include Custom Metadata", value=True)
            
            # Comment for clone operation
            comment_input = pn.widgets.TextAreaInput(
                name="Clone Comment",
                placeholder="Reason for cloning (optional)",
                rows=3,
                width=400
            )
            
            # Additional properties
            additional_props = pn.widgets.TextAreaInput(
                name="Additional Properties (JSON)",
                placeholder='{"custom_field": "value"}',
                rows=3,
                width=400
            )
            
            # Buttons
            submit_btn = pn.widgets.Button(name="Clone Document", button_type="success", width=150)
            cancel_btn = pn.widgets.Button(name="Cancel", button_type="default", width=100)
            
            # Create clone form layout
            clone_form = pn.Column(
                pn.pane.Markdown(f"# Clone Document: {doc_title}"),
                pn.pane.Markdown(f"**Source Document:** {getattr(self, 'doc_number', 'Unknown')} - {doc_title}"),
                pn.Spacer(height=10),
                
                pn.pane.Markdown("### Basic Information"),
                title_input,
                pn.Row(doc_type_select, department_select),
                
                pn.Spacer(height=10),
                pn.pane.Markdown("### Document Number"),
                custom_number_toggle,
                custom_number_input,
                validation_status,
                
                pn.Spacer(height=10),
                pn.pane.Markdown("### Clone Options"),
                clone_type,
                pn.Column(
                    include_content,
                    include_comments,
                    include_metadata,
                    margin=(10, 0, 0, 20)
                ),
                
                pn.Spacer(height=10),
                pn.pane.Markdown("### Comments & Properties"),
                comment_input,
                additional_props,
                
                pn.Spacer(height=20),
                pn.Row(
                    cancel_btn,
                    pn.Spacer(width=20),
                    submit_btn,
                    align='end'
                ),
                
                width=700,
                styles={'background': '#f8f9fa', 'padding': '20px', 'border-radius': '5px'},
                margin=(20, 20, 20, 20)
            )
            
            # Define submit handler
            def submit_clone(event):
                try:
                    # Validate form
                    if not title_input.value.strip():
                        self.notification_area.object = "**Error:** Document title is required"
                        return
                    
                    # Validate custom document number if provided
                    custom_doc_number = None
                    if custom_number_toggle.value and custom_number_input.value:
                        custom_doc_number = custom_number_input.value.strip().upper()
                        
                        # Final validation
                        try:
                            from CDocs.controllers.document_controller import validate_document_number
                            validation_result = validate_document_number(custom_doc_number)
                            if not validation_result.get('valid', False):
                                self.notification_area.object = f"**Error:** {validation_result.get('message', 'Invalid document number')}"
                                return
                        except Exception as e:
                            self.notification_area.object = f"**Error:** Failed to validate document number: {str(e)}"
                            return
                    
                    # Parse additional properties if provided
                    additional_properties = {}
                    if additional_props.value.strip():
                        try:
                            import json
                            additional_properties = json.loads(additional_props.value)
                        except json.JSONDecodeError as e:
                            self.notification_area.object = f"**Error:** Invalid JSON in additional properties: {str(e)}"
                            return
                    
                    # Convert full names to codes
                    from CDocs.config import settings
                    doc_type_full_name = doc_type_select.value
                    doc_type_code = settings.get_document_type_code(doc_type_full_name)
                    
                    dept_full_name = department_select.value
                    department_code = settings.get_department_code(dept_full_name)
                    
                    # Determine clone type and parameters
                    clone_as_new_revision = (clone_type.value == "new_revision")
                    
                    # Add clone options to additional properties
                    if comment_input.value.strip():
                        additional_properties['clone_comment'] = comment_input.value.strip()
                    
                    additional_properties.update({
                        'include_content': include_content.value,
                        'include_comments': include_comments.value,
                        'include_metadata': include_metadata.value,
                        'clone_source_uid': document_uid,
                        'clone_source_title': doc_title
                    })
                    
                    # Show progress
                    self.notification_area.object = "**Cloning document...**"
                    
                    # Call clone function
                    from CDocs.controllers.document_controller import clone_document
                    
                    result = clone_document(
                        user=self.user,
                        document_uid=document_uid,
                        new_title=title_input.value.strip(),
                        doc_type=doc_type_code,
                        department=department_code,
                        include_content=include_content.value,
                        clone_as_new_revision=clone_as_new_revision,
                        additional_properties=additional_properties,
                        custom_doc_number=custom_doc_number  # Pass custom number
                    )
                    
                    if result.get('success', False):
                        # Get the new document information
                        new_doc_uid = result.get('document_uid')
                        new_doc_number = result.get('document_number', 'Unknown')
                        
                        # Show success message
                        success_msg = f"**Success:** Document cloned as {new_doc_number}"
                        if custom_doc_number:
                            success_msg += f" (custom number)"
                        if clone_as_new_revision:
                            success_msg += f" (new revision)"
                        
                        self.notification_area.object = success_msg
                        
                        # Update document permissions for the new document
                        try:
                            from CDocs.controllers.share_controller import manage_document_permissions
                            from CDocs.models.document import ControlledDocument
                            new_document = ControlledDocument(uid=new_doc_uid)
                            permission_result = manage_document_permissions(new_document)
                            logger.debug(f"Permission management result for cloned document: {permission_result}")
                        except Exception as perm_error:
                            logger.warning(f"Error managing permissions for cloned document: {perm_error}")
                            # Don't fail the whole operation for permission errors
                        
                        # Navigate to the new document or back to dashboard
                        if new_doc_uid and hasattr(self, 'parent_app') and self.parent_app:
                            # Option 1: Navigate to the cloned document
                            self.parent_app.load_document(new_doc_uid)
                        else:
                            # Option 2: Go back to dashboard
                            if hasattr(self, 'parent_app') and self.parent_app:
                                self.parent_app.show_dashboard()
                            else:
                                self._load_document()
                    else:
                        # Show error message
                        error_msg = result.get('message', 'Unknown error occurred')
                        self.notification_area.object = f"**Error:** Failed to clone document: {error_msg}"
                    
                except Exception as e:
                    logger.error(f"Error cloning document: {e}")
                    import traceback
                    logger.error(traceback.format_exc())
                    self.notification_area.object = f"**Error cloning document:** {str(e)}"
            
            def cancel_clone(event):
                # Return to document view
                self.notification_area.object = ""
                self._load_document()
            
            # Bind handlers
            submit_btn.on_click(submit_clone)
            cancel_btn.on_click(cancel_clone)
            
            # Add form to main content
            self.main_content.append(clone_form)
            
            # Clear notification after form is shown
            self.notification_area.object = ""
            
        except Exception as e:
            logger.error(f"Error showing clone form: {e}")
            import traceback
            logger.error(traceback.format_exc())
            self.notification_area.object = f"**Error showing clone form:** {str(e)}"
            
            # Return to document view on error
            self._load_document()
    
    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

_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

_manage_training(self, event)

Purpose: Navigate to training management for this document.

Parameters:

  • event: Parameter

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

_compare_versions(self, versions_table, versions_df)

Purpose: Compare two selected document versions using LLM. Args: versions_table: The tabulator widget containing the selections versions_df: The DataFrame with version data

Parameters:

  • versions_table: Parameter
  • versions_df: Parameter

Returns: None

_get_version_content(self, version_uid)

Purpose: Get the content of a specific document version as text. Args: version_uid: UID of the version Returns: Document content as text or None if retrieval fails

Parameters:

  • version_uid: Parameter

Returns: See docstring for return details

_extract_text_from_pdf(self, file_content)

Purpose: Extract text from PDF content

Parameters:

  • file_content: Parameter

Returns: None

_extract_text_from_word(self, file_content, file_ext)

Purpose: Extract text from Word document content

Parameters:

  • file_content: Parameter
  • file_ext: Parameter

Returns: None

_extract_text_from_powerpoint(self, file_content, file_ext)

Purpose: Extract text from PowerPoint content

Parameters:

  • file_content: Parameter
  • file_ext: Parameter

Returns: None

_extract_text_from_excel(self, file_content, file_ext)

Purpose: Extract text from Excel content

Parameters:

  • file_content: Parameter
  • file_ext: Parameter

Returns: None

_convert_to_pdf(self, input_file)

Purpose: Convert a document to PDF using LibreOffice

Parameters:

  • input_file: Parameter

Returns: None

_show_comparison_results(self, comparison_result, version_numbers)

Purpose: Display comparison results in a custom modal dialog using FloatPanel for Panel 1.6.1 compatibility. Args: comparison_result: Results from the comparison version_numbers: Version numbers that were compared

Parameters:

  • comparison_result: Parameter
  • version_numbers: Parameter

Returns: None

floater_close(self, event)

Purpose: Close a floating panel Args: event: The event object containing the window and floater properties

Parameters:

  • event: Parameter

Returns: None

_create_versions_tab(self)

Purpose: Create a tab for document versions, with version selection and comparison functionality

Returns: None

_edit_document_online(self, event)

Purpose: Get edit URL from FileCloud and open it

Parameters:

  • event: Parameter

Returns: None

_convert_to_pdf(self, event)

Purpose: Convert the current document version to PDF

Parameters:

  • event: Parameter

Returns: None

_create_reviews_tab(self)

Purpose: Create the reviews tab content, hiding reviews for archived versions except current one

Returns: None

_is_archived_version(self, version_uid)

Purpose: Check if a version is archived

Parameters:

  • version_uid: Parameter

Returns: None

_review_selected(self, event)

Purpose: Handle review selection from table with support for both selection and cell click events

Parameters:

  • event: Parameter

Returns: None

_close_review_cycle(self, event, review_uid, update_status_checkbox, status_select)

Purpose: Close a review cycle and optionally update document status

Parameters:

  • event: Parameter
  • review_uid: Parameter
  • update_status_checkbox: Parameter
  • status_select: Parameter

Returns: None

_close_approval_cycle(self, event, approval_uid, update_status_checkbox, status_select)

Purpose: Close a review cycle and optionally update document status

Parameters:

  • event: Parameter
  • approval_uid: Parameter
  • update_status_checkbox: Parameter
  • status_select: Parameter

Returns: None

_show_add_approver_form(self, approval_uid)

Purpose: Show form to add an approver to an active approval cycle

Parameters:

  • approval_uid: Parameter

Returns: None

_show_extend_approval_deadline_form(self, approval_uid)

Purpose: Show form to extend approval deadline

Parameters:

  • approval_uid: Parameter

Returns: None

_show_extend_review_deadline_form(self, review_uid)

Purpose: Show form to extend review deadline

Parameters:

  • review_uid: Parameter

Returns: None

_show_add_reviewer_form(self, review_uid)

Purpose: Show form to add a reviewer to an active review cycle

Parameters:

  • review_uid: Parameter

Returns: None

_show_cancel_review_form(self, review_uid)

Purpose: Show form to cancel an active review cycle

Parameters:

  • review_uid: Parameter

Returns: None

_show_cancel_approval_form(self, approval_uid)

Purpose: Show form to cancel an active approval cycle

Parameters:

  • approval_uid: Parameter

Returns: None

_load_review_details_by_uid(self, review_uid)

Purpose: Helper method to refresh review details after an action

Parameters:

  • review_uid: Parameter

Returns: None

_convert_neo4j_datetimes(self, data)

Purpose: Recursively convert all Neo4j DateTime objects to Python datetime objects or strings. Args: data: Any data structure potentially containing Neo4j DateTime objects Returns: Same data structure with Neo4j DateTime objects converted to Python datetime

Parameters:

  • data: Parameter

Returns: See docstring for return details

_create_approvals_tab(self)

Purpose: Create the approvals tab content, hiding approvals for archived versions except current one

Returns: None

_get_status_color(self, status)

Purpose: Get color for a status value

Parameters:

  • status: Parameter

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

_approval_selected(self, event)

Purpose: Handle approval selection from table with support for both selection and cell click events

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_input)

Purpose: Format date string or object for display, handling multiple input formats. Args: date_input: Date input which could be: - String in various formats - datetime.datetime object - Neo4j DateTime object - None Returns: Formatted date string (YYYY-MM-DD) or None if input is invalid

Parameters:

  • date_input: Parameter

Returns: See docstring for return details

_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

_start_review_cycle(self, reviewer_table, due_date, review_type, sequential, required_approval_percentage, instructions)

Purpose: Start a new review cycle for the current document

Parameters:

  • reviewer_table: Parameter
  • due_date: Parameter
  • review_type: Parameter
  • sequential: Parameter
  • required_approval_percentage: Parameter
  • instructions: Parameter

Returns: None

_start_approval_cycle(self, approver_table, due_date, approval_type, sequential, required_approval_percentage, instructions)

Purpose: Start a new approval cycle for the current document

Parameters:

  • approver_table: Parameter
  • due_date: Parameter
  • approval_type: Parameter
  • sequential: Parameter
  • required_approval_percentage: Parameter
  • instructions: Parameter

Returns: None

_show_approval_form(self, event)

Purpose: Show form to start a approval cycle

Parameters:

  • event: Parameter

Returns: None

_create_approver_step(self, title, user_options)

Purpose: Helper to create a step card for approval workflow

Parameters:

  • title: Parameter
  • user_options: Parameter

Returns: None

_get_user_options(self)

Purpose: Get user options for approver selection

Returns: None

_show_publish_form(self, event)

Purpose: Show form to publish a document

Parameters:

  • event: Parameter

Returns: None

_publish_document(self, publish_comment, form_panel)

Purpose: Handle document publication process

Parameters:

  • publish_comment: Parameter
  • form_panel: Parameter

Returns: None

_show_archive_form(self, event)

Purpose: Show form to archive the document

Parameters:

  • event: Parameter

Returns: None

_archive_document(self, archive_reason, archive_comment, form_panel)

Purpose: Handle document archiving process

Parameters:

  • archive_reason: Parameter
  • archive_comment: Parameter
  • form_panel: Parameter

Returns: None

_show_clone_form(self, event)

Purpose: Show form to clone the document with custom number option

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.7% similar

    Document detail view component

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

    Document detail view component

    From: /tf/active/vicechatdev/document_detail_backup.py
  • function create_document_detail 57.4% 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 55.4% similar

    Dashboard for viewing and managing controlled documents.

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

    UI component for managing document training.

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