🔍 Code Extractor

class ReviewPanel

Maturity: 27

Review management interface component

File:
/tf/active/vicechatdev/CDocs/ui/review_panel.py
Lines:
62 - 2023
Complexity:
moderate

Purpose

Review management interface component

Source Code

class ReviewPanel(param.Parameterized):
    """Review management interface component"""
    
    review_uid = param.String(default='')
    document_uid = param.String(default='')
    current_tab = param.String(default='my_reviews')
    
    def __init__(self, template, session_manager=None, parent_app=None, embedded=False, **params):
        super().__init__(**params)
        self.template = template
        self.session_manager = session_manager or SessionManager()
        self.parent_app = parent_app  # Store reference to parent app
        self.user = self._get_current_user()
        self.notification_area = pn.pane.Markdown("")
        self.review_data = None
        self.document_data = None
        self.embedded = embedded  # Flag for embedded mode
        
        # Create container for main content
        self.main_content = pn.Column(sizing_mode='stretch_width')
        
        # Initialize stats area regardless of mode
        self.stats_area = pn.Column(sizing_mode='stretch_width')
        
        # Initialize other important containers
        self.review_list_area = pn.Column(sizing_mode='stretch_width')
        self.review_detail_area = pn.Column(sizing_mode='stretch_width')
        self.document_detail_area = pn.Column(sizing_mode='stretch_width')
        
        # Set up the user interface
        if not self.embedded:
            # Only set up template components in standalone mode
            self._setup_header()
            self._setup_sidebar()
            self.template.main.append(self.notification_area)
            self.template.main.append(self.main_content)
        
        # Always set up main content components
        self.main_content.append(self.stats_area)
        self.main_content.append(self.review_list_area)
        self.main_content.append(self.review_detail_area)
        self.main_content.append(self.document_detail_area)
        
        # Hide detail areas initially
        self.review_detail_area.visible = False
        self.document_detail_area.visible = False
        
        # Load initial data
        self._load_pending_reviews()
    
    def _get_current_user(self) -> Optional['DocUser']:
        """Get the current user from session or parent app"""
        # First check if parent app has current_user
        if hasattr(self, 'parent_app') and self.parent_app and hasattr(self.parent_app, 'current_user'):
            return self.parent_app.current_user
            
        # Otherwise try session manager
        if self.session_manager:
            user_id = self.session_manager.get_user_id()
            if user_id:
                # Import DocUser class
                from CDocs.models.user_extensions import DocUser
                # Return complete DocUser object, not just the ID
                return DocUser(uid=user_id)
                
        return None
    
    def set_user(self, user):
        """Set the current user - needed for compatibility with main app"""
        # Store the user object
        self.user = user
        
        # Reload review data with the new user
        try:
            # Ensure we have a stats_area
            if not hasattr(self, 'stats_area'):
                self.stats_area = pn.Column(sizing_mode='stretch_width')
                if hasattr(self, 'main_content'):
                    self.main_content.insert(0, self.stats_area)
                
            # Update statistics and reload data
            self._update_review_statistics()
            self._load_pending_reviews()
        except Exception as e:
            import logging
            logger = logging.getLogger('CDocs.ui.review_panel')
            logger.error(f"Error in set_user: {str(e)}")
    
    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._refresh_current_view)
        
        # Header with buttons
        header = Row(
            pn.pane.Markdown("# Review Management"),
            refresh_btn,
            back_btn,
            sizing_mode='stretch_width',
            align='end'
        )
        
        self.template.header.append(header)
    
    def _setup_sidebar(self):
        """Set up the sidebar with navigation options"""
        # Create navigation buttons
        my_reviews_btn = Button(
            name='My Pending Reviews',
            button_type='primary',
            width=200
        )
        my_reviews_btn.on_click(self._load_pending_reviews)
        
        completed_reviews_btn = Button(
            name='My Completed Reviews',
            button_type='default',
            width=200
        )
        completed_reviews_btn.on_click(self._load_completed_reviews)
        
        # Add management buttons if user has manage reviews permission
        if self.user and permissions.user_has_permission(self.user, "MANAGE_REVIEWS"):
            all_reviews_btn = Button(
                name='All Reviews',
                button_type='default',
                width=200
            )
            all_reviews_btn.on_click(self._load_all_reviews)
            
            # Add to navigation area
            navigation = Column(
                Markdown("## Navigation"),
                my_reviews_btn,
                completed_reviews_btn,
                all_reviews_btn,
                sizing_mode='fixed'
            )
        else:
            # Add to navigation area without all reviews button
            navigation = Column(
                Markdown("## Navigation"),
                my_reviews_btn,
                completed_reviews_btn,
                sizing_mode='fixed'
            )
        
        self.template.sidebar.append(navigation)
        
        # Add statistics area
        self.stats_area = Column(
            Markdown("## Review Statistics"),
            Markdown("*Loading statistics...*"),
            sizing_mode='fixed',
            styles={'background': '#f8f9fa'},
            css_classes=['p-3', 'border', 'rounded']
        )
        self.template.sidebar.append(self.stats_area)
        
        # Update statistics
        self._update_review_statistics()
    
    def _setup_main_area(self):
        """Set up the main area with reviews and details"""
        # Create notification area
        self.template.main.append(self.notification_area)
        
        # Create main content area
        self.main_content = Column(
            sizing_mode='stretch_width'
        )
        self.template.main.append(self.main_content)
    
    def _refresh_current_view(self, event=None):
        """Refresh the current view"""
        if self.current_tab == 'my_reviews':
            self._load_pending_reviews()
        elif self.current_tab == 'completed_reviews':
            self._load_completed_reviews()
        elif self.current_tab == 'all_reviews':
            self._load_all_reviews()
        elif self.current_tab == 'review_detail':
            self._load_review(self.review_uid)
    
    def _navigate_back(self, event=None):
        """Navigate back to dashboard"""
        # Check if we have a parent app reference
        if hasattr(self, 'parent_app') and self.parent_app is not None:
            # Use parent app's load_dashboard method
            try:
                self.parent_app.load_dashboard()
                return
            except Exception as e:
                logger.error(f"Error navigating back to dashboard via parent app: {e}")
        
        # Fallback to direct state manipulation
        try:
            import panel as pn
            if pn.state.curdoc:
                # Try to find an app object in the global variables
                import sys
                for name, obj in globals().items():
                    if isinstance(obj, object) and hasattr(obj, 'load_dashboard'):
                        obj.load_dashboard()
                        return
            # Last resort - use JavaScript
            pn.state.execute("window.location.href = '/'")
        except Exception as e:
            logger.error(f"Error navigating back to dashboard: {e}")
    
    def _update_review_statistics(self):
        """Update review statistics for the current user."""
        try:
            # First, ensure the stats_area exists
            if not hasattr(self, 'stats_area'):
                self.stats_area = pn.Column(sizing_mode='stretch_width')
                self.main_content.insert(0, self.stats_area)
            
            # Clear existing content
            self.stats_area.clear()
            
            if not self.user:
                # No user set, don't show statistics
                self.stats_area.append(pn.pane.Markdown("**No user logged in**"))
                return
                
            # Get statistics
            try:
                # Import the controller functions
                from CDocs.controllers.review_controller import get_user_pending_reviews
                
                # Get pending reviews
                pending_result = get_user_pending_reviews(
                    user=self.user,
                    include_completed=False
                )
                pending_reviews = pending_result.get('reviews', [])
                pending_count = len(pending_reviews)
                
                # Get completed reviews (last 90 days)
                date_from = (datetime.now() - timedelta(days=90)).isoformat()
                try:
                    completed_result = get_user_pending_reviews(
                        user=self.user,
                        include_completed=True,
                        date_from=date_from
                    )
                    
                    # Filter completed reviews
                    all_reviews = completed_result.get('reviews', [])
                    completed_reviews = [r for r in all_reviews 
                                       if r.get('reviewer', {}).get('status') == 'COMPLETED']
                    completed_count = len(completed_reviews)
                    
                    # Count by decision
                    approved_count = sum(1 for r in completed_reviews 
                                      if r.get('reviewer', {}).get('decision') in ['APPROVED', 'APPROVED_WITH_COMMENTS'])
                    rejected_count = sum(1 for r in completed_reviews 
                                      if r.get('reviewer', {}).get('decision') == 'REJECTED')
                except TypeError:
                    # Fall back to a simpler approach if include_completed param fails
                    completed_count = 0
                    approved_count = 0
                    rejected_count = 0
                    
                    # Try to get all reviews and filter manually
                    try:
                        all_result = get_user_pending_reviews(self.user)
                        all_reviews = all_result.get('reviews', [])
                        
                        # Filter and count manually
                        for review in all_reviews:
                            reviewer_data = review.get('reviewer', {})
                            status = reviewer_data.get('status')
                            decision = reviewer_data.get('decision')
                            
                            if status == 'COMPLETED':
                                completed_count += 1
                                if decision in ['APPROVED', 'APPROVED_WITH_COMMENTS']:
                                    approved_count += 1
                                elif decision == 'REJECTED':
                                    rejected_count += 1
                    except Exception as filter_error:
                        logger.error(f"Error filtering reviews: {filter_error}")
                
                # Calculate efficiency metrics
                if completed_count > 0:
                    approval_rate = (approved_count / completed_count) * 100
                else:
                    approval_rate = 0
                
                # Create statistics cards - similar to approval panel layout
                # Add main content statistics similar to the approval panel
                stats_row = pn.Row(
                    pn.Column(
                        pn.pane.Markdown("## My Review Statistics"),
                        pn.pane.Markdown(f"**Pending Reviews:** {pending_count}"),
                        pn.pane.Markdown(f"**Completed (Last 90 Days):** {completed_count}"),
                        pn.pane.Markdown(f"**Approval Rate:** {approval_rate:.1f}%"),
                        width=300,
                        styles={'background': '#f5f5f5', 'border': '1px solid #ddd', 'border-radius': '5px', 'padding': '10px'}
                    ),
                    pn.Column(
                        pn.pane.Markdown("## Decision Breakdown"),
                        pn.pane.Markdown(f"**Approved:** {approved_count}"),
                        pn.pane.Markdown(f"**Rejected:** {rejected_count}"),
                        width=300,
                        styles={'background': '#f5f5f5', 'border': '1px solid #ddd', 'border-radius': '5px', 'padding': '10px'}
                    ),
                    sizing_mode='stretch_width'
                )
                
                # Add to stats area in main content
                self.stats_area.append(stats_row)
                
                # Get more detailed system-wide statistics
                try:
                    from CDocs.controllers.review_controller import get_review_statistics
                    system_stats = get_review_statistics()
                    
                    if system_stats and system_stats.get('success'):
                        statistics = system_stats.get('statistics', {})
                        
                        # Create a system-wide statistics row
                        if permissions.user_has_permission(self.user, "MANAGE_REVIEWS"):
                            system_stats_row = pn.Row(
                                pn.Column(
                                    pn.pane.Markdown("## System Review Statistics"),
                                    pn.pane.Markdown(f"**Total Reviews:** {statistics.get('total_reviews', 0)}"),
                                    pn.pane.Markdown(f"**Pending Reviews:** {statistics.get('pending_reviews', 0)}"),
                                    pn.pane.Markdown(f"**Completed Reviews:** {statistics.get('completed_reviews', 0)}"),
                                    width=300,
                                    styles={'background': '#f5f5f5', 'border': '1px solid #ddd', 'border-radius': '5px', 'padding': '10px'}
                                ),
                                pn.Column(
                                    pn.pane.Markdown("## Review Efficiency"),
                                    pn.pane.Markdown(f"**System Approval Rate:** {statistics.get('review_approval_rate', 0):.1f}%"),
                                    pn.pane.Markdown(f"**Decision Approval Rate:** {statistics.get('decision_approval_rate', 0):.1f}%"),
                                    width=300,
                                    styles={'background': '#f5f5f5', 'border': '1px solid #ddd', 'border-radius': '5px', 'padding': '10px'}
                                ),
                                sizing_mode='stretch_width'
                            )
                            self.stats_area.append(system_stats_row)
                except Exception as sys_stats_error:
                    logger.error(f"Error loading system review statistics: {sys_stats_error}")
                
                # Also update sidebar statistics if we have a template
                # if hasattr(self, 'template') and hasattr(self.template, 'sidebar'):
                #     try:
                #         # Find existing stats area in sidebar
                #         sidebar_stats_area = None
                #         for component in self.template.sidebar:
                #             if isinstance(component, pn.Column) and component.objects and \
                #                isinstance(component.objects[0], pn.pane.Markdown) and \
                #                "Review Statistics" in component.objects[0].object:
                #                 sidebar_stats_area = component
                #                 break
                        
                #         # Create or update sidebar stats
                #         if sidebar_stats_area:
                #             sidebar_stats_area.clear()
                #             sidebar_stats_area.append(pn.pane.Markdown("## Review Statistics"))
                #             sidebar_stats_area.append(pn.pane.Markdown(f"**Pending:** {pending_count}"))
                #             sidebar_stats_area.append(pn.pane.Markdown(f"**Completed:** {completed_count}"))
                #             sidebar_stats_area.append(pn.pane.Markdown(f"**Approval Rate:** {approval_rate:.1f}%"))
                #     except Exception as sidebar_error:
                #         logger.error(f"Error updating sidebar stats: {sidebar_error}")
                
            except Exception as e:
                logger.error(f"Error fetching review statistics: {e}")
                # Show error message
                self.stats_area.append(pn.pane.Markdown(f"**Error loading statistics:** {str(e)}"))
                return
        
        except Exception as e:
            logger.error(f"Error updating review statistics: {e}")
            # Try to create stats area if it doesn't exist
            if not hasattr(self, 'stats_area'):
                self.stats_area = pn.Column(sizing_mode='stretch_width')
                if hasattr(self, 'main_content'):
                    self.main_content.insert(0, self.stats_area)
            
            # Add error message
            if hasattr(self, 'stats_area'):
                self.stats_area.clear()
                self.stats_area.append(pn.pane.Markdown(f"**Error updating statistics:** {str(e)}"))
    
    def _load_pending_reviews(self, event=None):
        """Load pending reviews for the current user"""
        try:
            self.current_tab = 'my_reviews'
            self.notification_area.object = "Loading your pending reviews..."
            
            # Clear areas and prepare main content
            self.review_list_area.clear()
            self.review_detail_area.clear()
            self.document_detail_area.clear()
            self.review_detail_area.visible = False
            self.document_detail_area.visible = False
            
            self.main_content.clear()
            self._update_review_statistics()
            
            # Add content areas back to main_content
            self.main_content.append(self.stats_area)
            self.main_content.append(self.review_list_area)
            self.main_content.append(self.review_detail_area)
            self.main_content.append(self.document_detail_area)
            
            if not self.user:
                self.review_list_area.append(Markdown("# My Pending Reviews"))
                self.review_list_area.append(Markdown("Please log in to view your pending reviews."))
                self.notification_area.object = ""
                return
            
            # Get pending reviews
            try:
                from CDocs.controllers.review_controller import get_user_assigned_reviews
                
                # Get only active reviews (PENDING and ACTIVE status)
                result = get_user_assigned_reviews(
                    user=self.user,
                    status_filter=["PENDING", "ACTIVE"],
                    include_completed=False
                )
                
                # Prepare data for display
                assignments = result.get('assignments', [])
                if not assignments:
                    self.review_list_area.append(Markdown("# My Pending Reviews"))
                    self.review_list_area.append(Markdown("You have no pending reviews."))
                    self.notification_area.object = ""
                    return
                    
                # Convert to DataFrame for table display
                reviews_data = []
                for assignment in assignments:
                    review_cycle = assignment.get('review_cycle', {})
                    document = assignment.get('document', {})
                    
                    reviews_data.append({
                        'review_uid': review_cycle.get('UID', ''),
                        'document_uid': document.get('uid', ''),
                        'doc_number': document.get('doc_number', ''),
                        'title': document.get('title', ''),
                        'status': review_cycle.get('status', ''),
                        'review_type': review_cycle.get('review_type', ''),
                        'initiated_date': review_cycle.get('startDate', review_cycle.get('start_date', '')),
                        'due_date': review_cycle.get('dueDate', review_cycle.get('due_date', '')),
                        'reviewer_status': assignment.get('assignment', {}).get('status', '')
                    })
                
                # Create DataFrame
                df = pd.DataFrame(reviews_data)
                
                # Format dates
                if 'initiated_date' in df.columns:
                    df['initiated_date'] = df['initiated_date'].apply(self._format_date)
                if 'due_date' in df.columns:
                    df['due_date'] = df['due_date'].apply(self._format_date)
                
                # Add action column
                df['action'] = 'Review'
                
                # Display columns setup
                display_columns = ['doc_number', 'title', 'status', 'review_type', 
                                  'initiated_date', 'due_date', 'action','review_uid']
                column_names = {
                    'doc_number': 'Document',
                    'title': 'Title',
                    'status': 'Status',
                    'review_type': 'Type',
                    'initiated_date': 'Started',
                    'due_date': 'Due Date',
                    'action': 'Action',
                    'review_uid': 'review_uid'
                }
                
                # Filter and rename columns
                exist_columns = [col for col in display_columns if col in df.columns]
                df = df[exist_columns]
                rename_dict = {col: column_names[col] for col in exist_columns if col in column_names}
                df = df.rename(columns=rename_dict)
                
                # Create table
                reviews_table = Tabulator(
                    df,
                    pagination='local',
                    page_size=10,
                    sizing_mode='stretch_width',
                    selectable=1,
                    height=400,
                    hidden_columns=['review_uid']
                )
                
                # Add click handler
                reviews_table.on_click(self._review_selected)
                
                # Add to review list area
                self.review_list_area.append(Markdown("# My Pending Reviews"))
                self.review_list_area.append(Markdown(f"You have {len(reviews_data)} pending review(s) to complete."))
                self.review_list_area.append(reviews_table)
                
                # Clear notification
                self.notification_area.object = ""
                
            except Exception as e:
                logger.error(f"Error fetching pending reviews: {e}")
                self.review_list_area.append(Markdown("# My Pending Reviews"))
                self.review_list_area.append(Markdown(f"Error fetching pending reviews: {str(e)}"))
                self.notification_area.object = ""
                
        except Exception as e:
            logger.error(f"Error loading pending reviews: {e}")
            self.notification_area.object = f"**Error:** {str(e)}"
            self.main_content.clear()
            self.main_content.append(self.stats_area)
            self.review_list_area.clear()
            self.review_list_area.append(Markdown("# My Pending Reviews"))
            self.review_list_area.append(Markdown("*Error loading pending reviews*"))
            self.main_content.append(self.review_list_area)
    
    def _load_completed_reviews(self, event=None):
        """Load completed reviews for the current user"""
        try:
            self.current_tab = 'completed_reviews'
            self.notification_area.object = "Loading your completed reviews..."
            
            # Clear areas and prepare main content
            self.review_list_area.clear()
            self.review_detail_area.clear()
            self.document_detail_area.clear()
            self.review_detail_area.visible = False
            self.document_detail_area.visible = False
            
            self.main_content.clear()
            self._update_review_statistics()
            
            # Add content areas back to main_content
            self.main_content.append(self.stats_area)
            self.main_content.append(self.review_list_area)
            self.main_content.append(self.review_detail_area)
            self.main_content.append(self.document_detail_area)
            
            if not self.user:
                self.review_list_area.append(Markdown("# My Completed Reviews"))
                self.review_list_area.append(Markdown("Please log in to view your completed reviews."))
                self.notification_area.object = ""
                return
            
            # Get completed reviews
            try:
                from CDocs.controllers.review_controller import get_user_assigned_reviews
                
                # Get only completed reviews
                result = get_user_assigned_reviews(
                    user=self.user,
                    status_filter=["COMPLETED"],
                    include_completed=True
                )
                
                # Prepare data for display
                assignments = result.get('assignments', [])
                if not assignments:
                    self.review_list_area.append(Markdown("# My Completed Reviews"))
                    self.review_list_area.append(Markdown("You have no completed reviews in the last 90 days."))
                    self.notification_area.object = ""
                    return
                    
                # Convert to DataFrame for table display
                reviews_data = []
                for assignment in assignments:
                    review_cycle = assignment.get('review_cycle', {})
                    document = assignment.get('document', {})
                    reviewer_assignment = assignment.get('assignment', {})
                    
                    reviews_data.append({
                        'review_uid': review_cycle.get('UID', ''),
                        'document_uid': document.get('uid', ''),
                        'doc_number': document.get('doc_number', ''),
                        'title': document.get('title', ''),
                        'status': review_cycle.get('status', ''),
                        'review_type': review_cycle.get('review_type', ''),
                        'completed_date': reviewer_assignment.get('decision_date', ''),
                        'decision': reviewer_assignment.get('decision', '')
                    })
                
                # Create DataFrame
                df = pd.DataFrame(reviews_data)
                
                # Format dates
                if 'completed_date' in df.columns:
                    df['completed_date'] = df['completed_date'].apply(self._format_date)
                
                # Add action column
                df['action'] = 'View'
                
                # Display columns setup
                display_columns = ['doc_number', 'title', 'status', 'review_type', 
                                  'completed_date', 'decision', 'action','review_uid']
                column_names = {
                    'doc_number': 'Document',
                    'title': 'Title',
                    'status': 'Status',
                    'review_type': 'Type',
                    'completed_date': 'Completed',
                    'decision': 'Decision',
                    'action': 'Action',
                    'review_uid': 'review_uid'
                }
                
                # Filter and rename columns
                exist_columns = [col for col in display_columns if col in df.columns]
                df = df[exist_columns]
                rename_dict = {col: column_names[col] for col in exist_columns if col in column_names}
                df = df.rename(columns=rename_dict)
                
                # Create table
                reviews_table = Tabulator(
                    df,
                    pagination='local',
                    page_size=10,
                    sizing_mode='stretch_width',
                    selectable=1,
                    height=400,
                    hidden_columns=['review_uid']
                )
                
                # Add click handler
                reviews_table.on_click(self._review_selected)
                
                # Add to review list area
                self.review_list_area.append(Markdown("# My Completed Reviews"))
                self.review_list_area.append(Markdown(f"You have completed {len(reviews_data)} review(s)."))
                self.review_list_area.append(reviews_table)
                
                # Clear notification
                self.notification_area.object = ""
                
            except Exception as e:
                logger.error(f"Error fetching completed reviews: {e}")
                self.review_list_area.append(Markdown("# My Completed Reviews"))
                self.review_list_area.append(Markdown(f"Error fetching completed reviews: {str(e)}"))
                self.notification_area.object = ""
                
        except Exception as e:
            logger.error(f"Error loading completed reviews: {e}")
            self.notification_area.object = f"**Error:** {str(e)}"
            self.main_content.clear()
            self.main_content.append(self.stats_area)
            self.review_list_area.clear()
            self.review_list_area.append(Markdown("# My Completed Reviews"))
            self.review_list_area.append(Markdown("*Error loading completed reviews*"))
            self.main_content.append(self.review_list_area)
    
    def _load_all_reviews(self, event=None):
        """Load all reviews (admin view)"""
        try:
            self.current_tab = 'all_reviews'
            self.notification_area.object = "Loading all reviews..."
            
            # Clear areas and prepare main content
            self.review_list_area.clear()
            self.review_detail_area.clear()
            self.document_detail_area.clear()
            self.review_detail_area.visible = False
            self.document_detail_area.visible = False
            
            # Make sure the main content only contains the necessary components in the right order
            self.main_content.clear()
            
            # Update statistics 
            self._update_review_statistics()
            
            # Add the stats area, list area, and detail areas back to main_content
            self.main_content.append(self.stats_area)
            self.main_content.append(self.review_list_area)
            self.main_content.append(self.review_detail_area)
            self.main_content.append(self.document_detail_area)
            
            if not self.user:
                self.review_list_area.append(pn.pane.Markdown("# All Reviews"))
                self.review_list_area.append(pn.pane.Markdown("Please log in to view reviews."))
                self.notification_area.object = ""
                return
            
            # Check permission
            if not permissions.user_has_permission(self.user, "MANAGE_REVIEWS"):
                self.review_list_area.append(pn.pane.Markdown("# All Reviews"))
                self.review_list_area.append(pn.pane.Markdown("You do not have permission to view all reviews."))
                self.notification_area.object = ""
                return
            
            # Create a main container that will hold all content with explicit structure
            all_reviews_container = pn.Column(sizing_mode='stretch_width')
            
            # Add title to the main container
            all_reviews_container.append(pn.pane.Markdown("# All Reviews"))
            
            # Create accordion for better layout control
            accordion = pn.Accordion(sizing_mode='stretch_width')
            
            # Create filters panel components
            status_filter = pn.widgets.MultiChoice(
                name='Status Filter',
                options=['PENDING', 'IN_PROGRESS', 'COMPLETED', 'REJECTED', 'CANCELED'],
                value=['PENDING', 'IN_PROGRESS'],
                width=300
            )

            # Add review type filter
            review_type_filter = pn.widgets.Select(
                name='Review Type',
                options=[''] + settings.REVIEW_TYPES,  # Add empty option + list from settings
                width=300
            )

            # Add document type filter
            doc_type_filter = pn.widgets.Select(
                name='Document Type', 
                options=[''] + list(settings.DOCUMENT_TYPES.values()),  # Add empty option + types from settings
                width=300
            )
            
            date_range = pn.widgets.DateRangeSlider(
                name='Date Range',
                start=datetime.now() - timedelta(days=90),
                end=datetime.now(),
                value=(datetime.now() - timedelta(days=30), datetime.now()),
                width=300
            )
            
            filter_btn = pn.widgets.Button(
                name='Apply Filters',
                button_type='primary',
                width=150
            )
            
            # Create filters panel with better structure and add to accordion
            filters_panel = pn.Column(
                pn.Row(
                    pn.Column(
                        status_filter,
                        review_type_filter,
                        width=350
                    ),
                    pn.Column(
                        doc_type_filter,
                        date_range,
                        width=350       
                    ),
                    pn.Column(
                        filter_btn,
                        pn.layout.VSpacer(),
                        width=150
                    ),
                    sizing_mode='stretch_width'
                ),
                margin=(10, 10, 10, 10),
                sizing_mode='stretch_width',
                height=200
            )
            
            # Add filters panel to accordion
            accordion.append(("Review Filters", filters_panel))
            
            # Create the table area with its own container and margin
            self._reviews_table_area = pn.Column(
                pn.pane.Markdown("*Loading reviews...*"),
                margin=(10, 10, 10, 10),
                sizing_mode='stretch_width'
            )
            
            # Add table area to accordion - always start expanded
            accordion.append(("Review List", self._reviews_table_area))
            
            # Always show the review list panel by opening that accordion tab
            accordion.active = [0,1]  # Open the second panel (Review List)
            
            # Add accordion to main container
            all_reviews_container.append(accordion)
            
            # Add the full container to the review list area
            self.review_list_area.append(all_reviews_container)
            
            # Set up filter button handler
            def filter_reviews(event):
                self._load_filtered_reviews(
                    status_filter=status_filter.value,
                    date_from=date_range.value[0],
                    date_to=date_range.value[1], 
                    review_type=review_type_filter.value if review_type_filter.value else None,
                    doc_type=doc_type_filter.value if doc_type_filter.value else None
                )
            
            filter_btn.on_click(filter_reviews)
            
            # Initial load of reviews with default filters
            self._load_filtered_reviews(
                status_filter=['PENDING', 'IN_PROGRESS'],
                date_from=datetime.now() - timedelta(days=30),
                date_to=datetime.now()
            )
            
            # Clear notification
            self.notification_area.object = ""
            
        except Exception as e:
            logger.error(f"Error loading all reviews: {e}")
            logger.error(traceback.format_exc())
            self.notification_area.object = f"**Error:** {str(e)}"
            self.main_content.clear()
            self.main_content.append(self.stats_area)
            self.review_list_area.clear()
            self.review_list_area.append(pn.pane.Markdown("# All Reviews"))
            self.review_list_area.append(pn.pane.Markdown(f"*Error loading all reviews: {str(e)}*"))
            self.main_content.append(self.review_list_area)

    def _load_filtered_reviews(self, status_filter=None, date_from=None, date_to=None, review_type=None, doc_type=None):
        """Load reviews based on filters"""
        try:
            # Ensure the table area exists and show loading indicator
            #if not hasattr(self, '_reviews_table_area') or self._reviews_table_area is None:
                # ... existing code for table area setup ...
                
            self._reviews_table_area.clear()
            self._reviews_table_area.append(pn.pane.Markdown("*Loading reviews...*"))
            
            # Format dates if needed
            from_date_str = None
            to_date_str = None
            
            # Neo4j date handling for version 5
            if isinstance(date_from, datetime):
                from_date_str = date_from.strftime("%Y-%m-%dT%H:%M:%S")
                
            if isinstance(date_to, datetime):
                to_date_str = date_to.strftime("%Y-%m-%dT%H:%M:%S")
            
            # Call Neo4j directly to get all review cycles efficiently
            from CDocs import db
            
            try:
                # Build WHERE conditions
                where_conditions = []
                params = {}
                
                # Status filter
                if status_filter:
                    where_conditions.append("r.status IN $statuses")
                    params["statuses"] = status_filter
                
                # # Date range filters
                # if from_date_str:
                #     where_conditions.append("r.startDate >= datetime($from_date)")
                #     params["from_date"] = from_date_str
                    
                # if to_date_str:
                #     where_conditions.append("r.startDate <= datetime($to_date)")
                #     params["to_date"] = to_date_str
                
                # Review type filter
                if review_type:
                    where_conditions.append("r.review_type = $review_type")
                    params["review_type"] = review_type
                
                # Document type filter
                if doc_type:
                    where_conditions.append("d.docType = $doc_type")
                    params["doc_type"] = doc_type
                
                # Build the WHERE clause
                status_condition = ""
                if where_conditions:
                    status_condition = "WHERE " + " AND ".join(where_conditions)
                
                logger.info("WHERE condition: %s", status_condition)
                logger.info("Params: %s", params)
                
                # Query for review cycles with document info
                query = f"""
                MATCH (r:ReviewCycle)-[:FOR_REVIEW]->(v:DocumentVersion)<-[:HAS_VERSION]-(d:ControlledDocument)
                {status_condition}
                // Count reviewers using ReviewerAssignment nodes
                OPTIONAL MATCH (r)-[:ASSIGNMENT]->(a:ReviewerAssignment)
                WITH r, d, v, COUNT(a) as reviewer_count_assign
                // Count reviewers using REVIEWED_BY relationship (for backward compatibility)
                OPTIONAL MATCH (r)-[:REVIEWED_BY]->(u:User)
                WITH r, d, v, reviewer_count_assign, COUNT(u) as reviewer_count_rel
                // Use the higher count (for compatibility with both data models)
                RETURN 
                    r.UID as review_uid,
                    r.status as status,
                    r.review_type as review_type,
                    r.startDate as start_date,
                    r.dueDate as due_date,
                    r.completionDate as completion_date,
                    r.initiated_by_name as initiated_by,
                    d.UID as document_uid,
                    d.docNumber as doc_number,
                    d.title as title,
                    d.docType as document_type,
                    d.status as doc_status,
                    v.version_number as version,
                    CASE WHEN reviewer_count_rel > reviewer_count_assign 
                         THEN reviewer_count_rel 
                         ELSE reviewer_count_assign 
                    END as reviewer_count
                ORDER BY r.startDate DESC
                LIMIT 100
                """
                
                # Execute query
                result = db.run_query(query, params)
                
                # Rest of your existing code for processing results...
                
                # Log result count for debugging
                logger.debug(f"Query returned {len(result) if result else 0} results")
                
                # Clear the loading indicator
                self._reviews_table_area.clear()
                
                # Check if we have results
                if not result:
                    self._reviews_table_area.append(pn.pane.Markdown("*No reviews found matching the filter criteria*"))
                    return
                    
                # Prepare data for table
                reviews_data = []
                for record in result:
                    # Convert neo4j DateTime to Python datetime
                    start_date = self._convert_neo4j_datetime(record.get('start_date'))
                    due_date = self._convert_neo4j_datetime(record.get('due_date'))
                    completion_date = self._convert_neo4j_datetime(record.get('completion_date'))
                    
                    # Add to data collection
                    reviews_data.append({
                        'review_uid': record.get('review_uid'),
                        'document_uid': record.get('document_uid'),
                        'doc_number': record.get('doc_number'),
                        'title': record.get('title'),
                        'version': record.get('version'),
                        'status': record.get('status'),
                        'review_type': record.get('review_type'),
                        'initiated_by': record.get('initiated_by'),
                        'reviewer_count': record.get('reviewer_count'),
                        'start_date': start_date,
                        'due_date': due_date,
                        'completion_date': completion_date,
                        'doc_status': record.get('doc_status'),
                        # Add a formatted status with HTML for color-coding
                        'status_formatted': self._format_status_html(record.get('status'))
                    })
                
                # Show summary first
                self._reviews_table_area.append(pn.pane.Markdown(f"### Found {len(reviews_data)} reviews"))
                
                # Create DataFrame
                df = pd.DataFrame(reviews_data)
                
                # Log DataFrame columns for debugging
                logger.debug(f"DataFrame columns: {df.columns.tolist() if not df.empty else 'Empty DataFrame'}")
                
                # Format dates
                for date_col in ['start_date', 'due_date', 'completion_date']:
                    if date_col in df.columns:
                        df[date_col] = df[date_col].apply(self._format_date)
                
                # Add action column with HTML button
                df['action'] = '<button class="btn btn-sm btn-primary">View</button>'
                
                # Display columns setup - use status_formatted instead of status
                display_columns = ['doc_number', 'title', 'version', 'status_formatted', 'review_type', 
                                'initiated_by', 'reviewer_count', 'start_date', 'due_date', 'action', 'review_uid', 'status']
                column_names = {
                    'doc_number': 'Document',
                    'title': 'Title',
                    'version': 'Version',
                    'status_formatted': 'Status',  # This will display the HTML-formatted status
                    'review_type': 'Type',
                    'initiated_by': 'Initiated By',
                    'reviewer_count': 'Reviewers',
                    'start_date': 'Started',
                    'due_date': 'Due Date',
                    'action': 'Action',
                    'review_uid': 'review_uid',
                    'status': 'raw_status'  # Keep original status hidden for filtering
                }
                
                # Filter and rename columns
                exist_columns = [col for col in display_columns if col in df.columns]
                df = df[exist_columns]
                rename_dict = {col: column_names[col] for col in exist_columns if col in column_names}
                df = df.rename(columns=rename_dict)
                
                # Define formatters for Panel 1.6.1's Tabulator
                formatters = {
                    'Status': {'type': 'html'},  # Format as HTML
                    'Action': {'type': 'html'}   # Format as HTML
                }
                
                # Create table with formatters
                reviews_table = pn.widgets.Tabulator(
                    df,
                    formatters=formatters,
                    pagination='local',
                    page_size=10,
                    sizing_mode='stretch_width',
                    selectable=1,
                    height=400,
                    hidden_columns=['review_uid', 'raw_status']
                )
                
                # Add click handler with debugger info
                def table_click_handler(event):
                    logger.debug(f"Table click event: {event}")
                    self._review_selected(event)
                    
                reviews_table.on_click(table_click_handler)
                
                # Add table to the area 
                self._reviews_table_area.append(reviews_table)
                
            except Exception as db_error:
                logger.error(f"Database error in _load_filtered_reviews: {db_error}")
                logger.error(traceback.format_exc())
                self._reviews_table_area.clear()
                self._reviews_table_area.append(pn.pane.Markdown(f"**Error loading reviews from database:** {str(db_error)}"))
                
        except Exception as e:
            logger.error(f"Error in _load_filtered_reviews: {e}")
            logger.error(traceback.format_exc())
            if hasattr(self, '_reviews_table_area'):
                self._reviews_table_area.clear()
                self._reviews_table_area.append(pn.pane.Markdown(f"**Error loading reviews:** {str(e)}"))
    
    def _format_status_html(self, status):
        """Format review status as HTML with color-coding"""
        if not status:
            return '<span>Unknown</span>'
            
        # Define color mapping
        status_colors = {
            'PENDING': {'bg': '#e9ecef', 'text': '#212529'},
            'IN_PROGRESS': {'bg': '#fff3cd', 'text': '#664d03'},
            'COMPLETED': {'bg': '#d1e7dd', 'text': '#0f5132'},
            'REJECTED': {'bg': '#f8d7da', 'text': '#842029'},
            'CANCELED': {'bg': '#f5f5f5', 'text': '#6c757d'}
        }
        
        # Get color for status or use default gray
        color_info = status_colors.get(status, {'bg': '#f8f9fa', 'text': '#212529'})
        
        # Return HTML with inline styling for the status badge
        return f'''<span style="display: inline-block; padding: 0.25rem 0.5rem; 
                    font-weight: bold; background-color: {color_info['bg']}; 
                    color: {color_info['text']}; border-radius: 0.25rem; 
                    text-align: center; min-width: 80px;">{status}</span>'''

    # Add this helper method to convert Neo4j DateTime objects
    def _convert_neo4j_datetime(self, dt_value):
        """Convert Neo4j DateTime to Python datetime"""
        if not dt_value:
            return None
            
        # Check if it's a string
        if isinstance(dt_value, str):
            try:
                return datetime.fromisoformat(dt_value)
            except ValueError:
                return None
        
        # Handle Neo4j DateTime objects
        try:
            if hasattr(dt_value, '__class__') and dt_value.__class__.__name__ == 'DateTime':
                # Convert Neo4j DateTime to Python datetime
                return datetime(
                    year=dt_value.year,
                    month=dt_value.month,
                    day=dt_value.day,
                    hour=dt_value.hour,
                    minute=dt_value.minute,
                    second=dt_value.second,
                    microsecond=dt_value.nanosecond // 1000
                )
        except Exception:
            pass
            
        # Return as-is if we can't convert
        return dt_value
    
    def _review_selected(self, event):
        """Handle review selection from table"""
        try:
            # Debug the event
            logger.debug(f"Review selection event type: {type(event).__name__}")
            
            # Handle different event types
            row_index = None
            review_uid = 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}")
                
                # Get data from the table's source
                if hasattr(event, 'model') and hasattr(event.model, 'source'):
                    source_data = event.model.source.data
                    logger.debug(f"Source data keys: {list(source_data.keys())}")
                    
                    # Try to find review_uid 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
                    
                    # If still no UID found, try to extract from data columns
                    if not review_uid:
                        # Find first column that might contain '_uid' text
                        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
                                
                # Last resort - try getting the cell's value directly
                if not review_uid and hasattr(event, 'value'):
                    review_uid = event.value
                    logger.debug(f"Using cell value as review_uid: {review_uid}")
                    
            # Standard selection event
            elif hasattr(event, 'new') and event.new is not None:
                selected_idx = event.new[0] if isinstance(event.new, list) else event.new
                logger.debug(f"Selection event with index {selected_idx}")
                
                # Get DataFrame
                if hasattr(event, 'obj') and hasattr(event.obj, 'value'):
                    df = event.obj.value
                    logger.debug(f"DataFrame columns: {list(df.columns)}")
                    
                    # 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[selected_idx][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[selected_idx][col]
                                logger.debug(f"Found review_uid in column '{col}': {review_uid}")
                                break
            
            # Exit if we couldn't determine the review UID
            if not review_uid:
                logger.warning("Could not determine review UID from selection event")
                logger.debug(f"Full event object: {str(event)}")
                self.notification_area.object = "**Error:** Could not determine review ID"
                return
            
            # Log found UID
            logger.info(f"Loading review with UID: {review_uid}")
            
            # Load the selected review
            self._load_review(review_uid)
                
        except Exception as e:
            logger.error(f"Error selecting review: {e}")
            logger.error(traceback.format_exc())
            self.notification_area.object = f"**Error:** {str(e)}"
    
    def _load_review(self, review_uid):
        """Load and display a specific review"""
        try:
            self.current_tab = 'review_detail'
            self.review_uid = review_uid
            self.notification_area.object = "Loading review details..."
            
            # Clear areas and prepare main content
            self.review_list_area.clear()
            self.review_detail_area.clear()
            self.document_detail_area.clear()
            self.review_detail_area.visible = True
            
            self.main_content.clear()
            self._update_review_statistics()
            
            # Add content areas back to main_content
            self.main_content.append(self.stats_area)
            self.main_content.append(self.review_detail_area)
            
            if not self.user:
                self.review_detail_area.append(Markdown("# Review Details"))
                self.review_detail_area.append(Markdown("Please log in to view review details."))
                self.notification_area.object = ""
                return
            
            # Get review data from review_controller get_review_cycle function
            try:
                from CDocs.controllers.review_controller import get_review_cycle
                #logger.info("review_uid: %s", review_uid)
                
                review_result = get_review_cycle(
                    review_uid=review_uid,
                    include_comments=True,
                    include_document=True
                )
                
                if not review_result:
                    self.review_detail_area.append(Markdown("# Review Details"))
                    self.review_detail_area.append(Markdown("Review not found or you do not have permission to view it."))
                    self.notification_area.object = ""
                    return
                
                # Store data for later use
                self.review_data = review_result
                document_data = review_result.get('document', {})
                self.document_data = document_data
                self.document_uid = document_data.get('uid', document_data.get('UID', ''))
                
                # Create the review detail view
                self._create_review_detail_view()
                
                # Clear notification
                self.notification_area.object = ""
                
            except ResourceNotFoundError:
                self.notification_area.object = "**Error:** Review not found"
                self.review_detail_area.append(Markdown("# Review Details"))
                self.review_detail_area.append(Markdown("Review not found."))
            except Exception as e:
                logger.error(f"Error loading review: {e}")
                self.notification_area.object = f"**Error:** {str(e)}"
                self.review_detail_area.append(Markdown("# Review Details"))
                self.review_detail_area.append(Markdown(f"Error loading review: {str(e)}"))
        except Exception as e:
            logger.error(f"Error in _load_review: {e}")
            self.notification_area.object = f"**Error:** {str(e)}"
    
    def _create_review_detail_view(self):
        """Create the review detail view"""
        if not self.review_data:
            return
            
        # Extract data
        review = self.review_data
        document = self.document_data
        
        # Get review metadata with proper fallbacks for different field names
        status = review.get('status', '')
        review_type = review.get('review_type', '')
        initiated_date = self._format_date(review.get('startDate', review.get('initiated_date', review.get('start_date', ''))))
        due_date = self._format_date(review.get('dueDate', review.get('due_date', '')))
        initiated_by = review.get('initiated_by_name', '')
        instructions = review.get('instructions', '')
        
        # Check if review is sequential
        is_sequential = review.get('sequential', False)
        
        # Get reviewer data - handle different data structures
        reviewer_assignments = review.get('reviewer_assignments', [])
        
        # Check for empty or missing assignments and look for reviewers in alternate locations
        if not reviewer_assignments and 'reviewers' in review:
            # Try to convert reviewers list to expected assignment format
            reviewers = review.get('reviewers', [])
            reviewer_assignments = []
            for reviewer in reviewers:
                assignment = {
                    'reviewer_uid': reviewer.get('UID', ''),
                    'reviewer_name': reviewer.get('name', ''),
                    'role': reviewer.get('role', ''),
                    'status': reviewer.get('status', 'PENDING')
                }
                reviewer_assignments.append(assignment)
                
        # Find the current user's assignment
        my_assignment = None
        if self.user:
            my_assignment = next((r for r in reviewer_assignments 
                            if r.get('reviewer_uid') == self.user.uid), None)
        
        # Get comment data with fallback options
        comments = review.get('comments', [])
    
        # Create document header with fallbacks for different field naming
        doc_number = document.get('doc_number', document.get('docNumber', ''))
        doc_title = document.get('title', '')
        doc_revision = document.get('revision', document.get('version', ''))
        
        # Create review detail view
        review_detail = pn.Column(
            sizing_mode='stretch_width'
        )
        
        # Add header with document info
        review_detail.append(pn.pane.Markdown(f"# Review for {doc_number} Rev {doc_revision}"))
        review_detail.append(pn.pane.Markdown(f"## {doc_title}"))
        
        # Create summary card
        summary_card = pn.Column(
            pn.pane.Markdown("### Review Summary"),
            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}"),
            width=350,
            styles={'background':'#f8f9fa'},
            css_classes=['p-3', 'border', 'rounded']
        )
        
        # Add sequential review indicator if this is a sequential review
        if is_sequential:
            summary_card.append(pn.pane.Markdown("**Sequential Review:** Yes"))
        
        # Create document info card
        doc_info_card = pn.Column(
            pn.pane.Markdown("### Document Information"),
            pn.pane.Markdown(f"**Number:** {doc_number}"),
            pn.pane.Markdown(f"**Revision:** {doc_revision}"),
            pn.pane.Markdown(f"**Type:** {document.get('doc_type', document.get('docType', ''))}"),
            pn.pane.Markdown(f"**Department:** {document.get('department', '')}"),
            width=350,
            styles={'background':'#f8f9fa'},
            css_classes=['p-3', 'border', 'rounded']
        )
        
        # Get document UID for access controls
        doc_uid = document.get('uid', document.get('UID', ''))
        
        # Create document action buttons row
        button_row = pn.Row(sizing_mode='stretch_width')
        
        # IMPORTANT: Instead of embedding the DocumentAccessControls directly,
        # create a placeholder first, then dynamically add the component
        # after the entire review detail structure is created
        access_controls_placeholder = pn.Column(sizing_mode='stretch_width')
        doc_info_card.append(access_controls_placeholder)
        
        # Add cards to layout
        review_detail.append(pn.Row(
            summary_card,
            pn.Column(width=20),  # spacing
            doc_info_card,
            sizing_mode='stretch_width'
        ))
        
        # Add instructions if available
        if instructions:
            review_detail.append(pn.pane.Markdown("### Review Instructions"))
            review_detail.append(pn.pane.Markdown(instructions))
        
        # If this user has an assignment with specific instructions, show them prominently
        if my_assignment and self.user and my_assignment.get('reviewer_uid') == self.user.uid:
            specific_instructions = my_assignment.get('instructions')
            if specific_instructions:
                # Create a special instructions card
                your_instructions = pn.Column(
                    pn.pane.Markdown("### Your Specific Instructions"),
                    pn.pane.Markdown(specific_instructions),
                    styles={'background':'#fff3cd'},  # Light yellow background for emphasis
                    css_classes=['p-3', 'border', 'rounded', 'mb-3'],
                    sizing_mode='stretch_width'
                )
                review_detail.append(your_instructions)
        
        # Create reviewers table with error handling for data structure
        try:
            # Check if user has permission to see all reviewer instructions
            can_see_all_instructions = permissions.user_has_permission(self.user, "MANAGE_REVIEWS")
            
            reviewers_df = self._create_reviewers_dataframe(reviewer_assignments, 
                                                          include_instructions=can_see_all_instructions)
            
            # Create reviewers table
            reviewers_table = pn.widgets.Tabulator(
                reviewers_df,
                sizing_mode='stretch_width',
                height=200
            )
            
            # Add reviewers section
            review_detail.append(pn.pane.Markdown("## Reviewers"))
            review_detail.append(reviewers_table)
        except Exception as e:
            logger.error(f"Error creating reviewers table: {e}")
            review_detail.append(pn.pane.Markdown("## Reviewers"))
            review_detail.append(pn.pane.Markdown("*Error loading reviewer data*"))
        
        # Add comments section
        review_detail.append(pn.pane.Markdown("## Comments"))
        
        # Create comments area with error handling
        try:
            comments_area = self._create_comments_area(comments)
            review_detail.append(comments_area)
        except Exception as e:
            logger.error(f"Error creating comments area: {e}")
            review_detail.append(pn.pane.Markdown("*Error loading comments*"))
    
        # Add close review button if review is completed and user has permission
        if status == 'COMPLETED' and document.get('status','NA') == 'IN_REVIEW' and self.user:
            # Check if user is document owner, review initiator, or has manage permission
            is_document_owner = document.get('owner_uid') == self.user.uid
            is_review_initiator = review.get('initiated_by_uid') == self.user.uid
            has_manage_permission = permissions.user_has_permission(self.user, "MANAGE_REVIEWS")
            
            if is_document_owner or is_review_initiator or has_manage_permission:
                # 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
                close_btn.on_click(lambda event: self._close_review_cycle(
                    review_uid=self.review_uid,
                    update_status=update_status_checkbox.value,
                    target_status=status_select.value
                ))
                
                # Add section to review detail
                review_detail.append(close_review_section)
        
        # Check if user can review based on sequential or non-sequential flow
        can_review = True
        if my_assignment:
            # Always allow if status is active
            if my_assignment.get('status') == 'ACTIVE':
                can_review = True
            # For pending status, check sequential rules
            elif my_assignment.get('status') == 'PENDING':
                # In sequential mode, need to check if this reviewer is next
                if is_sequential:
                    # User can only review if they're active (their turn in sequence)
                    can_review = False
                    
                    # Find active reviewer
                    active_reviewer = next((r for r in reviewer_assignments if r.get('status') == 'ACTIVE'), None)
                    
                    # If no active reviewer and this user is first in sequence
                    if not active_reviewer and my_assignment.get('sequence_order', 999) == 1:
                        can_review = True
                else:
                    # Non-sequential review - all pending reviewers can review
                    can_review = True
        
        # Add review actions if user is a pending or active reviewer
        if my_assignment and my_assignment.get('status') in ['PENDING', 'ACTIVE']:
            if can_review:
                # User can review now, show review form
                review_actions = self._create_review_actions(my_assignment)
                review_detail.append(pn.pane.Markdown("## Your Review"))
                review_detail.append(review_actions)
            elif is_sequential:
                # Sequential review - show waiting message
                waiting_message = pn.Column(
                    pn.pane.Markdown("## Your Review"),
                    pn.pane.Markdown("This is a **sequential review**. You will be notified when it's your turn to review."),
                    pn.pane.Markdown("Reviewers must complete their reviews in the specified order."),
                    styles={'background':'#f8f9fa'},
                    css_classes=['p-3', 'border', 'rounded'],
                    sizing_mode='stretch_width'
                )
                review_detail.append(waiting_message)
        
        # Add to review detail area
        self.review_detail_area.clear()
        self.review_detail_area.append(review_detail)
        
        # NOW THAT THE REVIEW DETAIL IS FULLY CONSTRUCTED AND ATTACHED TO THE DOM,
        # ADD THE DOCUMENT ACCESS CONTROLS TO THE PLACEHOLDER
        try:
            from CDocs.ui.components.document_access_controls import DocumentAccessControls
            
            if doc_uid and self.user and hasattr(self.user, 'uid'):
                # Create the controls object
                access_controls = DocumentAccessControls(
                    document_uid=doc_uid,
                    user_uid=self.user.uid,
                    show_access_indicator=True
                )
                
                # Get the view component without directly embedding the class
                access_view = access_controls.view()
                
                # Add it to the placeholder we created earlier
                access_controls_placeholder.clear()
                access_controls_placeholder.append(access_view)
            else:
                # Fallback to simple buttons if we're missing required information
                logger.warning(f"Could not create document access controls: doc_uid={doc_uid}, user={self.user}")
                
                # Create View button
                view_btn = pn.widgets.Button(name="View Document", button_type="primary", width=120)
                
                # Create click handler that navigates to document
                def view_document(event):
                    if doc_uid:
                        pn.state.execute(f"window.location.href = '/document/view/{doc_uid}'")
                        
                view_btn.on_click(view_document)
                
                # Check if user can edit
                can_edit = False
                try:
                    can_edit = permissions.user_can_edit_document(
                        user_uid=self.user.uid if self.user else None,
                        doc_uid=doc_uid,
                        doc_status=document.get('status', '')
                    )
                except Exception:
                    pass
                    
                # Create button row
                button_row = pn.Row(view_btn)
                
                # Add Edit button if user can edit
                if can_edit:
                    edit_btn = pn.widgets.Button(name="Edit Document", button_type="success", width=120)
                    
                    def edit_document(event):
                        if doc_uid:
                            pn.state.execute(f"window.location.href = '/document/edit/{doc_uid}'")
                            
                    edit_btn.on_click(edit_document)
                    button_row.append(edit_btn)
                    
                # Add buttons to placeholder
                access_controls_placeholder.clear()
                access_controls_placeholder.append(button_row)
        except Exception as access_error:
            # Fallback to simple button if there's an error
            logger.error(f"Error creating document access controls: {access_error}")
            logger.error(traceback.format_exc())
            
            view_btn = pn.widgets.Button(name="View Document", button_type="primary", width=150)
            
            def view_document(event):
                if doc_uid:
                    pn.state.execute(f"window.location.href = '/document/view/{doc_uid}'")
                    
            view_btn.on_click(view_document)
            
            access_controls_placeholder.clear()
            access_controls_placeholder.append(view_btn)

    def _close_review_cycle(self, review_uid, update_status=True, target_status="DRAFT"):
        """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,
                target_status=target_status
            )
            
            if result['success']:
                self.notification_area.object = "**Success:** Review cycle closed successfully"
                
                # If status was updated, show additional message
                if update_status:
                    self.notification_area.object += f"<br>Document status updated to {target_status}"
                    
                # Reload the review details after a short delay
                time.sleep(2)
                self._load_review(review_uid)
            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 _create_reviewers_dataframe(self, reviewer_assignments, include_instructions=False):
        """Create a DataFrame for the reviewers table"""
        # Create data for table
        reviewers_data = []
        
        for assignment in reviewer_assignments:
            # Format dates
            assigned_date = self._format_date(assignment.get('assigned_date'))
            decision_date = self._format_date(assignment.get('decision_date'))
            
            # Add to data
            reviewer_data = {
                'reviewer_name': assignment.get('reviewer_name', ''),
                'role': assignment.get('role', ''),
                'status': assignment.get('status', ''),
                'decision': assignment.get('decision', ''),
                'assigned_date': assigned_date,
                'decision_date': decision_date,
                'sequence_order': assignment.get('sequence_order', '')
            }
            
            # Add instructions if requested and available
            if include_instructions and 'instructions' in assignment:
                reviewer_data['instructions'] = assignment.get('instructions', '')
            
            reviewers_data.append(reviewer_data)
        
        # Create DataFrame
        df = pd.DataFrame(reviewers_data)
        
        # Select and rename columns for display
        display_columns = ['reviewer_name', 'role', 'status', 'decision', 
                     'assigned_date', 'decision_date']
        
        # Add sequence_order column if any assignments have it
        if any('sequence_order' in r and r['sequence_order'] for r in reviewers_data):
            display_columns.insert(0, 'sequence_order')
        
        # Add instructions column if included
        if include_instructions and 'instructions' in df.columns:
            display_columns.append('instructions')
        
        # Column names mapping
        column_names = {
            'reviewer_name': 'Reviewer',
            'role': 'Role',
            'status': 'Status',
            'decision': 'Decision',
            'assigned_date': 'Assigned',
            'decision_date': 'Completed',
            'sequence_order': 'Order',
            'instructions': 'Instructions'
        }
        
        # Filter and rename columns
        exist_columns = [col for col in display_columns if col in df.columns]
        df = df[exist_columns]
        rename_dict = {col: column_names[col] for col in exist_columns if col in column_names}
        df = df.rename(columns=rename_dict)
        
        return df
    
    def _create_comments_area(self, comments):
        """Create the comments area"""
        # Sort comments by timestamp
        sorted_comments = sorted(
            comments,
            key=lambda x: x.get('timestamp', ''),
            reverse=True  # Most recent first
        )
        
        if not sorted_comments:
            return pn.pane.Markdown("*No comments have been submitted yet.*")
        
        # Create comments HTML
        comments_html = "<div class='p-3'>"
        
        for comment in sorted_comments:
            # Format timestamp
            timestamp = comment.get('timestamp', '')
            timestamp_str = ""
            
            if timestamp:
                try:
                    # Handle different timestamp formats
                    if isinstance(timestamp, str):
                        # Try to parse ISO format string
                        dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
                        timestamp_str = dt.strftime('%Y-%m-%d %H:%M')
                    elif isinstance(timestamp, datetime):
                        # If already a datetime object
                        timestamp_str = timestamp.strftime('%Y-%m-%d %H:%M')
                    elif hasattr(timestamp, '__class__') and timestamp.__class__.__name__ == 'DateTime':
                        # Handle Neo4j DateTime objects
                        timestamp_str = f"{timestamp.year}-{timestamp.month:02d}-{timestamp.day:02d} {timestamp.hour:02d}:{timestamp.minute:02d}"
                    else:
                        # Fallback for unknown format
                        timestamp_str = str(timestamp)
                except Exception as e:
                    logger.debug(f"Error formatting timestamp: {e}")
                    timestamp_str = str(timestamp)
            
            # Get user info
            user_name = comment.get('user_name', 'Unknown')
            
            # Get comment text and section
            text = comment.get('text', '')
            section = comment.get('section', '')
            
            # Format as a comment card
            comments_html += f"""
            <div class='p-2 mb-3 border rounded'>
                <p><strong>{user_name}</strong> <span class='text-muted'>on {timestamp_str}</span></p>
                {f"<p><strong>Section:</strong> {section}</p>" if section else ""}
                <p>{text}</p>
            </div>
            """
        
        comments_html += "</div>"
        
        return pn.pane.HTML(comments_html)
    
    def _create_review_actions(self, my_assignment):
        """Create the review actions form"""
        # Check for specific instructions
        specific_instructions = my_assignment.get('instructions', '')
        
        components = []
        
        # Add specific instructions if they exist
        if specific_instructions:
            instructions_alert = pn.pane.Alert(
                f"**Your Instructions:** {specific_instructions}",
                alert_type="warning"  # Use Bootstrap warning style
            )
            components.append(instructions_alert)
        
        # Create comment input
        comment_input = TextAreaInput(
            name="Comments",
            placeholder="Enter your review comments...",
            rows=5,
            width=600
        )
        
        # Create section input
        section_input = TextInput(
            name="Section",
            placeholder="Optional: specify document section",
            width=300
        )
        
        # Create decision buttons
        decision_group = RadioButtonGroup(
            name='Decision',
            options={
                'APPROVED': 'APPROVED',
                'REJECTED': 'REJECTED', 
                'APPROVED_WITH_COMMENTS': 'APPROVED_WITH_COMMENTS'
            },
            button_type='success'
        )
        
        # Create submit button
        submit_btn = Button(
            name="Submit Review",
            button_type="primary",
            width=150
        )
        
        # Set up event handler
        submit_btn.on_click(lambda event: self._submit_review(
            decision_group.value,
            comment_input.value,
            section_input.value
        ))
        
        # Create form layout adding all components
        components.extend([
            Row(
                Column(
                    Markdown("### Your Review Decision"),
                    decision_group,
                    width=400
                ),
                Column(
                    Markdown("### Add Comment"),
                    section_input,
                    comment_input,
                    width=600
                ),
                align='start'
            ),
            Row(
                submit_btn,
                align='end'
            )
        ])
        
        review_actions = Column(
            *components,
            styles={'background':'#f8f9fa'},
            css_classes=['p-3', 'border', 'rounded'],
            sizing_mode='stretch_width'
        )
        
        return review_actions
    
    def _submit_review(self, decision, comment, section):
        """Submit a review decision and comment"""
        try:
            if not self.review_uid:
                self.notification_area.object = "**Error:** No review selected"
                return
                
            if not decision:
                self.notification_area.object = "**Error:** Please select a decision"
                return
                
            # First add the comment if provided
            if comment:
                try:
                    add_result = add_review_comment(
                        user=self.user,
                        review_uid=self.review_uid,
                        comment_text=comment,
                        comment_type="GENERAL",
                        page_number=None,
                        location_info={"section": section} if section else None
                    )
                    
                    if not add_result or not add_result.get('success'):
                        error_msg = add_result.get('message', 'Failed to add comment')
                        self.notification_area.object = f"**Error:** {error_msg}"
                        return
                except Exception as comment_error:
                    logger.error(f"Error adding review comment: {comment_error}")
                    self.notification_area.object = f"**Warning:** Could not add comment, but proceeding with review submission: {str(comment_error)}"
            
            # Then complete the review
            complete_result = complete_review(
                user=self.user,
                review_uid=self.review_uid,
                decision=decision,
                comments=comment if comment else ""
            )
            
            if complete_result and complete_result.get('success'):
                self.notification_area.object = "Review submitted successfully"
                
                # Reload the review
                self._load_review(self.review_uid)
                
                # Update statistics
                self._update_review_statistics()
            else:
                error_msg = complete_result.get('message', 'Failed to submit review')
                self.notification_area.object = f"**Error:** {error_msg}"
                
        except ResourceNotFoundError as e:
            self.notification_area.object = f"**Error:** {str(e)}"
        except ValidationError as e:
            self.notification_area.object = f"**Error:** {str(e)}"
        except PermissionError as e:
            self.notification_area.object = f"**Error:** {str(e)}"
        except BusinessRuleError as e:
            self.notification_area.object = f"**Error:** {str(e)}"
        except Exception as e:
            logger.error(f"Error submitting review: {e}")
            self.notification_area.object = f"**Error:** {str(e)}"
    
    def _view_document(self, event=None):
        """Navigate to document view"""
        if not self.document_uid:
            self.notification_area.object = "**Error:** Document ID not available"
            return
            
        # Use panel state to navigate to document page
        return pn.state.execute(f"window.location.href = '/document/{self.document_uid}'")
    
    def _format_date(self, date_str):
        """Format a date string"""
        if not date_str:
            return ""
            
        try:
            # Handle different date formats
            if isinstance(date_str, str):
                # Try ISO format first
                try:
                    date = datetime.fromisoformat(date_str)
                except ValueError:
                    # Fall back to parsing with dateutil
                    from dateutil import parser
                    date = parser.parse(date_str)
            elif isinstance(date_str, datetime):
                date = date_str
            else:
                return str(date_str)
                
            return date.strftime('%Y-%m-%d')
        except Exception:
            return str(date_str)

    def get_main_content(self):
        """Return just the main content for embedding in other panels"""
        # Ensure notification area is included with main content
        container = pn.Column(
            self.notification_area,
            self.main_content,
            sizing_mode='stretch_width'
        )
        return container

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, template, session_manager, parent_app, embedded)

Purpose: Internal method: init

Parameters:

  • template: Parameter
  • session_manager: Parameter
  • parent_app: Parameter
  • embedded: Parameter

Returns: None

_get_current_user(self) -> Optional['DocUser']

Purpose: Get the current user from session or parent app

Returns: Returns Optional['DocUser']

set_user(self, user)

Purpose: Set the current user - needed for compatibility with main app

Parameters:

  • user: Parameter

Returns: None

_setup_header(self)

Purpose: Set up the header with title and actions

Returns: None

_setup_sidebar(self)

Purpose: Set up the sidebar with navigation options

Returns: None

_setup_main_area(self)

Purpose: Set up the main area with reviews and details

Returns: None

_refresh_current_view(self, event)

Purpose: Refresh the current view

Parameters:

  • event: Parameter

Returns: None

_navigate_back(self, event)

Purpose: Navigate back to dashboard

Parameters:

  • event: Parameter

Returns: None

_update_review_statistics(self)

Purpose: Update review statistics for the current user.

Returns: None

_load_pending_reviews(self, event)

Purpose: Load pending reviews for the current user

Parameters:

  • event: Parameter

Returns: None

_load_completed_reviews(self, event)

Purpose: Load completed reviews for the current user

Parameters:

  • event: Parameter

Returns: None

_load_all_reviews(self, event)

Purpose: Load all reviews (admin view)

Parameters:

  • event: Parameter

Returns: None

_load_filtered_reviews(self, status_filter, date_from, date_to, review_type, doc_type)

Purpose: Load reviews based on filters

Parameters:

  • status_filter: Parameter
  • date_from: Parameter
  • date_to: Parameter
  • review_type: Parameter
  • doc_type: Parameter

Returns: None

_format_status_html(self, status)

Purpose: Format review status as HTML with color-coding

Parameters:

  • status: Parameter

Returns: None

_convert_neo4j_datetime(self, dt_value)

Purpose: Convert Neo4j DateTime to Python datetime

Parameters:

  • dt_value: Parameter

Returns: None

_review_selected(self, event)

Purpose: Handle review selection from table

Parameters:

  • event: Parameter

Returns: None

_load_review(self, review_uid)

Purpose: Load and display a specific review

Parameters:

  • review_uid: Parameter

Returns: None

_create_review_detail_view(self)

Purpose: Create the review detail view

Returns: None

_close_review_cycle(self, review_uid, update_status, target_status)

Purpose: Close a review cycle and optionally update document status

Parameters:

  • review_uid: Parameter
  • update_status: Parameter
  • target_status: Parameter

Returns: None

_create_reviewers_dataframe(self, reviewer_assignments, include_instructions)

Purpose: Create a DataFrame for the reviewers table

Parameters:

  • reviewer_assignments: Parameter
  • include_instructions: Parameter

Returns: None

_create_comments_area(self, comments)

Purpose: Create the comments area

Parameters:

  • comments: Parameter

Returns: None

_create_review_actions(self, my_assignment)

Purpose: Create the review actions form

Parameters:

  • my_assignment: Parameter

Returns: None

_submit_review(self, decision, comment, section)

Purpose: Submit a review decision and comment

Parameters:

  • decision: Parameter
  • comment: Parameter
  • section: Parameter

Returns: None

_view_document(self, event)

Purpose: Navigate to document view

Parameters:

  • event: Parameter

Returns: None

_format_date(self, date_str)

Purpose: Format a date string

Parameters:

  • date_str: Parameter

Returns: None

get_main_content(self)

Purpose: Return just the main content for embedding in other panels

Returns: See docstring for return details

Required Imports

import logging
from typing import Dict
from typing import List
from typing import Any
from typing import Optional

Usage Example

# Example usage:
# result = ReviewPanel(bases)

Similar Components

AI-powered semantic similarity - components with related functionality:

  • class ApprovalPanel 75.1% similar

    Approval management interface component

    From: /tf/active/vicechatdev/CDocs/ui/approval_panel_bis.py
  • class ApprovalPanel_v1 74.2% similar

    Approval management interface component

    From: /tf/active/vicechatdev/CDocs/ui/approval_panel.py
  • function create_review_panel 68.4% similar

    Factory function that creates and initializes a ReviewPanel instance with error handling, supporting both standalone and embedded modes for document review management.

    From: /tf/active/vicechatdev/CDocs/ui/review_panel.py
  • class AdminPanel 63.3% similar

    Admin configuration interface component

    From: /tf/active/vicechatdev/CDocs/ui/admin_panel.py
  • class ReviewerAssignment 54.9% similar

    Model class representing a reviewer assignment within a review cycle, managing reviewer information, status, decisions, and lifecycle tracking for document review processes.

    From: /tf/active/vicechatdev/CDocs/models/review.py
← Back to Browse