🔍 Code Extractor

class ApprovalPanel

Maturity: 28

Approval management interface component inheriting from WorkflowPanelBase

File:
/tf/active/vicechatdev/CDocs single class/ui/approval_panel.py
Lines:
42 - 636
Complexity:
moderate

Purpose

Approval management interface component inheriting from WorkflowPanelBase

Source Code

class ApprovalPanel(WorkflowPanelBase):
    """Approval management interface component inheriting from WorkflowPanelBase"""
    
    def __init__(self, template, session_manager=None, parent_app=None, embedded=False, controller=None, **params):
        """
        Initialize the approval panel.
        
        Args:
            template: Panel template for displaying the UI
            session_manager: Authentication session manager
            parent_app: Parent application reference for navigation
            embedded: Whether this panel is embedded in another UI
            controller: Approval controller instance
        """
        # Import controller if not provided
        if controller is None:
            from CDocs.controllers.approval_controller import _controller
            controller = _controller
            
        # Initialize base class with approval-specific settings
        super().__init__(
            template=template,
            session_manager=session_manager,
            parent_app=parent_app,
            embedded=embedded,
            workflow_type='APPROVAL',
            **params
        )
        
        # Set controller
        self.controller = controller
    
    def _create_workflow_detail_view(self):
        """Create the approval detail view"""
        if not self.workflow_data:
            return
            
        # Extract data
        approval = self.workflow_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', '')
        is_sequential = approval.get('sequential', True)
        current_sequence = approval.get('current_sequence', 0)
        
        # 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:
                # Create a simple assignment-like structure
                assignment = {
                    'approver_uid': approver.get('UID'),
                    'approver_name': approver.get('name'),
                    'status': 'PENDING'
                }
                approver_assignments.append(assignment)
        
        # Find the current user's assignment
        my_assignment = None
        if self.user:
            my_assignment = next((a for a in approver_assignments 
                            if a.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}"),
            pn.pane.Markdown(f"**Approval Flow:** {'Sequential' if is_sequential else 'Parallel'}"),
            width=350,
            styles={'background':'#f8f9fa'},
            css_classes=['p-3', 'border', 'rounded']
        )
        
        # Create document info card
        doc_info_card = pn.Column(
            pn.pane.Markdown("### Document Information"),
            pn.pane.Markdown(f"**Number:** {doc_number}"),
            pn.pane.Markdown(f"**Revision:** {doc_revision}"),
            pn.pane.Markdown(f"**Type:** {document.get('doc_type', document.get('docType', ''))}"),
            pn.pane.Markdown(f"**Department:** {document.get('department', '')}"),
            Button(name="View Document", button_type="primary", width=150, on_click=self._view_document),
            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))
        
        # Create approvers table with error handling for data structure
        try:
            approvers_df = self._create_participants_dataframe(approver_assignments)
            
            # Create approvers table
            approvers_table = 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(f"*Error loading approver data: {str(e)}*"))
        
        # 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(f"*Error loading comments: {str(e)}*"))

        # Add cancel approval button if approval is in progress and user has permission
        if status in ['PENDING', 'IN_PROGRESS'] 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_APPROVALS")
            
            if is_document_owner or is_approval_initiator or has_manage_permission:
                cancel_button = Button(
                    name="Cancel Approval",
                    button_type="danger",
                    width=150
                )
                cancel_button.on_click(lambda event: self._cancel_approval_cycle(approval.get('UID')))
                
                cancel_section = pn.Row(
                    cancel_button,
                    sizing_mode='stretch_width',
                    align='end'
                )
                approval_detail.append(cancel_section)
        
        # Add extend deadline button if user has permission
        if status in ['PENDING', 'IN_PROGRESS'] and self.user:
            # Check if user has manage permission
            if permissions.user_has_permission(self.user, "MANAGE_APPROVALS"):
                extend_button = Button(
                    name="Extend Deadline",
                    button_type="warning",
                    width=150
                )
                extend_button.on_click(lambda event: self._show_extend_deadline_form(approval.get('UID')))
                
                # If the cancel section already exists, add to it, otherwise create new
                if 'cancel_section' in locals():
                    cancel_section.append(extend_button)
                else:
                    extend_section = pn.Row(
                        extend_button,
                        sizing_mode='stretch_width',
                        align='end'
                    )
                    approval_detail.append(extend_section)
        
        # Add approval actions if user is a pending approver
        if my_assignment and (my_assignment.get('status') == 'PENDING' or my_assignment.get('status') == 'IN_PROGRESS'):
            # For sequential approval, only show form if it's this approver's turn
            can_approve = True
            if is_sequential:
                assignment_sequence = my_assignment.get('sequence_order', 0)
                can_approve = assignment_sequence <= current_sequence
            
            if can_approve:
                approval_actions = self._create_submit_workflow_actions(my_assignment)
                approval_detail.append(pn.pane.Markdown("## Your Approval"))
                approval_detail.append(approval_actions)
            else:
                # Show waiting message for sequential approval
                waiting_message = pn.pane.Markdown(
                    "### Your approval is scheduled for a later step\n\n"
                    "You will be notified when it's your turn to approve."
                )
                approval_detail.append(waiting_message)
        
        # Add to approval detail area
        self.workflow_detail_area.clear()
        self.workflow_detail_area.append(approval_detail)

    def _create_participants_dataframe(self, approver_assignments):
        """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', assignment.get('assigned_at', '')))
            decision_date = self._format_date(assignment.get('decision_date', ''))
            
            # Add to data
            approvers_data.append({
                'approver_uid': assignment.get('approver_uid'),
                'approver_name': assignment.get('approver_name', assignment.get('user_name', 'Unknown')),
                'role': assignment.get('role', 'Approver'),
                'status': assignment.get('status', 'PENDING'),
                'decision': assignment.get('decision', ''),
                'sequence_order': assignment.get('sequence_order', 0),
                'assigned_date': assigned_date,
                'decision_date': decision_date
            })
        
        # Create DataFrame
        df = pd.DataFrame(approvers_data)
        
        # Sort by sequence order if available
        if 'sequence_order' in df.columns:
            df = df.sort_values('sequence_order')
        
        # Select and rename columns for display
        display_columns = ['approver_name', 'role', 'status', 'decision', 
                         'sequence_order', 'assigned_date', 'decision_date']
        column_names = {
            'approver_name': 'Approver',
            'role': 'Role',
            'status': 'Status',
            'decision': 'Decision',
            'sequence_order': 'Sequence',
            'assigned_date': 'Assigned',
            'decision_date': 'Completed'
        }
        
        # 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_submit_workflow_actions(self, my_assignment):
        """Create the approval actions form"""
        # Create comment input
        comment_input = TextAreaInput(
            name="Comments",
            placeholder="Enter your approval comments...",
            rows=5,
            width=600
        )
        
        # Create decision buttons
        decision_options = {d: d for d in settings.APPROVAL_DECISIONS}
        decision_group = RadioButtonGroup(
            name='Decision',
            options=decision_options,
            button_type='success',
            value='APPROVED'  # Default selection
        )
        
        # 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_workflow_action(
            decision_group.value,
            comment_input.value
        ))
        
        # Create form layout
        approval_actions = Column(
            Row(
                Column(
                    Markdown("### Your Approval Decision"),
                    decision_group,
                    width=400
                ),
                Column(
                    Markdown("### Add Comments (Optional)"),
                    comment_input,
                    width=600
                ),
                align='start'
            ),
            Row(
                submit_btn,
                align='end'
            ),
            styles={'background':'#f8f9fa'},
            css_classes=['p-3', 'border', 'rounded'],
            sizing_mode='stretch_width'
        )
        
        return approval_actions
    
    def _submit_workflow_action(self, decision, comment):
        """Submit an approval decision and comment"""
        try:
            if not self.cycle_uid:
                self.notification_area.object = "Error: No approval cycle selected"
                return
                
            if not decision:
                self.notification_area.object = "Please select a decision"
                return
                
            # Set notification
            self.notification_area.object = f"Submitting approval with decision: {decision}..."
            
            # Call API to complete approval
            result = complete_approval(
                user=self.user,
                approval_uid=self.cycle_uid,
                decision=decision,
                comments=comment
            )
            
            # Handle result
            if result and result.get('success'):
                self.notification_area.object = "Approval submitted successfully!"
                # Reload the approval after short delay
                pn.state.add_timeout_callback(lambda: self._load_cycle(self.cycle_uid), 1500)
            else:
                self.notification_area.object = f"Error submitting approval: {result.get('message', 'Unknown error')}"
                
        except ResourceNotFoundError as e:
            self.notification_area.object = f"**Not Found:** {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 submitting approval: {e}")
            logger.error(traceback.format_exc())
            self.notification_area.object = f"**Error:** An unexpected error occurred when submitting your approval"
    
    def _show_extend_deadline_form(self, approval_uid):
        """Show form to extend approval deadline"""
        try:
            # Get current due date
            approval = self.controller.get_cycle_by_uid(approval_uid)
            if not approval:
                self.notification_area.object = "Error: Could not find approval cycle"
                return
            
            current_due = None
            if 'due_date' in approval:
                current_due = approval.get('due_date')
            elif 'dueDate' in approval:
                current_due = approval.get('dueDate')
            
            # Create form components
            if isinstance(current_due, str):
                try:
                    current_due = datetime.fromisoformat(current_due.replace('Z', '+00:00'))
                except:
                    # Use current date + 7 days as default if parsing fails
                    current_due = datetime.now() + timedelta(days=7)
            elif not current_due:
                current_due = datetime.now() + timedelta(days=7)
                
            # Calculate min date (tomorrow)
            min_date = datetime.now() + timedelta(days=1)
            
            date_picker = DatePicker(
                name="New Due Date",
                value=current_due,
                start=min_date.date()
            )
            
            submit_btn = Button(
                name="Extend Deadline",
                button_type="primary",
                width=150
            )
            
            cancel_btn = Button(
                name="Cancel",
                button_type="default",
                width=150
            )
            
            # Create form layout
            form = pn.Column(
                pn.pane.Markdown("## Extend Approval Deadline"),
                pn.pane.Markdown(f"Current due date: {self._format_date(current_due)}"),
                date_picker,
                pn.Row(
                    submit_btn,
                    cancel_btn,
                    align='end'
                ),
                styles={'background':'#f8f9fa'},
                css_classes=['p-3', 'border', 'rounded'],
                width=400,
                height=250
            )
            
            # Create modal for the form
            form_modal = pn.Column(form)
            
            # Define actions
            def submit_extension(event):
                self._extend_deadline(approval_uid, date_picker.value)
                form_modal.visible = False
                
            def cancel(event):
                form_modal.visible = False
                
            submit_btn.on_click(submit_extension)
            cancel_btn.on_click(cancel)
            
            # Add modal to notification area
            self.notification_area.object = ""
            self.notification_area.append(form_modal)
            
        except Exception as e:
            logger.error(f"Error showing deadline extension form: {e}")
            self.notification_area.object = f"**Error:** {str(e)}"
    
    def _extend_deadline(self, approval_uid, new_due_date):
        """Extend the approval deadline"""
        try:
            self.notification_area.object = "Extending approval deadline..."
            
            # Call controller to extend deadline
            from CDocs.controllers.approval_controller import extend_approval_deadline
            result = extend_approval_deadline(
                user=self.user,
                approval_uid=approval_uid,
                new_due_date=new_due_date
            )
            
            if result['success']:
                self.notification_area.object = "Approval deadline extended successfully."
                # Reload the approval
                self._load_cycle(approval_uid)
            else:
                self.notification_area.object = f"Error extending deadline: {result.get('message', 'Unknown error')}"
                
        except ResourceNotFoundError as e:
            self.notification_area.object = f"**Not Found:** {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 extending approval deadline: {e}")
            logger.error(traceback.format_exc())
            self.notification_area.object = f"**Error:** An unexpected error occurred"
    
    def _cancel_approval_cycle(self, approval_uid, reason=None):
        """Cancel an approval cycle"""
        try:
            # First show confirmation dialog with reason input
            if reason is None:
                self._show_cancel_confirmation(approval_uid)
                return
                
            self.notification_area.object = "Canceling approval cycle..."
            
            # Call controller to cancel approval cycle
            from CDocs.controllers.approval_controller import cancel_approval_cycle
            result = cancel_approval_cycle(
                user=self.user,
                approval_uid=approval_uid,
                reason=reason
            )
            
            if result['success']:
                self.notification_area.object = "Approval cycle canceled successfully."
                # Reload the approval
                self._load_cycle(approval_uid)
            else:
                self.notification_area.object = f"Error canceling approval cycle: {result.get('message', 'Unknown error')}"
                
        except ResourceNotFoundError as e:
            self.notification_area.object = f"**Not Found:** {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 canceling approval cycle: {e}")
            logger.error(traceback.format_exc())
            self.notification_area.object = f"**Error:** An unexpected error occurred"
    
    def _show_cancel_confirmation(self, approval_uid):
        """Show confirmation dialog for canceling approval"""
        try:
            # Create form components
            reason_input = TextAreaInput(
                name="Reason for Cancellation",
                placeholder="Please provide a reason for canceling this approval cycle...",
                rows=3,
                width=350
            )
            
            confirm_btn = Button(
                name="Confirm Cancel",
                button_type="danger",
                width=150
            )
            
            cancel_btn = Button(
                name="Back",
                button_type="default",
                width=150
            )
            
            # Create form layout
            form = pn.Column(
                pn.pane.Markdown("## Cancel Approval Cycle"),
                pn.pane.Markdown("Are you sure you want to cancel this approval cycle? This action cannot be undone."),
                reason_input,
                pn.Row(
                    confirm_btn,
                    cancel_btn,
                    align='end'
                ),
                styles={'background':'#f8f9fa'},
                css_classes=['p-3', 'border', 'rounded'],
                width=400,
                height=250
            )
            
            # Create modal for the form
            form_modal = pn.Column(form)
            
            # Define actions
            def confirm(event):
                self._cancel_approval_cycle(approval_uid, reason_input.value)
                form_modal.visible = False
                
            def cancel(event):
                form_modal.visible = False
                
            confirm_btn.on_click(confirm)
            cancel_btn.on_click(cancel)
            
            # Add modal to notification area
            self.notification_area.object = ""
            self.notification_area.append(form_modal)
            
        except Exception as e:
            logger.error(f"Error showing cancel confirmation dialog: {e}")
            self.notification_area.object = f"**Error:** {str(e)}"

Parameters

Name Type Default Kind
bases WorkflowPanelBase -

Parameter Details

bases: Parameter of type WorkflowPanelBase

Return Value

Returns unspecified type

Class Interface

Methods

__init__(self, template, session_manager, parent_app, embedded, controller)

Purpose: Initialize the approval panel. Args: template: Panel template for displaying the UI session_manager: Authentication session manager parent_app: Parent application reference for navigation embedded: Whether this panel is embedded in another UI controller: Approval controller instance

Parameters:

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

Returns: None

_create_workflow_detail_view(self)

Purpose: Create the approval detail view

Returns: None

_create_participants_dataframe(self, approver_assignments)

Purpose: Create a DataFrame for the approvers table

Parameters:

  • approver_assignments: Parameter

Returns: None

_create_submit_workflow_actions(self, my_assignment)

Purpose: Create the approval actions form

Parameters:

  • my_assignment: Parameter

Returns: None

_submit_workflow_action(self, decision, comment)

Purpose: Submit an approval decision and comment

Parameters:

  • decision: Parameter
  • comment: Parameter

Returns: None

_show_extend_deadline_form(self, approval_uid)

Purpose: Show form to extend approval deadline

Parameters:

  • approval_uid: Parameter

Returns: None

_extend_deadline(self, approval_uid, new_due_date)

Purpose: Extend the approval deadline

Parameters:

  • approval_uid: Parameter
  • new_due_date: Parameter

Returns: None

_cancel_approval_cycle(self, approval_uid, reason)

Purpose: Cancel an approval cycle

Parameters:

  • approval_uid: Parameter
  • reason: Parameter

Returns: None

_show_cancel_confirmation(self, approval_uid)

Purpose: Show confirmation dialog for canceling approval

Parameters:

  • approval_uid: Parameter

Returns: None

Required Imports

import logging
import traceback
from typing import Dict
from typing import List
from typing import Any

Usage Example

# Example usage:
# result = ApprovalPanel(bases)

Similar Components

AI-powered semantic similarity - components with related functionality:

  • class ApprovalPanel_v1 87.3% similar

    Approval management interface component

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

    Approval management interface component

    From: /tf/active/vicechatdev/CDocs/ui/approval_panel.py
  • class WorkflowPanelBase 79.7% similar

    Base class for workflow panels (review and approval) that provides common functionality and UI components.

    From: /tf/active/vicechatdev/CDocs single class/ui/workflow_panel_base.py
  • class ReviewPanel 73.3% similar

    ReviewPanel is a UI component class for managing document review workflows, providing interfaces for viewing review details, submitting review decisions, and managing review cycles.

    From: /tf/active/vicechatdev/CDocs single class/ui/review_panel.py
  • function create_approval_panel_v2 69.6% 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
← Back to Browse