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 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 (
|
from data_formatting import (
|
||||||
format_duration, prepare_export_data, prepare_team_hours_export_data,
|
format_duration, prepare_export_data, prepare_team_hours_export_data,
|
||||||
format_table_data, format_graph_data, format_team_data
|
format_table_data, format_graph_data, format_team_data
|
||||||
@@ -879,62 +879,11 @@ def verify_email(token):
|
|||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
@app.route('/dashboard')
|
@app.route('/dashboard')
|
||||||
@role_required(Role.TEAM_LEADER)
|
@role_required(Role.TEAM_MEMBER)
|
||||||
|
@company_required
|
||||||
def dashboard():
|
def dashboard():
|
||||||
# Get dashboard data based on user role
|
"""User dashboard with configurable widgets."""
|
||||||
dashboard_data = {}
|
return render_template('dashboard.html', title='Dashboard')
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
# Redirect old admin dashboard URL to new dashboard
|
# Redirect old admin dashboard URL to new dashboard
|
||||||
|
|
||||||
@@ -4467,6 +4416,606 @@ def delete_kanban_column(column_id):
|
|||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
return jsonify({'success': False, 'message': str(e)})
|
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__':
|
if __name__ == '__main__':
|
||||||
port = int(os.environ.get('PORT', 5000))
|
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,
|
from models import (User, TimeEntry, WorkConfig, SystemSettings, Team, Role, Project,
|
||||||
Company, CompanyWorkConfig, UserPreferences, WorkRegion, AccountType,
|
Company, CompanyWorkConfig, UserPreferences, WorkRegion, AccountType,
|
||||||
ProjectCategory, Task, SubTask, TaskStatus, TaskPriority, Announcement, SystemEvent, KanbanBoard,
|
ProjectCategory, Task, SubTask, TaskStatus, TaskPriority, Announcement, SystemEvent, KanbanBoard,
|
||||||
KanbanColumn, KanbanCard)
|
KanbanColumn, KanbanCard, WidgetType, UserDashboard, DashboardWidget,
|
||||||
|
WidgetTemplate)
|
||||||
from werkzeug.security import generate_password_hash
|
from werkzeug.security import generate_password_hash
|
||||||
FLASK_AVAILABLE = True
|
FLASK_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -74,6 +75,7 @@ def run_all_migrations(db_path=None):
|
|||||||
migrate_task_system(db_path)
|
migrate_task_system(db_path)
|
||||||
migrate_system_events(db_path)
|
migrate_system_events(db_path)
|
||||||
migrate_kanban_system(db_path)
|
migrate_kanban_system(db_path)
|
||||||
|
migrate_dashboard_system(db_path)
|
||||||
|
|
||||||
if FLASK_AVAILABLE:
|
if FLASK_AVAILABLE:
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
@@ -985,6 +987,149 @@ def migrate_kanban_system(db_file=None):
|
|||||||
conn.close()
|
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():
|
def main():
|
||||||
"""Main function with command line interface."""
|
"""Main function with command line interface."""
|
||||||
@@ -1004,6 +1149,8 @@ def main():
|
|||||||
help='Run only system events migration')
|
help='Run only system events migration')
|
||||||
parser.add_argument('--kanban', '-k', action='store_true',
|
parser.add_argument('--kanban', '-k', action='store_true',
|
||||||
help='Run only Kanban system migration')
|
help='Run only Kanban system migration')
|
||||||
|
parser.add_argument('--dashboard', '--dash', action='store_true',
|
||||||
|
help='Run only dashboard system migration')
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
@@ -1039,6 +1186,9 @@ def main():
|
|||||||
elif args.kanban:
|
elif args.kanban:
|
||||||
migrate_kanban_system(db_path)
|
migrate_kanban_system(db_path)
|
||||||
|
|
||||||
|
elif args.dashboard:
|
||||||
|
migrate_dashboard_system(db_path)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Default: run all migrations
|
# Default: run all migrations
|
||||||
run_all_migrations(db_path)
|
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 if a user can access this card"""
|
||||||
# Check board's project permissions
|
# Check board's project permissions
|
||||||
return self.column.board.project.is_user_allowed(user)
|
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>
|
<ul>
|
||||||
{% if g.user %}
|
{% 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('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('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>
|
<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