Add a customizable dashboard feature.
This commit is contained in:
663
app.py
663
app.py
@@ -1,5 +1,5 @@
|
||||
from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, session, g, Response, send_file
|
||||
from models import db, TimeEntry, WorkConfig, User, SystemSettings, Team, Role, Project, Company, CompanyWorkConfig, UserPreferences, WorkRegion, AccountType, ProjectCategory, Task, SubTask, TaskStatus, TaskPriority, Announcement, SystemEvent, KanbanBoard, KanbanColumn, KanbanCard
|
||||
from models import db, TimeEntry, WorkConfig, User, SystemSettings, Team, Role, Project, Company, CompanyWorkConfig, UserPreferences, WorkRegion, AccountType, ProjectCategory, Task, SubTask, TaskStatus, TaskPriority, Announcement, SystemEvent, KanbanBoard, KanbanColumn, KanbanCard, WidgetType, UserDashboard, DashboardWidget, WidgetTemplate
|
||||
from data_formatting import (
|
||||
format_duration, prepare_export_data, prepare_team_hours_export_data,
|
||||
format_table_data, format_graph_data, format_team_data
|
||||
@@ -879,62 +879,11 @@ def verify_email(token):
|
||||
return redirect(url_for('login'))
|
||||
|
||||
@app.route('/dashboard')
|
||||
@role_required(Role.TEAM_LEADER)
|
||||
@role_required(Role.TEAM_MEMBER)
|
||||
@company_required
|
||||
def dashboard():
|
||||
# Get dashboard data based on user role
|
||||
dashboard_data = {}
|
||||
|
||||
if g.user.role == Role.ADMIN and g.user.company_id:
|
||||
# Admin sees everything within their company
|
||||
|
||||
dashboard_data.update({
|
||||
'total_users': User.query.filter_by(company_id=g.user.company_id).count(),
|
||||
'total_teams': Team.query.filter_by(company_id=g.user.company_id).count(),
|
||||
'blocked_users': User.query.filter_by(company_id=g.user.company_id, is_blocked=True).count(),
|
||||
'unverified_users': User.query.filter_by(company_id=g.user.company_id, is_verified=False).count(),
|
||||
'recent_registrations': User.query.filter_by(company_id=g.user.company_id).order_by(User.id.desc()).limit(5).all()
|
||||
})
|
||||
|
||||
if g.user.role in [Role.TEAM_LEADER, Role.SUPERVISOR, Role.ADMIN]:
|
||||
|
||||
# Team leaders and supervisors see team-related data
|
||||
if g.user.team_id or g.user.role == Role.ADMIN:
|
||||
if g.user.role == Role.ADMIN and g.user.company_id:
|
||||
# Admin can see all teams in their company
|
||||
teams = Team.query.filter_by(company_id=g.user.company_id).all()
|
||||
team_members = User.query.filter(
|
||||
User.team_id.isnot(None),
|
||||
User.company_id == g.user.company_id
|
||||
).all()
|
||||
else:
|
||||
# Team leaders/supervisors see their own team
|
||||
teams = [Team.query.get(g.user.team_id)] if g.user.team_id else []
|
||||
|
||||
team_members = User.query.filter_by(
|
||||
team_id=g.user.team_id,
|
||||
company_id=g.user.company_id
|
||||
).all() if g.user.team_id else []
|
||||
|
||||
dashboard_data.update({
|
||||
'teams': teams,
|
||||
'team_members': team_members,
|
||||
'team_member_count': len(team_members)
|
||||
})
|
||||
|
||||
# Get recent time entries for the user's oversight
|
||||
if g.user.role == Role.ADMIN:
|
||||
# Admin sees all recent entries
|
||||
recent_entries = TimeEntry.query.order_by(TimeEntry.arrival_time.desc()).limit(10).all()
|
||||
elif g.user.team_id:
|
||||
# Team leaders see their team's entries
|
||||
team_user_ids = [user.id for user in User.query.filter_by(team_id=g.user.team_id).all()]
|
||||
recent_entries = TimeEntry.query.filter(TimeEntry.user_id.in_(team_user_ids)).order_by(TimeEntry.arrival_time.desc()).limit(10).all()
|
||||
else:
|
||||
recent_entries = []
|
||||
|
||||
dashboard_data['recent_entries'] = recent_entries
|
||||
|
||||
return render_template('dashboard.html', title='Dashboard', **dashboard_data)
|
||||
"""User dashboard with configurable widgets."""
|
||||
return render_template('dashboard.html', title='Dashboard')
|
||||
|
||||
# Redirect old admin dashboard URL to new dashboard
|
||||
|
||||
@@ -4467,6 +4416,606 @@ def delete_kanban_column(column_id):
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'message': str(e)})
|
||||
|
||||
# Dashboard API Endpoints
|
||||
@app.route('/api/dashboard')
|
||||
@role_required(Role.TEAM_MEMBER)
|
||||
@company_required
|
||||
def get_dashboard():
|
||||
"""Get user's dashboard configuration and widgets."""
|
||||
try:
|
||||
# Get or create user dashboard
|
||||
dashboard = UserDashboard.query.filter_by(user_id=g.user.id).first()
|
||||
if not dashboard:
|
||||
dashboard = UserDashboard(user_id=g.user.id)
|
||||
db.session.add(dashboard)
|
||||
db.session.commit()
|
||||
logger.info(f"Created new dashboard {dashboard.id} for user {g.user.id}")
|
||||
else:
|
||||
logger.info(f"Using existing dashboard {dashboard.id} for user {g.user.id}")
|
||||
|
||||
# Get user's widgets
|
||||
widgets = DashboardWidget.query.filter_by(dashboard_id=dashboard.id).order_by(DashboardWidget.grid_y, DashboardWidget.grid_x).all()
|
||||
|
||||
logger.info(f"Found {len(widgets)} widgets for dashboard {dashboard.id}")
|
||||
|
||||
# Convert to JSON format
|
||||
widget_data = []
|
||||
for widget in widgets:
|
||||
# Convert grid size to simple size names
|
||||
if widget.grid_width == 1 and widget.grid_height == 1:
|
||||
size = 'small'
|
||||
elif widget.grid_width == 2 and widget.grid_height == 1:
|
||||
size = 'medium'
|
||||
elif widget.grid_width == 2 and widget.grid_height == 2:
|
||||
size = 'large'
|
||||
elif widget.grid_width == 3 and widget.grid_height == 1:
|
||||
size = 'wide'
|
||||
else:
|
||||
size = 'small'
|
||||
|
||||
# Parse config JSON
|
||||
try:
|
||||
import json
|
||||
config = json.loads(widget.config) if widget.config else {}
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
config = {}
|
||||
|
||||
widget_data.append({
|
||||
'id': widget.id,
|
||||
'type': widget.widget_type.value,
|
||||
'title': widget.title,
|
||||
'size': size,
|
||||
'grid_x': widget.grid_x,
|
||||
'grid_y': widget.grid_y,
|
||||
'grid_width': widget.grid_width,
|
||||
'grid_height': widget.grid_height,
|
||||
'config': config
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'dashboard': {
|
||||
'id': dashboard.id,
|
||||
'layout_config': dashboard.layout_config,
|
||||
'grid_columns': dashboard.grid_columns,
|
||||
'theme': dashboard.theme
|
||||
},
|
||||
'widgets': widget_data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading dashboard: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
@app.route('/api/dashboard/widgets', methods=['POST'])
|
||||
@role_required(Role.TEAM_MEMBER)
|
||||
@company_required
|
||||
def create_or_update_widget():
|
||||
"""Create or update a dashboard widget."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
|
||||
# Get or create user dashboard
|
||||
dashboard = UserDashboard.query.filter_by(user_id=g.user.id).first()
|
||||
if not dashboard:
|
||||
dashboard = UserDashboard(user_id=g.user.id)
|
||||
db.session.add(dashboard)
|
||||
db.session.flush() # Get the ID
|
||||
logger.info(f"Created new dashboard {dashboard.id} for user {g.user.id} in widget creation")
|
||||
else:
|
||||
logger.info(f"Using existing dashboard {dashboard.id} for user {g.user.id} in widget creation")
|
||||
|
||||
# Check if updating existing widget
|
||||
widget_id = data.get('widget_id')
|
||||
if widget_id:
|
||||
widget = DashboardWidget.query.filter_by(
|
||||
id=widget_id,
|
||||
dashboard_id=dashboard.id
|
||||
).first()
|
||||
if not widget:
|
||||
return jsonify({'success': False, 'error': 'Widget not found'})
|
||||
else:
|
||||
# Create new widget
|
||||
widget = DashboardWidget(dashboard_id=dashboard.id)
|
||||
# Find next available position
|
||||
max_y = db.session.query(func.max(DashboardWidget.grid_y)).filter_by(
|
||||
dashboard_id=dashboard.id
|
||||
).scalar() or 0
|
||||
widget.grid_y = max_y + 1
|
||||
widget.grid_x = 0
|
||||
|
||||
# Update widget properties
|
||||
widget.widget_type = WidgetType(data['type'])
|
||||
widget.title = data['title']
|
||||
|
||||
# Convert size to grid dimensions
|
||||
size = data.get('size', 'small')
|
||||
if size == 'small':
|
||||
widget.grid_width = 1
|
||||
widget.grid_height = 1
|
||||
elif size == 'medium':
|
||||
widget.grid_width = 2
|
||||
widget.grid_height = 1
|
||||
elif size == 'large':
|
||||
widget.grid_width = 2
|
||||
widget.grid_height = 2
|
||||
elif size == 'wide':
|
||||
widget.grid_width = 3
|
||||
widget.grid_height = 1
|
||||
|
||||
# Build config from form data
|
||||
config = {}
|
||||
for key, value in data.items():
|
||||
if key not in ['type', 'title', 'size', 'widget_id']:
|
||||
config[key] = value
|
||||
|
||||
# Store config as JSON string
|
||||
if config:
|
||||
import json
|
||||
widget.config = json.dumps(config)
|
||||
else:
|
||||
widget.config = None
|
||||
|
||||
if not widget_id:
|
||||
db.session.add(widget)
|
||||
logger.info(f"Creating new widget: {widget.widget_type.value} for dashboard {dashboard.id}")
|
||||
else:
|
||||
logger.info(f"Updating existing widget {widget_id}")
|
||||
|
||||
db.session.commit()
|
||||
logger.info(f"Widget saved successfully with ID: {widget.id}")
|
||||
|
||||
# Verify the widget was actually saved
|
||||
saved_widget = DashboardWidget.query.filter_by(id=widget.id).first()
|
||||
if saved_widget:
|
||||
logger.info(f"Verification: Widget {widget.id} exists in database with dashboard_id: {saved_widget.dashboard_id}")
|
||||
else:
|
||||
logger.error(f"Verification failed: Widget {widget.id} not found in database")
|
||||
|
||||
# Convert grid size back to simple size name for response
|
||||
if widget.grid_width == 1 and widget.grid_height == 1:
|
||||
size_name = 'small'
|
||||
elif widget.grid_width == 2 and widget.grid_height == 1:
|
||||
size_name = 'medium'
|
||||
elif widget.grid_width == 2 and widget.grid_height == 2:
|
||||
size_name = 'large'
|
||||
elif widget.grid_width == 3 and widget.grid_height == 1:
|
||||
size_name = 'wide'
|
||||
else:
|
||||
size_name = 'small'
|
||||
|
||||
# Parse config for response
|
||||
try:
|
||||
import json
|
||||
config_dict = json.loads(widget.config) if widget.config else {}
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
config_dict = {}
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Widget saved successfully',
|
||||
'widget': {
|
||||
'id': widget.id,
|
||||
'type': widget.widget_type.value,
|
||||
'title': widget.title,
|
||||
'size': size_name,
|
||||
'grid_x': widget.grid_x,
|
||||
'grid_y': widget.grid_y,
|
||||
'grid_width': widget.grid_width,
|
||||
'grid_height': widget.grid_height,
|
||||
'config': config_dict
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Error saving widget: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
@app.route('/api/dashboard/widgets/<int:widget_id>', methods=['DELETE'])
|
||||
@role_required(Role.TEAM_MEMBER)
|
||||
@company_required
|
||||
def delete_widget(widget_id):
|
||||
"""Delete a dashboard widget."""
|
||||
try:
|
||||
# Get user dashboard
|
||||
dashboard = UserDashboard.query.filter_by(user_id=g.user.id).first()
|
||||
if not dashboard:
|
||||
return jsonify({'success': False, 'error': 'Dashboard not found'})
|
||||
|
||||
# Find and delete widget
|
||||
widget = DashboardWidget.query.filter_by(
|
||||
id=widget_id,
|
||||
dashboard_id=dashboard.id
|
||||
).first()
|
||||
|
||||
if not widget:
|
||||
return jsonify({'success': False, 'error': 'Widget not found'})
|
||||
|
||||
# No need to update positions for grid-based layout
|
||||
|
||||
db.session.delete(widget)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True, 'message': 'Widget deleted successfully'})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Error deleting widget: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
@app.route('/api/dashboard/positions', methods=['POST'])
|
||||
@role_required(Role.TEAM_MEMBER)
|
||||
@company_required
|
||||
def update_widget_positions():
|
||||
"""Update widget grid positions after drag and drop."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
positions = data.get('positions', [])
|
||||
|
||||
# Get user dashboard
|
||||
dashboard = UserDashboard.query.filter_by(user_id=g.user.id).first()
|
||||
if not dashboard:
|
||||
return jsonify({'success': False, 'error': 'Dashboard not found'})
|
||||
|
||||
# Update grid positions
|
||||
for pos_data in positions:
|
||||
widget = DashboardWidget.query.filter_by(
|
||||
id=pos_data['id'],
|
||||
dashboard_id=dashboard.id
|
||||
).first()
|
||||
if widget:
|
||||
# For now, just assign sequential grid positions
|
||||
# In a more advanced implementation, we'd calculate actual grid coordinates
|
||||
widget.grid_x = pos_data.get('grid_x', 0)
|
||||
widget.grid_y = pos_data.get('grid_y', pos_data.get('position', 0))
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True, 'message': 'Positions updated successfully'})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Error updating positions: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
# Widget data endpoints
|
||||
@app.route('/api/dashboard/widgets/<int:widget_id>/data')
|
||||
@role_required(Role.TEAM_MEMBER)
|
||||
@company_required
|
||||
def get_widget_data(widget_id):
|
||||
"""Get data for a specific widget."""
|
||||
try:
|
||||
# Get user dashboard
|
||||
dashboard = UserDashboard.query.filter_by(user_id=g.user.id).first()
|
||||
if not dashboard:
|
||||
return jsonify({'success': False, 'error': 'Dashboard not found'})
|
||||
|
||||
# Find widget
|
||||
widget = DashboardWidget.query.filter_by(
|
||||
id=widget_id,
|
||||
dashboard_id=dashboard.id
|
||||
).first()
|
||||
|
||||
if not widget:
|
||||
return jsonify({'success': False, 'error': 'Widget not found'})
|
||||
|
||||
# Get widget-specific data based on type
|
||||
widget_data = {}
|
||||
|
||||
if widget.widget_type == WidgetType.DAILY_SUMMARY:
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
config = widget.config or {}
|
||||
period = config.get('summary_period', 'daily')
|
||||
|
||||
# Calculate time summaries
|
||||
now = datetime.now()
|
||||
|
||||
# Today's summary
|
||||
start_of_today = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
today_entries = TimeEntry.query.filter(
|
||||
TimeEntry.user_id == g.user.id,
|
||||
TimeEntry.arrival_time >= start_of_today,
|
||||
TimeEntry.departure_time.isnot(None)
|
||||
).all()
|
||||
today_seconds = sum(entry.duration or 0 for entry in today_entries)
|
||||
|
||||
# This week's summary
|
||||
start_of_week = start_of_today - timedelta(days=start_of_today.weekday())
|
||||
week_entries = TimeEntry.query.filter(
|
||||
TimeEntry.user_id == g.user.id,
|
||||
TimeEntry.arrival_time >= start_of_week,
|
||||
TimeEntry.departure_time.isnot(None)
|
||||
).all()
|
||||
week_seconds = sum(entry.duration or 0 for entry in week_entries)
|
||||
|
||||
# This month's summary
|
||||
start_of_month = start_of_today.replace(day=1)
|
||||
month_entries = TimeEntry.query.filter(
|
||||
TimeEntry.user_id == g.user.id,
|
||||
TimeEntry.arrival_time >= start_of_month,
|
||||
TimeEntry.departure_time.isnot(None)
|
||||
).all()
|
||||
month_seconds = sum(entry.duration or 0 for entry in month_entries)
|
||||
|
||||
widget_data.update({
|
||||
'today': f"{today_seconds // 3600}h {(today_seconds % 3600) // 60}m",
|
||||
'week': f"{week_seconds // 3600}h {(week_seconds % 3600) // 60}m",
|
||||
'month': f"{month_seconds // 3600}h {(month_seconds % 3600) // 60}m",
|
||||
'entries_today': len(today_entries),
|
||||
'entries_week': len(week_entries),
|
||||
'entries_month': len(month_entries)
|
||||
})
|
||||
|
||||
elif widget.widget_type == WidgetType.ACTIVE_PROJECTS:
|
||||
config = widget.config or {}
|
||||
project_filter = config.get('project_filter', 'all')
|
||||
max_projects = int(config.get('max_projects', 5))
|
||||
|
||||
# Get user's projects
|
||||
if g.user.role in [Role.ADMIN, Role.SUPERVISOR]:
|
||||
projects = Project.query.filter_by(
|
||||
company_id=g.user.company_id,
|
||||
is_active=True
|
||||
).limit(max_projects).all()
|
||||
elif g.user.team_id:
|
||||
projects = Project.query.filter(
|
||||
Project.company_id == g.user.company_id,
|
||||
Project.is_active == True,
|
||||
db.or_(Project.team_id == g.user.team_id, Project.team_id == None)
|
||||
).limit(max_projects).all()
|
||||
else:
|
||||
projects = []
|
||||
|
||||
widget_data['projects'] = [{
|
||||
'id': p.id,
|
||||
'name': p.name,
|
||||
'code': p.code,
|
||||
'description': p.description
|
||||
} for p in projects]
|
||||
|
||||
elif widget.widget_type == WidgetType.ASSIGNED_TASKS:
|
||||
config = widget.config or {}
|
||||
task_filter = config.get('task_filter', 'assigned')
|
||||
task_status = config.get('task_status', 'active')
|
||||
|
||||
# Get user's tasks based on filter
|
||||
if task_filter == 'assigned':
|
||||
tasks = Task.query.filter_by(assigned_to_id=g.user.id)
|
||||
elif task_filter == 'created':
|
||||
tasks = Task.query.filter_by(created_by_id=g.user.id)
|
||||
else:
|
||||
# Get tasks from user's projects
|
||||
if g.user.team_id:
|
||||
project_ids = [p.id for p in Project.query.filter(
|
||||
Project.company_id == g.user.company_id,
|
||||
db.or_(Project.team_id == g.user.team_id, Project.team_id == None)
|
||||
).all()]
|
||||
tasks = Task.query.filter(Task.project_id.in_(project_ids))
|
||||
else:
|
||||
tasks = Task.query.join(Project).filter(Project.company_id == g.user.company_id)
|
||||
|
||||
# Filter by status if specified
|
||||
if task_status != 'all':
|
||||
if task_status == 'active':
|
||||
tasks = tasks.filter(Task.status.in_([TaskStatus.PENDING, TaskStatus.IN_PROGRESS]))
|
||||
elif task_status == 'pending':
|
||||
tasks = tasks.filter_by(status=TaskStatus.PENDING)
|
||||
elif task_status == 'completed':
|
||||
tasks = tasks.filter_by(status=TaskStatus.COMPLETED)
|
||||
|
||||
tasks = tasks.limit(10).all()
|
||||
|
||||
widget_data['tasks'] = [{
|
||||
'id': t.id,
|
||||
'name': t.name,
|
||||
'description': t.description,
|
||||
'status': t.status.value if t.status else 'Pending',
|
||||
'priority': t.priority.value if t.priority else 'Medium',
|
||||
'project_name': t.project.name if t.project else 'No Project'
|
||||
} for t in tasks]
|
||||
|
||||
elif widget.widget_type == WidgetType.WEEKLY_CHART:
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Get weekly data for chart
|
||||
now = datetime.now()
|
||||
start_of_week = now - timedelta(days=now.weekday())
|
||||
|
||||
weekly_data = []
|
||||
for i in range(7):
|
||||
day = start_of_week + timedelta(days=i)
|
||||
day_start = day.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
day_end = day_start + timedelta(days=1)
|
||||
|
||||
day_entries = TimeEntry.query.filter(
|
||||
TimeEntry.user_id == g.user.id,
|
||||
TimeEntry.arrival_time >= day_start,
|
||||
TimeEntry.arrival_time < day_end,
|
||||
TimeEntry.departure_time.isnot(None)
|
||||
).all()
|
||||
|
||||
total_seconds = sum(entry.duration or 0 for entry in day_entries)
|
||||
weekly_data.append({
|
||||
'day': day.strftime('%A'),
|
||||
'date': day.strftime('%Y-%m-%d'),
|
||||
'hours': round(total_seconds / 3600, 2),
|
||||
'entries': len(day_entries)
|
||||
})
|
||||
|
||||
widget_data['weekly_data'] = weekly_data
|
||||
|
||||
elif widget.widget_type == WidgetType.TASK_PRIORITY:
|
||||
# Get tasks by priority
|
||||
if g.user.team_id:
|
||||
project_ids = [p.id for p in Project.query.filter(
|
||||
Project.company_id == g.user.company_id,
|
||||
db.or_(Project.team_id == g.user.team_id, Project.team_id == None)
|
||||
).all()]
|
||||
tasks = Task.query.filter(
|
||||
Task.project_id.in_(project_ids),
|
||||
Task.assigned_to_id == g.user.id
|
||||
).order_by(Task.priority.desc(), Task.created_at.desc()).limit(10).all()
|
||||
else:
|
||||
tasks = Task.query.filter_by(assigned_to_id=g.user.id).order_by(
|
||||
Task.priority.desc(), Task.created_at.desc()
|
||||
).limit(10).all()
|
||||
|
||||
widget_data['priority_tasks'] = [{
|
||||
'id': t.id,
|
||||
'name': t.name,
|
||||
'description': t.description,
|
||||
'priority': t.priority.value if t.priority else 'Medium',
|
||||
'status': t.status.value if t.status else 'Pending',
|
||||
'project_name': t.project.name if t.project else 'No Project'
|
||||
} for t in tasks]
|
||||
|
||||
elif widget.widget_type == WidgetType.KANBAN_SUMMARY:
|
||||
# Get kanban data summary
|
||||
if g.user.team_id:
|
||||
project_ids = [p.id for p in Project.query.filter(
|
||||
Project.company_id == g.user.company_id,
|
||||
db.or_(Project.team_id == g.user.team_id, Project.team_id == None)
|
||||
).all()]
|
||||
boards = KanbanBoard.query.filter(
|
||||
KanbanBoard.project_id.in_(project_ids),
|
||||
KanbanBoard.is_active == True
|
||||
).limit(3).all()
|
||||
else:
|
||||
boards = []
|
||||
|
||||
board_summaries = []
|
||||
for board in boards:
|
||||
columns = KanbanColumn.query.filter_by(board_id=board.id).order_by(KanbanColumn.position).all()
|
||||
total_cards = sum(len([c for c in col.cards if c.is_active]) for col in columns)
|
||||
|
||||
board_summaries.append({
|
||||
'id': board.id,
|
||||
'name': board.name,
|
||||
'project_name': board.project.name,
|
||||
'total_cards': total_cards,
|
||||
'columns': len(columns)
|
||||
})
|
||||
|
||||
widget_data['kanban_boards'] = board_summaries
|
||||
|
||||
elif widget.widget_type == WidgetType.PROJECT_PROGRESS:
|
||||
# Get project progress data
|
||||
if g.user.role in [Role.ADMIN, Role.SUPERVISOR]:
|
||||
projects = Project.query.filter_by(
|
||||
company_id=g.user.company_id,
|
||||
is_active=True
|
||||
).limit(5).all()
|
||||
elif g.user.team_id:
|
||||
projects = Project.query.filter(
|
||||
Project.company_id == g.user.company_id,
|
||||
Project.is_active == True,
|
||||
db.or_(Project.team_id == g.user.team_id, Project.team_id == None)
|
||||
).limit(5).all()
|
||||
else:
|
||||
projects = []
|
||||
|
||||
project_progress = []
|
||||
for project in projects:
|
||||
total_tasks = Task.query.filter_by(project_id=project.id).count()
|
||||
completed_tasks = Task.query.filter_by(
|
||||
project_id=project.id,
|
||||
status=TaskStatus.COMPLETED
|
||||
).count()
|
||||
|
||||
progress = (completed_tasks / total_tasks * 100) if total_tasks > 0 else 0
|
||||
|
||||
project_progress.append({
|
||||
'id': project.id,
|
||||
'name': project.name,
|
||||
'code': project.code,
|
||||
'progress': round(progress, 1),
|
||||
'completed_tasks': completed_tasks,
|
||||
'total_tasks': total_tasks
|
||||
})
|
||||
|
||||
widget_data['project_progress'] = project_progress
|
||||
|
||||
elif widget.widget_type == WidgetType.PRODUCTIVITY_METRICS:
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Calculate productivity metrics
|
||||
now = datetime.now()
|
||||
today = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
week_ago = today - timedelta(days=7)
|
||||
|
||||
# This week vs last week comparison
|
||||
this_week_entries = TimeEntry.query.filter(
|
||||
TimeEntry.user_id == g.user.id,
|
||||
TimeEntry.arrival_time >= week_ago,
|
||||
TimeEntry.departure_time.isnot(None)
|
||||
).all()
|
||||
|
||||
last_week_entries = TimeEntry.query.filter(
|
||||
TimeEntry.user_id == g.user.id,
|
||||
TimeEntry.arrival_time >= week_ago - timedelta(days=7),
|
||||
TimeEntry.arrival_time < week_ago,
|
||||
TimeEntry.departure_time.isnot(None)
|
||||
).all()
|
||||
|
||||
this_week_hours = sum(entry.duration or 0 for entry in this_week_entries) / 3600
|
||||
last_week_hours = sum(entry.duration or 0 for entry in last_week_entries) / 3600
|
||||
|
||||
productivity_change = ((this_week_hours - last_week_hours) / last_week_hours * 100) if last_week_hours > 0 else 0
|
||||
|
||||
widget_data.update({
|
||||
'this_week_hours': round(this_week_hours, 1),
|
||||
'last_week_hours': round(last_week_hours, 1),
|
||||
'productivity_change': round(productivity_change, 1),
|
||||
'avg_daily_hours': round(this_week_hours / 7, 1),
|
||||
'total_entries': len(this_week_entries)
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': widget_data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting widget data: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
@app.route('/api/current-timer-status')
|
||||
@role_required(Role.TEAM_MEMBER)
|
||||
@company_required
|
||||
def get_current_timer_status():
|
||||
"""Get current timer status for dashboard widgets."""
|
||||
try:
|
||||
# Get the user's current active time entry
|
||||
active_entry = TimeEntry.query.filter_by(
|
||||
user_id=g.user.id,
|
||||
departure_time=None
|
||||
).first()
|
||||
|
||||
if active_entry:
|
||||
# Calculate current duration
|
||||
now = datetime.now()
|
||||
elapsed_seconds = int((now - active_entry.arrival_time).total_seconds())
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'isActive': True,
|
||||
'startTime': active_entry.arrival_time.isoformat(),
|
||||
'currentDuration': elapsed_seconds,
|
||||
'entryId': active_entry.id
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'isActive': False,
|
||||
'message': 'No active timer'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting timer status: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
if __name__ == '__main__':
|
||||
port = int(os.environ.get('PORT', 5000))
|
||||
app.run(debug=False, host='0.0.0.0', port=port)
|
||||
app.run(debug=True, host='0.0.0.0', port=port)
|
||||
152
migrate_db.py
152
migrate_db.py
@@ -16,7 +16,8 @@ try:
|
||||
from models import (User, TimeEntry, WorkConfig, SystemSettings, Team, Role, Project,
|
||||
Company, CompanyWorkConfig, UserPreferences, WorkRegion, AccountType,
|
||||
ProjectCategory, Task, SubTask, TaskStatus, TaskPriority, Announcement, SystemEvent, KanbanBoard,
|
||||
KanbanColumn, KanbanCard)
|
||||
KanbanColumn, KanbanCard, WidgetType, UserDashboard, DashboardWidget,
|
||||
WidgetTemplate)
|
||||
from werkzeug.security import generate_password_hash
|
||||
FLASK_AVAILABLE = True
|
||||
except ImportError:
|
||||
@@ -74,6 +75,7 @@ def run_all_migrations(db_path=None):
|
||||
migrate_task_system(db_path)
|
||||
migrate_system_events(db_path)
|
||||
migrate_kanban_system(db_path)
|
||||
migrate_dashboard_system(db_path)
|
||||
|
||||
if FLASK_AVAILABLE:
|
||||
with app.app_context():
|
||||
@@ -985,6 +987,149 @@ def migrate_kanban_system(db_file=None):
|
||||
conn.close()
|
||||
|
||||
|
||||
def migrate_dashboard_system(db_file=None):
|
||||
"""Migrate to add Dashboard widget system."""
|
||||
db_path = get_db_path(db_file)
|
||||
|
||||
print(f"Migrating Dashboard system in {db_path}...")
|
||||
|
||||
if not os.path.exists(db_path):
|
||||
print(f"Database file {db_path} does not exist. Run basic migration first.")
|
||||
return False
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# Check if user_dashboard table already exists
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='user_dashboard'")
|
||||
if cursor.fetchone():
|
||||
print("Dashboard tables already exist. Skipping migration.")
|
||||
return True
|
||||
|
||||
print("Creating Dashboard system tables...")
|
||||
|
||||
# Create user_dashboard table
|
||||
cursor.execute("""
|
||||
CREATE TABLE user_dashboard (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
name VARCHAR(100) DEFAULT 'My Dashboard',
|
||||
is_default BOOLEAN DEFAULT 1,
|
||||
layout_config TEXT,
|
||||
grid_columns INTEGER DEFAULT 6,
|
||||
theme VARCHAR(20) DEFAULT 'light',
|
||||
auto_refresh INTEGER DEFAULT 300,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES user (id)
|
||||
)
|
||||
""")
|
||||
|
||||
# Create dashboard_widget table
|
||||
cursor.execute("""
|
||||
CREATE TABLE dashboard_widget (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
dashboard_id INTEGER NOT NULL,
|
||||
widget_type VARCHAR(50) NOT NULL,
|
||||
grid_x INTEGER NOT NULL DEFAULT 0,
|
||||
grid_y INTEGER NOT NULL DEFAULT 0,
|
||||
grid_width INTEGER NOT NULL DEFAULT 1,
|
||||
grid_height INTEGER NOT NULL DEFAULT 1,
|
||||
title VARCHAR(100),
|
||||
config TEXT,
|
||||
refresh_interval INTEGER DEFAULT 60,
|
||||
is_visible BOOLEAN DEFAULT 1,
|
||||
is_minimized BOOLEAN DEFAULT 0,
|
||||
z_index INTEGER DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (dashboard_id) REFERENCES user_dashboard (id)
|
||||
)
|
||||
""")
|
||||
|
||||
# Create widget_template table
|
||||
cursor.execute("""
|
||||
CREATE TABLE widget_template (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
widget_type VARCHAR(50) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
icon VARCHAR(50),
|
||||
default_width INTEGER DEFAULT 1,
|
||||
default_height INTEGER DEFAULT 1,
|
||||
default_config TEXT,
|
||||
required_role VARCHAR(50) DEFAULT 'Team Member',
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
category VARCHAR(50) DEFAULT 'General',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
|
||||
# Create indexes for better performance
|
||||
cursor.execute("CREATE INDEX idx_user_dashboard_user ON user_dashboard(user_id)")
|
||||
cursor.execute("CREATE INDEX idx_user_dashboard_default ON user_dashboard(user_id, is_default)")
|
||||
cursor.execute("CREATE INDEX idx_dashboard_widget_dashboard ON dashboard_widget(dashboard_id)")
|
||||
cursor.execute("CREATE INDEX idx_dashboard_widget_type ON dashboard_widget(widget_type)")
|
||||
cursor.execute("CREATE INDEX idx_widget_template_type ON widget_template(widget_type)")
|
||||
cursor.execute("CREATE INDEX idx_widget_template_category ON widget_template(category)")
|
||||
|
||||
# Insert default widget templates
|
||||
default_templates = [
|
||||
# Time Tracking Widgets
|
||||
('current_timer', 'Current Timer', 'Shows active time tracking session', '⏲️', 2, 1, '{}', 'Team Member', 'Time'),
|
||||
('daily_summary', 'Daily Summary', 'Today\'s time tracking summary', '📊', 2, 1, '{}', 'Team Member', 'Time'),
|
||||
('weekly_chart', 'Weekly Chart', 'Weekly time distribution chart', '📈', 3, 2, '{}', 'Team Member', 'Time'),
|
||||
('break_reminder', 'Break Reminder', 'Reminds when breaks are due', '☕', 1, 1, '{}', 'Team Member', 'Time'),
|
||||
|
||||
# Project Management Widgets
|
||||
('active_projects', 'Active Projects', 'List of current active projects', '📁', 2, 2, '{}', 'Team Member', 'Projects'),
|
||||
('project_progress', 'Project Progress', 'Visual progress of projects', '🎯', 2, 1, '{}', 'Team Member', 'Projects'),
|
||||
('project_activity', 'Recent Activity', 'Recent project activities', '🔄', 2, 1, '{}', 'Team Member', 'Projects'),
|
||||
('project_deadlines', 'Upcoming Deadlines', 'Projects with approaching deadlines', '⚠️', 2, 1, '{}', 'Team Member', 'Projects'),
|
||||
|
||||
# Task Management Widgets
|
||||
('assigned_tasks', 'My Tasks', 'Tasks assigned to me', '✅', 2, 2, '{}', 'Team Member', 'Tasks'),
|
||||
('task_priority', 'Priority Matrix', 'Tasks organized by priority', '🔥', 2, 2, '{}', 'Team Member', 'Tasks'),
|
||||
('kanban_summary', 'Kanban Overview', 'Summary of Kanban boards', '📋', 3, 1, '{}', 'Team Member', 'Tasks'),
|
||||
('task_trends', 'Task Trends', 'Task completion trends', '📉', 2, 1, '{}', 'Team Member', 'Tasks'),
|
||||
|
||||
# Analytics Widgets
|
||||
('productivity_metrics', 'Productivity', 'Personal productivity metrics', '⚡', 1, 1, '{}', 'Team Member', 'Analytics'),
|
||||
('time_distribution', 'Time Distribution', 'How time is distributed', '🥧', 2, 2, '{}', 'Team Member', 'Analytics'),
|
||||
('goal_progress', 'Goals', 'Progress towards goals', '🎯', 1, 1, '{}', 'Team Member', 'Analytics'),
|
||||
('performance_comparison', 'Performance', 'Performance comparison over time', '📊', 2, 1, '{}', 'Team Member', 'Analytics'),
|
||||
|
||||
# Team Widgets (Role-based)
|
||||
('team_overview', 'Team Overview', 'Overview of team performance', '👥', 3, 2, '{}', 'Team Leader', 'Team'),
|
||||
('resource_allocation', 'Resources', 'Team resource allocation', '📊', 2, 2, '{}', 'Administrator', 'Team'),
|
||||
('team_performance', 'Team Performance', 'Team performance metrics', '📈', 3, 1, '{}', 'Supervisor', 'Team'),
|
||||
('company_metrics', 'Company Metrics', 'Company-wide metrics', '🏢', 3, 2, '{}', 'System Administrator', 'Team'),
|
||||
|
||||
# Quick Action Widgets
|
||||
('quick_timer', 'Quick Timer', 'Quick time tracking controls', '▶️', 1, 1, '{}', 'Team Member', 'Actions'),
|
||||
('favorite_projects', 'Favorites', 'Quick access to favorite projects', '⭐', 1, 2, '{}', 'Team Member', 'Actions'),
|
||||
('recent_actions', 'Recent Actions', 'Recently performed actions', '🕒', 2, 1, '{}', 'Team Member', 'Actions'),
|
||||
('shortcuts_panel', 'Shortcuts', 'Quick action shortcuts', '🚀', 1, 1, '{}', 'Team Member', 'Actions'),
|
||||
]
|
||||
|
||||
cursor.executemany("""
|
||||
INSERT INTO widget_template
|
||||
(widget_type, name, description, icon, default_width, default_height, default_config, required_role, category)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", default_templates)
|
||||
|
||||
conn.commit()
|
||||
print("Dashboard system migration completed successfully!")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error during Dashboard system migration: {e}")
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function with command line interface."""
|
||||
@@ -1004,6 +1149,8 @@ def main():
|
||||
help='Run only system events migration')
|
||||
parser.add_argument('--kanban', '-k', action='store_true',
|
||||
help='Run only Kanban system migration')
|
||||
parser.add_argument('--dashboard', '--dash', action='store_true',
|
||||
help='Run only dashboard system migration')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -1039,6 +1186,9 @@ def main():
|
||||
elif args.kanban:
|
||||
migrate_kanban_system(db_path)
|
||||
|
||||
elif args.dashboard:
|
||||
migrate_dashboard_system(db_path)
|
||||
|
||||
else:
|
||||
# Default: run all migrations
|
||||
run_all_migrations(db_path)
|
||||
|
||||
162
models.py
162
models.py
@@ -831,3 +831,165 @@ class KanbanCard(db.Model):
|
||||
"""Check if a user can access this card"""
|
||||
# Check board's project permissions
|
||||
return self.column.board.project.is_user_allowed(user)
|
||||
|
||||
# Dashboard Widget System
|
||||
class WidgetType(enum.Enum):
|
||||
# Time Tracking Widgets
|
||||
CURRENT_TIMER = "current_timer"
|
||||
DAILY_SUMMARY = "daily_summary"
|
||||
WEEKLY_CHART = "weekly_chart"
|
||||
BREAK_REMINDER = "break_reminder"
|
||||
|
||||
# Project Management Widgets
|
||||
ACTIVE_PROJECTS = "active_projects"
|
||||
PROJECT_PROGRESS = "project_progress"
|
||||
PROJECT_ACTIVITY = "project_activity"
|
||||
PROJECT_DEADLINES = "project_deadlines"
|
||||
|
||||
# Task Management Widgets
|
||||
ASSIGNED_TASKS = "assigned_tasks"
|
||||
TASK_PRIORITY = "task_priority"
|
||||
KANBAN_SUMMARY = "kanban_summary"
|
||||
TASK_TRENDS = "task_trends"
|
||||
|
||||
# Analytics Widgets
|
||||
PRODUCTIVITY_METRICS = "productivity_metrics"
|
||||
TIME_DISTRIBUTION = "time_distribution"
|
||||
GOAL_PROGRESS = "goal_progress"
|
||||
PERFORMANCE_COMPARISON = "performance_comparison"
|
||||
|
||||
# Team Widgets (Role-based)
|
||||
TEAM_OVERVIEW = "team_overview"
|
||||
RESOURCE_ALLOCATION = "resource_allocation"
|
||||
TEAM_PERFORMANCE = "team_performance"
|
||||
COMPANY_METRICS = "company_metrics"
|
||||
|
||||
# Quick Action Widgets
|
||||
QUICK_TIMER = "quick_timer"
|
||||
FAVORITE_PROJECTS = "favorite_projects"
|
||||
RECENT_ACTIONS = "recent_actions"
|
||||
SHORTCUTS_PANEL = "shortcuts_panel"
|
||||
|
||||
class WidgetSize(enum.Enum):
|
||||
SMALL = "1x1" # 1 grid unit
|
||||
MEDIUM = "2x1" # 2 grid units wide, 1 high
|
||||
LARGE = "2x2" # 2x2 grid units
|
||||
WIDE = "3x1" # 3 grid units wide, 1 high
|
||||
TALL = "1x2" # 1 grid unit wide, 2 high
|
||||
EXTRA_LARGE = "3x2" # 3x2 grid units
|
||||
|
||||
class UserDashboard(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
name = db.Column(db.String(100), default='My Dashboard')
|
||||
is_default = db.Column(db.Boolean, default=True)
|
||||
layout_config = db.Column(db.Text) # JSON string for grid layout configuration
|
||||
|
||||
# Dashboard settings
|
||||
grid_columns = db.Column(db.Integer, default=6) # Number of grid columns
|
||||
theme = db.Column(db.String(20), default='light') # light, dark, auto
|
||||
auto_refresh = db.Column(db.Integer, default=300) # Auto-refresh interval in seconds
|
||||
|
||||
# Metadata
|
||||
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
# Relationships
|
||||
user = db.relationship('User', backref='dashboards')
|
||||
widgets = db.relationship('DashboardWidget', backref='dashboard', lazy=True, cascade='all, delete-orphan')
|
||||
|
||||
# Unique constraint - one default dashboard per user
|
||||
__table_args__ = (db.Index('idx_user_default_dashboard', 'user_id', 'is_default'),)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<UserDashboard {self.name} (User: {self.user.username})>'
|
||||
|
||||
class DashboardWidget(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
dashboard_id = db.Column(db.Integer, db.ForeignKey('user_dashboard.id'), nullable=False)
|
||||
widget_type = db.Column(db.Enum(WidgetType), nullable=False)
|
||||
|
||||
# Grid position and size
|
||||
grid_x = db.Column(db.Integer, nullable=False, default=0) # X position in grid
|
||||
grid_y = db.Column(db.Integer, nullable=False, default=0) # Y position in grid
|
||||
grid_width = db.Column(db.Integer, nullable=False, default=1) # Width in grid units
|
||||
grid_height = db.Column(db.Integer, nullable=False, default=1) # Height in grid units
|
||||
|
||||
# Widget configuration
|
||||
title = db.Column(db.String(100)) # Custom widget title
|
||||
config = db.Column(db.Text) # JSON string for widget-specific configuration
|
||||
refresh_interval = db.Column(db.Integer, default=60) # Refresh interval in seconds
|
||||
|
||||
# Widget state
|
||||
is_visible = db.Column(db.Boolean, default=True)
|
||||
is_minimized = db.Column(db.Boolean, default=False)
|
||||
z_index = db.Column(db.Integer, default=1) # Stacking order
|
||||
|
||||
# Metadata
|
||||
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<DashboardWidget {self.widget_type.value} ({self.grid_width}x{self.grid_height})>'
|
||||
|
||||
@property
|
||||
def config_dict(self):
|
||||
"""Parse widget configuration JSON"""
|
||||
if self.config:
|
||||
import json
|
||||
try:
|
||||
return json.loads(self.config)
|
||||
except:
|
||||
return {}
|
||||
return {}
|
||||
|
||||
@config_dict.setter
|
||||
def config_dict(self, value):
|
||||
"""Set widget configuration as JSON"""
|
||||
import json
|
||||
self.config = json.dumps(value) if value else None
|
||||
|
||||
class WidgetTemplate(db.Model):
|
||||
"""Pre-defined widget templates for easy dashboard setup"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
widget_type = db.Column(db.Enum(WidgetType), nullable=False)
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
description = db.Column(db.Text)
|
||||
icon = db.Column(db.String(50)) # Icon name or emoji
|
||||
|
||||
# Default configuration
|
||||
default_width = db.Column(db.Integer, default=1)
|
||||
default_height = db.Column(db.Integer, default=1)
|
||||
default_config = db.Column(db.Text) # JSON string for default widget configuration
|
||||
|
||||
# Access control
|
||||
required_role = db.Column(db.Enum(Role), default=Role.TEAM_MEMBER)
|
||||
is_active = db.Column(db.Boolean, default=True)
|
||||
|
||||
# Categories for organization
|
||||
category = db.Column(db.String(50), default='General') # Time, Projects, Tasks, Analytics, Team, Actions
|
||||
|
||||
# Metadata
|
||||
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<WidgetTemplate {self.name} ({self.widget_type.value})>'
|
||||
|
||||
def can_user_access(self, user):
|
||||
"""Check if user has required role to use this widget"""
|
||||
if not self.is_active:
|
||||
return False
|
||||
|
||||
# Define role hierarchy
|
||||
role_hierarchy = {
|
||||
Role.TEAM_MEMBER: 1,
|
||||
Role.TEAM_LEADER: 2,
|
||||
Role.SUPERVISOR: 3,
|
||||
Role.ADMIN: 4,
|
||||
Role.SYSTEM_ADMIN: 5
|
||||
}
|
||||
|
||||
user_level = role_hierarchy.get(user.role, 0)
|
||||
required_level = role_hierarchy.get(self.required_role, 0)
|
||||
|
||||
return user_level >= required_level
|
||||
File diff suppressed because it is too large
Load Diff
@@ -41,6 +41,7 @@
|
||||
<ul>
|
||||
{% if g.user %}
|
||||
<li><a href="{{ url_for('home') }}" data-tooltip="Home"><i class="nav-icon">🏠</i><span class="nav-text">Home</span></a></li>
|
||||
<li><a href="{{ url_for('dashboard') }}" data-tooltip="Dashboard"><i class="nav-icon">📊</i><span class="nav-text">Dashboard</span></a></li>
|
||||
<li><a href="{{ url_for('kanban_overview') }}" data-tooltip="Kanban Board"><i class="nav-icon">📋</i><span class="nav-text">Kanban Board</span></a></li>
|
||||
<li><a href="{{ url_for('analytics') }}" data-tooltip="Time Analytics"><i class="nav-icon">📊</i><span class="nav-text">Analytics</span></a></li>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user