class ApprovalPanel_v1
Approval management interface component
/tf/active/vicechatdev/CDocs/ui/approval_panel.py
62 - 1956
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: Parametersession_manager: Parameterparent_app: Parameterembedded: 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: Parameterdate_from: Parameterdate_to: Parameterapproval_type: Parameterdoc_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: Parameterupdate_status: Parametertarget_status: Parameter
Returns: None
_create_approvers_dataframe(self, approver_assignments, include_instructions)
Purpose: Create a DataFrame for the approvers table
Parameters:
approver_assignments: Parameterinclude_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: Parametercomment: Parametersection: 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)
Tags
Similar Components
AI-powered semantic similarity - components with related functionality:
-
class ApprovalPanel 98.0% similar
-
class ReviewPanel 74.2% similar
-
function create_approval_panel_v1 72.2% similar
-
function create_approval_panel 70.0% similar
-
class AdminPanel 68.5% similar