🔍 Code Extractor

class ApprovalPanel_v1

Maturity: 27

Approval management interface component

File:
/tf/active/vicechatdev/CDocs/ui/approval_panel.py
Lines:
62 - 1956
Complexity:
moderate

Purpose

Approval management interface component

Source Code

class ApprovalPanel(param.Parameterized):
    """Approval management interface component"""
    
    approval_uid = param.String(default='')
    document_uid = param.String(default='')
    current_tab = param.String(default='my_approvals')
    
    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.approval_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.approval_list_area = pn.Column(sizing_mode='stretch_width')
        self.approval_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.approval_list_area)
        self.main_content.append(self.approval_detail_area)
        self.main_content.append(self.document_detail_area)
        
        # Hide detail areas initially
        self.approval_detail_area.visible = False
        self.document_detail_area.visible = False
        
        # Load initial data
        self._load_pending_approvals()
    
    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 approval 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_approval_statistics()
            self._load_pending_approvals()
        except Exception as e:
            import logging
            logger = logging.getLogger('CDocs.ui.approval_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("# Approval 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_approvals_btn = Button(
            name='My Pending Approvals',
            button_type='primary',
            width=200
        )
        my_approvals_btn.on_click(self._load_pending_approvals)
        
        completed_approvals_btn = Button(
            name='My Completed Approvals',
            button_type='default',
            width=200
        )
        completed_approvals_btn.on_click(self._load_completed_approvals)
        
        # Add management buttons if user has manage approvals permission
        if self.user and permissions.user_has_permission(self.user, "MANAGE_REVIEWS"):
            all_approvals_btn = Button(
                name='All Approvals',
                button_type='default',
                width=200
            )
            all_approvals_btn.on_click(self._load_all_approvals)
            
            # Add to navigation area
            navigation = Column(
                Markdown("## Navigation"),
                my_approvals_btn,
                completed_approvals_btn,
                all_approvals_btn,
                sizing_mode='fixed'
            )
        else:
            # Add to navigation area without all approvals button
            navigation = Column(
                Markdown("## Navigation"),
                my_approvals_btn,
                completed_approvals_btn,
                sizing_mode='fixed'
            )
        
        self.template.sidebar.append(navigation)
        
        # Add statistics area
        self.stats_area = Column(
            Markdown("## Approval 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_approval_statistics()
    
    def _setup_main_area(self):
        """Set up the main area with approvals 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_approvals':
            self._load_pending_approvals()
        elif self.current_tab == 'completed_approvals':
            self._load_completed_approvals()
        elif self.current_tab == 'all_approvals':
            self._load_all_approvals()
        elif self.current_tab == 'approval_detail':
            self._load_approval(self.approval_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_approval_statistics(self):
        """Update approval 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.approval_controller import get_user_pending_approvals
                
                # Get pending approvals
                pending_result = get_user_pending_approvals(
                    user=self.user,
                    include_completed=False
                )
                pending_approvals = pending_result.get('approvals', [])
                pending_count = len(pending_approvals)
                
                # Get completed approvals (last 90 days)
                date_from = (datetime.now() - timedelta(days=90)).isoformat()
                try:
                    completed_result = get_user_pending_approvals(
                        user=self.user,
                        include_completed=True,
                        date_from=date_from
                    )
                    
                    # Filter completed approvals
                    all_approvals = completed_result.get('approvals', [])
                    completed_approvals = [r for r in all_approvals 
                                       if r.get('approver', {}).get('status') == 'COMPLETED']
                    completed_count = len(completed_approvals)
                    
                    # Count by decision
                    approved_count = sum(1 for r in completed_approvals 
                                      if r.get('approver', {}).get('decision') in ['APPROVED', 'CONDITIONAL'])
                    rejected_count = sum(1 for r in completed_approvals 
                                      if r.get('approver', {}).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 approvals and filter manually
                    try:
                        all_result = get_user_pending_approvals(self.user)
                        all_approvals = all_result.get('approvals', [])
                        
                        # Filter and count manually
                        for approval in all_approvals:
                            approver_data = approval.get('approver', {})
                            status = approver_data.get('status')
                            decision = approver_data.get('decision')
                            
                            if status == 'COMPLETED':
                                completed_count += 1
                                if decision in ['APPROVED', 'CONDITIONAL']:
                                    approved_count += 1
                                elif decision == 'REJECTED':
                                    rejected_count += 1
                    except Exception as filter_error:
                        logger.error(f"Error filtering approvals: {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 Approval Statistics"),
                        pn.pane.Markdown(f"**Pending Approvals:** {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.approval_controller import get_approval_statistics
                    system_stats = get_approval_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 Approval Statistics"),
                                    pn.pane.Markdown(f"**Total Approvals:** {statistics.get('total_approvals', 0)}"),
                                    pn.pane.Markdown(f"**Pending Approvals:** {statistics.get('pending_approvals', 0)}"),
                                    pn.pane.Markdown(f"**Completed Approvals:** {statistics.get('completed_approvals', 0)}"),
                                    width=300,
                                    styles={'background': '#f5f5f5', 'border': '1px solid #ddd', 'border-radius': '5px', 'padding': '10px'}
                                ),
                                pn.Column(
                                    pn.pane.Markdown("## Approval Efficiency"),
                                    pn.pane.Markdown(f"**System Approval Rate:** {statistics.get('approval_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 approval 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 \
                #                "Approval 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("## Approval 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 approval 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 approval 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_approvals(self, event=None):
        """Load pending approvals for the current user"""
        try:
            self.current_tab = 'my_approvals'
            self.notification_area.object = "Loading your pending approvals..."
            
            # Clear areas and prepare main content
            self.approval_list_area.clear()
            self.approval_detail_area.clear()
            self.document_detail_area.clear()
            self.approval_detail_area.visible = False
            self.document_detail_area.visible = False
            
            self.main_content.clear()
            self._update_approval_statistics()
            
            # Add content areas back to main_content
            self.main_content.append(self.stats_area)
            self.main_content.append(self.approval_list_area)
            self.main_content.append(self.approval_detail_area)
            self.main_content.append(self.document_detail_area)
            
            if not self.user:
                self.approval_list_area.append(Markdown("# My Pending Approvals"))
                self.approval_list_area.append(Markdown("Please log in to view your pending approvals."))
                self.notification_area.object = ""
                return
            
            # Get pending approvals
            try:
                from CDocs.controllers.approval_controller import get_user_assigned_approvals
                
                # Get only active approvals (PENDING and ACTIVE status)
                result = get_user_assigned_approvals(
                    user=self.user,
                    status_filter=["PENDING", "ACTIVE"],
                    include_completed=False
                )
                
                # Prepare data for display
                assignments = result.get('assignments', [])
                if not assignments:
                    self.approval_list_area.append(Markdown("# My Pending Approvals"))
                    self.approval_list_area.append(Markdown("You have no pending approvals."))
                    self.notification_area.object = ""
                    return
                    
                # Convert to DataFrame for table display
                approvals_data = []
                for assignment in assignments:
                    approval_cycle = assignment.get('approval_cycle', {})
                    document = assignment.get('document', {})
                    
                    approvals_data.append({
                        'approval_uid': approval_cycle.get('UID', ''),
                        'document_uid': document.get('uid', ''),
                        'doc_number': document.get('doc_number', ''),
                        'title': document.get('title', ''),
                        'status': approval_cycle.get('status', ''),
                        'approval_type': approval_cycle.get('approval_type', ''),
                        'initiated_date': approval_cycle.get('startDate', approval_cycle.get('start_date', '')),
                        'due_date': approval_cycle.get('dueDate', approval_cycle.get('due_date', '')),
                        'approver_status': assignment.get('assignment', {}).get('status', '')
                    })
                

                # Create DataFrame
                df = pd.DataFrame(approvals_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'] = 'Approval'
                
                # Display columns setup
                display_columns = ['doc_number', 'title', 'status', 'approval_type', 
                                  'initiated_date', 'due_date', 'action','approval_uid']
                column_names = {
                    'doc_number': 'Document',
                    'title': 'Title',
                    'status': 'Status',
                    'approval_type': 'Type',
                    'initiated_date': 'Started',
                    'due_date': 'Due Date',
                    'action': 'Action',
                    'approval_uid': 'approval_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
                approvals_table = Tabulator(
                    df,
                    pagination='local',
                    page_size=10,
                    sizing_mode='stretch_width',
                    selectable=1,
                    height=400,
                    hidden_columns=['approval_uid']
                )
                
                # Add click handler
                approvals_table.on_click(self._approval_selected)
                
                # Add to approval list area
                self.approval_list_area.append(Markdown("# My Pending Approvals"))
                self.approval_list_area.append(Markdown(f"You have {len(approvals_data)} pending approval(s) to complete."))
                self.approval_list_area.append(approvals_table)
                
                # Clear notification
                self.notification_area.object = ""
                
            except Exception as e:
                logger.error(f"Error fetching pending approvals: {e}")
                self.approval_list_area.append(Markdown("# My Pending Approvals"))
                self.approval_list_area.append(Markdown(f"Error fetching pending approvals: {str(e)}"))
                self.notification_area.object = ""
                
        except Exception as e:
            logger.error(f"Error loading pending approvals: {e}")
            self.notification_area.object = f"**Error:** {str(e)}"
            self.main_content.clear()
            self.main_content.append(self.stats_area)
            self.approval_list_area.clear()
            self.approval_list_area.append(Markdown("# My Pending Approvals"))
            self.approval_list_area.append(Markdown("*Error loading pending approvals*"))
            self.main_content.append(self.approval_list_area)
    
    def _load_completed_approvals(self, event=None):
        """Load completed approvals for the current user"""
        try:
            self.current_tab = 'completed_approvals'
            self.notification_area.object = "Loading your completed approvals..."
            
            # Clear areas and prepare main content
            self.approval_list_area.clear()
            self.approval_detail_area.clear()
            self.document_detail_area.clear()
            self.approval_detail_area.visible = False
            self.document_detail_area.visible = False
            
            self.main_content.clear()
            self._update_approval_statistics()
            
            # Add content areas back to main_content
            self.main_content.append(self.stats_area)
            self.main_content.append(self.approval_list_area)
            self.main_content.append(self.approval_detail_area)
            self.main_content.append(self.document_detail_area)
            
            if not self.user:
                self.approval_list_area.append(Markdown("# My Completed Approvals"))
                self.approval_list_area.append(Markdown("Please log in to view your completed approvals."))
                self.notification_area.object = ""
                return
            
            # Get completed approvals
            try:
                from CDocs.controllers.approval_controller import get_user_assigned_approvals
                
                # Get only completed approvals
                result = get_user_assigned_approvals(
                    user=self.user,
                    status_filter=["COMPLETED"],
                    include_completed=True
                )
                
                # Prepare data for display
                assignments = result.get('assignments', [])
                if not assignments:
                    self.approval_list_area.append(Markdown("# My Completed Approvals"))
                    self.approval_list_area.append(Markdown("You have no completed approvals in the last 90 days."))
                    self.notification_area.object = ""
                    return
                    
                # Convert to DataFrame for table display
                approvals_data = []
                for assignment in assignments:
                    approval_cycle = assignment.get('approval_cycle', {})
                    document = assignment.get('document', {})
                    approver_assignment = assignment.get('assignment', {})
                    
                    approvals_data.append({
                        'approval_uid': approval_cycle.get('UID', ''),
                        'document_uid': document.get('uid', ''),
                        'doc_number': document.get('doc_number', ''),
                        'title': document.get('title', ''),
                        'status': approval_cycle.get('status', ''),
                        'approval_type': approval_cycle.get('approval_type', ''),
                        'completed_date': approver_assignment.get('decision_date', ''),
                        'decision': approver_assignment.get('decision', '')
                    })
                
                # Create DataFrame
                df = pd.DataFrame(approvals_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', 'approval_type', 
                                  'completed_date', 'decision', 'action','approval_uid']
                column_names = {
                    'doc_number': 'Document',
                    'title': 'Title',
                    'status': 'Status',
                    'approval_type': 'Type',
                    'completed_date': 'Completed',
                    'decision': 'Decision',
                    'action': 'Action',
                    'approval_uid': 'approval_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
                approvals_table = Tabulator(
                    df,
                    pagination='local',
                    page_size=10,
                    sizing_mode='stretch_width',
                    selectable=1,
                    height=400,
                    hidden_columns=['approval_uid']
                )
                
                # Add click handler
                approvals_table.on_click(self._approval_selected)
                
                # Add to approval list area
                self.approval_list_area.append(Markdown("# My Completed Approvals"))
                self.approval_list_area.append(Markdown(f"You have completed {len(approvals_data)} approval(s)."))
                self.approval_list_area.append(approvals_table)
                
                # Clear notification
                self.notification_area.object = ""
                
            except Exception as e:
                logger.error(f"Error fetching completed approvals: {e}")
                self.approval_list_area.append(Markdown("# My Completed Approvals"))
                self.approval_list_area.append(Markdown(f"Error fetching completed approvals: {str(e)}"))
                self.notification_area.object = ""
                
        except Exception as e:
            logger.error(f"Error loading completed approvals: {e}")
            self.notification_area.object = f"**Error:** {str(e)}"
            self.main_content.clear()
            self.main_content.append(self.stats_area)
            self.approval_list_area.clear()
            self.approval_list_area.append(Markdown("# My Completed Approvals"))
            self.approval_list_area.append(Markdown("*Error loading completed approvals*"))
            self.main_content.append(self.approval_list_area)
    
    def _load_all_approvals(self, event=None):
        """Load all approvals (admin view)"""
        try:
            self.current_tab = 'all_approvals'
            self.notification_area.object = "Loading all approvals..."
            
            # Clear areas and prepare main content
            self.approval_list_area.clear()
            self.approval_detail_area.clear()
            self.document_detail_area.clear()
            self.approval_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_approval_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.approval_list_area)
            self.main_content.append(self.approval_detail_area)
            self.main_content.append(self.document_detail_area)
            
            if not self.user:
                self.approval_list_area.append(pn.pane.Markdown("# All Approvals"))
                self.approval_list_area.append(pn.pane.Markdown("Please log in to view approvals."))
                self.notification_area.object = ""
                return
            
            # Check permission
            if not permissions.user_has_permission(self.user, "MANAGE_REVIEWS"):
                self.approval_list_area.append(pn.pane.Markdown("# All Approvals"))
                self.approval_list_area.append(pn.pane.Markdown("You do not have permission to view all approvals."))
                self.notification_area.object = ""
                return
            
            # Create a main container that will hold all content with explicit structure
            all_approvals_container = pn.Column(sizing_mode='stretch_width')
            
            # Add title to the main container
            all_approvals_container.append(pn.pane.Markdown("# All Approvals"))
            
            # 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 approval type filter
            approval_type_filter = pn.widgets.Select(
                name='Approval 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,
                        approval_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(("Approval Filters", filters_panel))
            
            # Create the table area with its own container and margin
            self._approvals_table_area = pn.Column(
                pn.pane.Markdown("*Loading approvals...*"),
                margin=(10, 10, 10, 10),
                sizing_mode='stretch_width'
            )
            
            # Add table area to accordion - always start expanded
            accordion.append(("Approval List", self._approvals_table_area))
            
            # Always show the approval list panel by opening that accordion tab
            accordion.active = [0,1]  # Open the second panel (Approval List)
            
            # Add accordion to main container
            all_approvals_container.append(accordion)
            
            # Add the full container to the approval list area
            self.approval_list_area.append(all_approvals_container)
            
            # Set up filter button handler
            def filter_approvals(event):
                self._load_filtered_approvals(
                    status_filter=status_filter.value,
                    date_from=date_range.value[0],
                    date_to=date_range.value[1], 
                    approval_type=approval_type_filter.value if approval_type_filter.value else None,
                    doc_type=doc_type_filter.value if doc_type_filter.value else None
                )
            
            filter_btn.on_click(filter_approvals)
            
            # Initial load of approvals with default filters
            self._load_filtered_approvals(
                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 approvals: {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.approval_list_area.clear()
            self.approval_list_area.append(pn.pane.Markdown("# All Approvals"))
            self.approval_list_area.append(pn.pane.Markdown(f"*Error loading all approvals: {str(e)}*"))
            self.main_content.append(self.approval_list_area)

    def _load_filtered_approvals(self, status_filter=None, date_from=None, date_to=None, approval_type=None, doc_type=None):
        """Load approvals based on filters"""
        try:
            # Ensure the table area exists and show loading indicator
            #if not hasattr(self, '_approvals_table_area') or self._approvals_table_area is None:
                # ... existing code for table area setup ...
                
            self._approvals_table_area.clear()
            self._approvals_table_area.append(pn.pane.Markdown("*Loading approvals...*"))
            
            # 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 approval 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
                
                # Approval type filter
                if approval_type:
                    where_conditions.append("r.approval_type = $approval_type")
                    params["approval_type"] = approval_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 approval cycles with document info
                query = f"""
                MATCH (r:ApprovalCycle)-[:FOR_REVIEW]->(v:DocumentVersion)<-[:HAS_VERSION]-(d:ControlledDocument)
                {status_condition}
                // Count approvers using ApproverAssignment nodes
                OPTIONAL MATCH (r)-[:ASSIGNMENT]->(a:ApproverAssignment)
                WITH r, d, v, COUNT(a) as approver_count_assign
                // Count approvers using REVIEWED_BY relationship (for backward compatibility)
                OPTIONAL MATCH (r)-[:REVIEWED_BY]->(u:User)
                WITH r, d, v, approver_count_assign, COUNT(u) as approver_count_rel
                // Use the higher count (for compatibility with both data models)
                RETURN 
                    r.UID as approval_uid,
                    r.status as status,
                    r.approval_type as approval_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 approver_count_rel > approver_count_assign 
                         THEN approver_count_rel 
                         ELSE approver_count_assign 
                    END as approver_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._approvals_table_area.clear()
                
                # Check if we have results
                if not result:
                    self._approvals_table_area.append(pn.pane.Markdown("*No approvals found matching the filter criteria*"))
                    return
                    
                # Prepare data for table
                approvals_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
                    approvals_data.append({
                        'approval_uid': record.get('approval_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'),
                        'approval_type': record.get('approval_type'),
                        'initiated_by': record.get('initiated_by'),
                        'approver_count': record.get('approver_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._approvals_table_area.append(pn.pane.Markdown(f"### Found {len(approvals_data)} approvals"))
                
                # Create DataFrame
                df = pd.DataFrame(approvals_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', 'approval_type', 
                                'initiated_by', 'approver_count', 'start_date', 'due_date', 'action', 'approval_uid', 'status']
                column_names = {
                    'doc_number': 'Document',
                    'title': 'Title',
                    'version': 'Version',
                    'status_formatted': 'Status',  # This will display the HTML-formatted status
                    'approval_type': 'Type',
                    'initiated_by': 'Initiated By',
                    'approver_count': 'Approvers',
                    'start_date': 'Started',
                    'due_date': 'Due Date',
                    'action': 'Action',
                    'approval_uid': 'approval_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
                approvals_table = pn.widgets.Tabulator(
                    df,
                    formatters=formatters,
                    pagination='local',
                    page_size=10,
                    sizing_mode='stretch_width',
                    selectable=1,
                    height=400,
                    hidden_columns=['approval_uid', 'raw_status']
                )
                
                # Add click handler with debugger info
                def table_click_handler(event):
                    logger.debug(f"Table click event: {event}")
                    self._approval_selected(event)
                    
                approvals_table.on_click(table_click_handler)
                
                # Add table to the area 
                self._approvals_table_area.append(approvals_table)
                
            except Exception as db_error:
                logger.error(f"Database error in _load_filtered_approvals: {db_error}")
                logger.error(traceback.format_exc())
                self._approvals_table_area.clear()
                self._approvals_table_area.append(pn.pane.Markdown(f"**Error loading approvals from database:** {str(db_error)}"))
                
        except Exception as e:
            logger.error(f"Error in _load_filtered_approvals: {e}")
            logger.error(traceback.format_exc())
            if hasattr(self, '_approvals_table_area'):
                self._approvals_table_area.clear()
                self._approvals_table_area.append(pn.pane.Markdown(f"**Error loading approvals:** {str(e)}"))
    
    def _format_status_html(self, status):
        """Format approval 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 _approval_selected(self, event):
        """Handle approval selection from table"""
        try:
            # Debug the event
            logger.debug(f"Approval selection event type: {type(event).__name__}")
            
            # Handle different event types
            row_index = None
            approval_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 approval_uid in source data
                    uid_keys = ['approval_uid', '_approval_uid', 'UID', 'uid']
                    for key in uid_keys:
                        if key in source_data and len(source_data[key]) > row_index:
                            approval_uid = source_data[key][row_index]
                            logger.debug(f"Found approval_uid using key '{key}': {approval_uid}")
                            break
                    
                    # If still no UID found, try to extract from data columns
                    if not approval_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:
                                approval_uid = source_data[key][row_index]
                                logger.debug(f"Found approval_uid from column '{key}': {approval_uid}")
                                break
                                
                # Last resort - try getting the cell's value directly
                if not approval_uid and hasattr(event, 'value'):
                    approval_uid = event.value
                    logger.debug(f"Using cell value as approval_uid: {approval_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 = ['approval_uid', '_approval_uid', 'UID', 'uid']
                    for col in uid_columns:
                        if col in df.columns:
                            approval_uid = df.iloc[selected_idx][col]
                            logger.debug(f"Found approval_uid in column '{col}': {approval_uid}")
                            break
                            
                    # If still no UID found, check through all columns that might contain UID
                    if not approval_uid:
                        for col in df.columns:
                            if '_uid' in col.lower():
                                approval_uid = df.iloc[selected_idx][col]
                                logger.debug(f"Found approval_uid in column '{col}': {approval_uid}")
                                break
            
            # Exit if we couldn't determine the approval UID
            if not approval_uid:
                logger.warning("Could not determine approval UID from selection event")
                logger.debug(f"Full event object: {str(event)}")
                self.notification_area.object = "**Error:** Could not determine approval ID"
                return
            
            # Log found UID
            logger.info(f"Loading approval with UID: {approval_uid}")
            
            # Load the selected approval
            self._load_approval(approval_uid)
                
        except Exception as e:
            logger.error(f"Error selecting approval: {e}")
            logger.error(traceback.format_exc())
            self.notification_area.object = f"**Error:** {str(e)}"
    
    def _load_approval(self, approval_uid):
        """Load and display a specific approval"""
        try:
            self.current_tab = 'approval_detail'
            self.approval_uid = approval_uid
            self.notification_area.object = "Loading approval details..."
            
            # Clear areas and prepare main content
            self.approval_list_area.clear()
            self.approval_detail_area.clear()
            self.document_detail_area.clear()
            self.approval_detail_area.visible = True
            
            self.main_content.clear()
            self._update_approval_statistics()
            
            # Add content areas back to main_content
            self.main_content.append(self.stats_area)
            self.main_content.append(self.approval_detail_area)
            
            if not self.user:
                self.approval_detail_area.append(Markdown("# Approval Details"))
                self.approval_detail_area.append(Markdown("Please log in to view approval details."))
                self.notification_area.object = ""
                return
            
            # Get approval data from approval_controller get_approval_cycle function
            try:
                from CDocs.controllers.approval_controller import get_approval_cycle
                #logger.info("approval_uid: %s", approval_uid)
                
                approval_result = get_approval_cycle(
                    approval_uid=approval_uid,
                    include_comments=True,
                    include_document=True
                )
                
                if not approval_result:
                    self.approval_detail_area.append(Markdown("# Approval Details"))
                    self.approval_detail_area.append(Markdown("Approval not found or you do not have permission to view it."))
                    self.notification_area.object = ""
                    return
                
                # Store data for later use
                self.approval_data = approval_result
                document_data = approval_result.get('document', {})
                self.document_data = document_data
                self.document_uid = document_data.get('uid', document_data.get('UID', ''))
                
                # Create the approval detail view
                self._create_approval_detail_view()
                
                # Clear notification
                self.notification_area.object = ""
                
            except ResourceNotFoundError:
                self.notification_area.object = "**Error:** Approval not found"
                self.approval_detail_area.append(Markdown("# Approval Details"))
                self.approval_detail_area.append(Markdown("Approval not found."))
            except Exception as e:
                logger.error(f"Error loading approval: {e}")
                self.notification_area.object = f"**Error:** {str(e)}"
                self.approval_detail_area.append(Markdown("# Approval Details"))
                self.approval_detail_area.append(Markdown(f"Error loading approval: {str(e)}"))
        except Exception as e:
            logger.error(f"Error in _load_approval: {e}")
            self.notification_area.object = f"**Error:** {str(e)}"
    
    def _create_approval_detail_view(self):
        """Create the approval detail view"""
        if not self.approval_data:
            return
            
        # Extract data
        approval = self.approval_data
        document = self.document_data
        
        # Get approval metadata with proper fallbacks for different field names
        status = approval.get('status', '')
        approval_type = approval.get('approval_type', '')
        initiated_date = self._format_date(approval.get('startDate', approval.get('initiated_date', approval.get('start_date', ''))))
        due_date = self._format_date(approval.get('dueDate', approval.get('due_date', '')))
        initiated_by = approval.get('initiated_by_name', '')
        instructions = approval.get('instructions', '')
        
        # Check if approval is sequential
        is_sequential = approval.get('sequential', False)
        
        # Get approver data - handle different data structures
        approver_assignments = approval.get('approver_assignments', [])
        
        # Check for empty or missing assignments and look for approvers in alternate locations
        if not approver_assignments and 'approvers' in approval:
            # Try to convert approvers list to expected assignment format
            approvers = approval.get('approvers', [])
            approver_assignments = []
            for approver in approvers:
                assignment = {
                    'approver_uid': approver.get('UID', ''),
                    'approver_name': approver.get('name', ''),
                    'role': approver.get('role', ''),
                    'status': approver.get('status', 'PENDING')
                }
                approver_assignments.append(assignment)
                
        # Find the current user's assignment
        my_assignment = None
        if self.user:
            my_assignment = next((r for r in approver_assignments 
                            if r.get('approver_uid') == self.user.uid), None)
        
        # Get comment data with fallback options
        comments = approval.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 approval detail view
        approval_detail = pn.Column(
            sizing_mode='stretch_width'
        )
        
        # Add header with document info
        approval_detail.append(pn.pane.Markdown(f"# Approval for {doc_number} Rev {doc_revision}"))
        approval_detail.append(pn.pane.Markdown(f"## {doc_title}"))
        
        # Create summary card
        summary_card = pn.Column(
            pn.pane.Markdown("### Approval Summary"),
            pn.pane.Markdown(f"**Status:** {status}"),
            pn.pane.Markdown(f"**Type:** {approval_type}"),
            pn.pane.Markdown(f"**Started:** {initiated_date}"),
            pn.pane.Markdown(f"**Due Date:** {due_date}"),
            pn.pane.Markdown(f"**Initiated By:** {initiated_by}"),
            width=350,
            styles={'background':'#f8f9fa'},
            css_classes=['p-3', 'border', 'rounded']
        )
        
        # Add sequential approval indicator if this is a sequential approval
        if is_sequential:
            summary_card.append(pn.pane.Markdown("**Sequential Approval:** Yes"))
        
        # Create document info card
        # Use DocumentAccessControls for proper button handling
        try:
            from CDocs.ui.components.document_access_controls import DocumentAccessControls
            
            # Create access controls for this document
            access_controls = DocumentAccessControls(
                document_uid=self.document_uid,
                user_uid=self.user.uid if self.user else None,
                show_access_indicator=False
            )
            
            doc_access_view = access_controls.view()
            
        except Exception as e:
            logger.warning(f"Error creating access controls for {self.document_uid}: {e}")
            # Fallback to simple button
            doc_info_btn = pn.widgets.Button(name="View Document", button_type="primary", width=150)
            doc_info_btn.on_click(self._view_document)
            doc_access_view = doc_info_btn
        
        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', '')}"),
            doc_access_view,
            width=350,
            styles={'background':'#f8f9fa'},
            css_classes=['p-3', 'border', 'rounded']
        )
        
        # Add cards to layout
        approval_detail.append(pn.Row(
            summary_card,
            pn.Column(width=20),  # spacing
            doc_info_card,
            sizing_mode='stretch_width'
        ))
        
        # Add instructions if available
        if instructions:
            approval_detail.append(pn.pane.Markdown("### Approval Instructions"))
            approval_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('approver_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'
                )
                approval_detail.append(your_instructions)
        
        # Create approvers table with error handling for data structure
        try:
            # Check if user has permission to see all approver instructions
            can_see_all_instructions = permissions.user_has_permission(self.user, "MANAGE_REVIEWS")
            
            approvers_df = self._create_approvers_dataframe(approver_assignments, 
                                                          include_instructions=can_see_all_instructions)
            
            # Create approvers table
            approvers_table = pn.widgets.Tabulator(
                approvers_df,
                sizing_mode='stretch_width',
                height=200
            )
            
            # Add approvers section
            approval_detail.append(pn.pane.Markdown("## Approvers"))
            approval_detail.append(approvers_table)
        except Exception as e:
            logger.error(f"Error creating approvers table: {e}")
            approval_detail.append(pn.pane.Markdown("## Approvers"))
            approval_detail.append(pn.pane.Markdown("*Error loading approver data*"))
        
        # Add comments section
        approval_detail.append(pn.pane.Markdown("## Comments"))
        
        # Create comments area with error handling
        try:
            comments_area = self._create_comments_area(comments)
            approval_detail.append(comments_area)
        except Exception as e:
            logger.error(f"Error creating comments area: {e}")
            approval_detail.append(pn.pane.Markdown("*Error loading comments*"))
    
        # Add close approval button if approval 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, approval initiator, or has manage permission
            is_document_owner = document.get('owner_uid') == self.user.uid
            is_approval_initiator = approval.get('initiated_by_uid') == self.user.uid
            has_manage_permission = permissions.user_has_permission(self.user, "MANAGE_REVIEWS")
            
            if is_document_owner or is_approval_initiator or has_manage_permission:
                # Create close approval section
                close_approval_section = pn.Column(
                    pn.pane.Markdown("## Close Approval Cycle"),
                    pn.pane.Markdown("The approval cycle is completed. You can now close it and update the document status."),
                    sizing_mode='stretch_width'
                )
                
                # Create status dropdown
                status_select = pn.widgets.Select(
                    name="Update Document Status To",
                    options=['DRAFT', 'APPROVED'],
                    value='DRAFT',
                    width=200
                )
                
                # Create checkbox for updating status
                update_status_checkbox = pn.widgets.Checkbox(
                    name="Update Document Status",
                    value=True,
                    width=200
                )
                
                # Create close button
                close_btn = pn.widgets.Button(
                    name="Close Approval Cycle",
                    button_type="primary",
                    width=150
                )
                
                # Create form layout
                close_form = pn.Column(
                    pn.Row(
                        pn.Column(update_status_checkbox, status_select),
                        pn.layout.HSpacer(),
                        pn.Column(close_btn, align='end')
                    ),
                    styles={'background':'#f8f9fa'},
                    css_classes=['p-3', 'border', 'rounded'],
                    sizing_mode='stretch_width'
                )
                
                # Add to section
                close_approval_section.append(close_form)
                
                # Add handler
                close_btn.on_click(lambda event: self._close_approval_cycle(
                    approval_uid=self.approval_uid,
                    update_status=update_status_checkbox.value,
                    target_status=status_select.value
                ))
                
                # Add section to approval detail
                approval_detail.append(close_approval_section)
        
        # Check if user can approval based on sequential or non-sequential flow
        can_approval = True
        if my_assignment:
            # Always allow if status is active
            if my_assignment.get('status') == 'ACTIVE':
                can_approval = True
            # For pending status, check sequential rules
            elif my_assignment.get('status') == 'PENDING':
                # In sequential mode, need to check if this approver is next
                if is_sequential:
                    # User can only approval if they're active (their turn in sequence)
                    can_approval = False
                    
                    # Find active approver
                    active_approver = next((r for r in approver_assignments if r.get('status') == 'ACTIVE'), None)
                    
                    # If no active approver and this user is first in sequence
                    if not active_approver and my_assignment.get('sequence_order', 999) == 1:
                        can_approval = True
                else:
                    # Non-sequential approval - all pending approvers can approval
                    can_approval = True
        
        # Add approval actions if user is a pending or active approver
        if my_assignment and my_assignment.get('status') in ['PENDING', 'ACTIVE']:
            if can_approval:
                # User can approval now, show approval form
                approval_actions = self._create_approval_actions(my_assignment)
                approval_detail.append(pn.pane.Markdown("## Your Approval"))
                approval_detail.append(approval_actions)
            elif is_sequential:
                # Sequential approval - show waiting message
                waiting_message = pn.Column(
                    pn.pane.Markdown("## Your Approval"),
                    pn.pane.Markdown("This is a **sequential approval**. You will be notified when it's your turn to approval."),
                    pn.pane.Markdown("Approvers must complete their approvals in the specified order."),
                    styles={'background':'#f8f9fa'},
                    css_classes=['p-3', 'border', 'rounded'],
                    sizing_mode='stretch_width'
                )
                approval_detail.append(waiting_message)
        
        # Add to approval detail area
        self.approval_detail_area.clear()
        self.approval_detail_area.append(approval_detail)

    def _close_approval_cycle(self, approval_uid, update_status=True, target_status="DRAFT"):
        """Close a approval cycle and optionally update document status"""
        try:
            self.notification_area.object = "Closing approval cycle..."
            
            # Call controller to close approval cycle
            from CDocs.controllers.approval_controller import close_approval_cycle
            result = close_approval_cycle(
                user=self.user,
                approval_uid=approval_uid,
                update_document_status=update_status,
                target_status=target_status
            )
            
            if result['success']:
                self.notification_area.object = "**Success:** Approval 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 approval details after a short delay
                time.sleep(2)
                self._load_approval(approval_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 approval cycle: {e}")
            logger.error(traceback.format_exc())
            self.notification_area.object = f"**Error:** An unexpected error occurred"
    

    def _create_approvers_dataframe(self, approver_assignments, include_instructions=False):
        """Create a DataFrame for the approvers table"""
        # Create data for table
        approvers_data = []
        
        for assignment in approver_assignments:
            # Format dates
            assigned_date = self._format_date(assignment.get('assigned_date'))
            decision_date = self._format_date(assignment.get('decision_date'))
            
            # Add to data
            approver_data = {
                'approver_name': assignment.get('approver_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:
                approver_data['instructions'] = assignment.get('instructions', '')
            
            approvers_data.append(approver_data)
        
        # Create DataFrame
        df = pd.DataFrame(approvers_data)
        
        # Select and rename columns for display
        display_columns = ['approver_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 approvers_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 = {
            'approver_name': 'Approver',
            '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_approval_actions(self, my_assignment):
        """Create the approval 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 approval 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', 
                'CONDITIONAL': 'CONDITIONAL'
            },
            button_type='success'
        )
        
        # Create submit button
        submit_btn = Button(
            name="Submit Approval",
            button_type="primary",
            width=150
        )
        
        # Set up event handler
        submit_btn.on_click(lambda event: self._submit_approval(
            decision_group.value,
            comment_input.value,
            section_input.value
        ))
        
        # Create form layout adding all components
        components.extend([
            Row(
                Column(
                    Markdown("### Your Approval Decision"),
                    decision_group,
                    width=400
                ),
                Column(
                    Markdown("### Add Comment"),
                    section_input,
                    comment_input,
                    width=600
                ),
                align='start'
            ),
            Row(
                submit_btn,
                align='end'
            )
        ])
        
        approval_actions = Column(
            *components,
            styles={'background':'#f8f9fa'},
            css_classes=['p-3', 'border', 'rounded'],
            sizing_mode='stretch_width'
        )
        
        return approval_actions
    
    def _submit_approval(self, decision, comment, section):
        """Submit a approval decision and comment"""
        try:
            if not self.approval_uid:
                self.notification_area.object = "**Error:** No approval 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_approval_comment(
                        user=self.user,
                        approval_uid=self.approval_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 approval comment: {comment_error}")
                    self.notification_area.object = f"**Warning:** Could not add comment, but proceeding with approval submission: {str(comment_error)}"
            
            # Then complete the approval
            complete_result = complete_approval(
                user=self.user,
                approval_uid=self.approval_uid,
                decision=decision,
                comments=comment if comment else ""
            )
            
            if complete_result and complete_result.get('success'):
                self.notification_area.object = "Approval submitted successfully"
                
                # Reload the approval
                self._load_approval(self.approval_uid)
                
                # Update statistics
                self._update_approval_statistics()
            else:
                error_msg = complete_result.get('message', 'Failed to submit approval')
                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 approval: {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 approvals 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_approval_statistics(self)

Purpose: Update approval statistics for the current user.

Returns: None

_load_pending_approvals(self, event)

Purpose: Load pending approvals for the current user

Parameters:

  • event: Parameter

Returns: None

_load_completed_approvals(self, event)

Purpose: Load completed approvals for the current user

Parameters:

  • event: Parameter

Returns: None

_load_all_approvals(self, event)

Purpose: Load all approvals (admin view)

Parameters:

  • event: Parameter

Returns: None

_load_filtered_approvals(self, status_filter, date_from, date_to, approval_type, doc_type)

Purpose: Load approvals based on filters

Parameters:

  • status_filter: Parameter
  • date_from: Parameter
  • date_to: Parameter
  • approval_type: Parameter
  • doc_type: Parameter

Returns: None

_format_status_html(self, status)

Purpose: Format approval 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

_approval_selected(self, event)

Purpose: Handle approval selection from table

Parameters:

  • event: Parameter

Returns: None

_load_approval(self, approval_uid)

Purpose: Load and display a specific approval

Parameters:

  • approval_uid: Parameter

Returns: None

_create_approval_detail_view(self)

Purpose: Create the approval detail view

Returns: None

_close_approval_cycle(self, approval_uid, update_status, target_status)

Purpose: Close a approval cycle and optionally update document status

Parameters:

  • approval_uid: Parameter
  • update_status: Parameter
  • target_status: Parameter

Returns: None

_create_approvers_dataframe(self, approver_assignments, include_instructions)

Purpose: Create a DataFrame for the approvers table

Parameters:

  • approver_assignments: Parameter
  • include_instructions: Parameter

Returns: None

_create_comments_area(self, comments)

Purpose: Create the comments area

Parameters:

  • comments: Parameter

Returns: None

_create_approval_actions(self, my_assignment)

Purpose: Create the approval actions form

Parameters:

  • my_assignment: Parameter

Returns: None

_submit_approval(self, decision, comment, section)

Purpose: Submit a approval 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 = ApprovalPanel(bases)

Similar Components

AI-powered semantic similarity - components with related functionality:

  • class ApprovalPanel 98.0% similar

    Approval management interface component

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

    Review management interface component

    From: /tf/active/vicechatdev/CDocs/ui/review_panel.py
  • function create_approval_panel_v1 72.2% similar

    Factory function that creates and initializes an ApprovalPanel instance for managing document approvals, with error handling and fallback to a minimal panel on failure.

    From: /tf/active/vicechatdev/CDocs/ui/approval_panel.py
  • function create_approval_panel 70.0% similar

    Factory function that creates and initializes an ApprovalPanel instance with error handling, supporting both standalone and embedded modes for document approval management.

    From: /tf/active/vicechatdev/CDocs/ui/approval_panel_bis.py
  • class AdminPanel 68.5% similar

    Admin configuration interface component

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