From 1fe3f18bbdf59a4dd4f407ef367acd7067bd7fd6 Mon Sep 17 00:00:00 2001 From: Jens Luedicke Date: Fri, 4 Jul 2025 21:55:54 +0200 Subject: [PATCH] Remove obsolete Kanban parts. --- app.py | 947 +--------------- migrate_db.py | 112 +- models.py | 131 --- static/css/style.css | 61 - templates/admin_projects.html | 1 - templates/confirm_company_deletion.html | 26 - templates/dashboard.html | 71 -- templates/kanban_overview.html | 695 ------------ templates/project_kanban.html | 1377 ----------------------- templates/sprint_management.html | 330 +++++- templates/unified_task_management.html | 311 ++++- 11 files changed, 625 insertions(+), 3437 deletions(-) delete mode 100644 templates/kanban_overview.html delete mode 100644 templates/project_kanban.html diff --git a/app.py b/app.py index 800ca20..fcfc37f 100644 --- a/app.py +++ b/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, Sprint, SprintStatus, Announcement, SystemEvent, KanbanBoard, KanbanColumn, KanbanCard, WidgetType, UserDashboard, DashboardWidget, WidgetTemplate +from models import db, TimeEntry, WorkConfig, User, SystemSettings, Team, Role, Project, Company, CompanyWorkConfig, UserPreferences, WorkRegion, AccountType, ProjectCategory, Task, SubTask, TaskStatus, TaskPriority, Sprint, SprintStatus, Announcement, SystemEvent, 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, format_burndown_data @@ -1061,12 +1061,6 @@ def delete_user(user_id): # Transfer ownership of project categories to alternative admin ProjectCategory.query.filter_by(created_by_id=user_id).update({'created_by_id': alternative_admin.id}) - - # Transfer ownership of kanban boards to alternative admin - KanbanBoard.query.filter_by(created_by_id=user_id).update({'created_by_id': alternative_admin.id}) - - # Transfer ownership of kanban cards to alternative admin - KanbanCard.query.filter_by(created_by_id=user_id).update({'created_by_id': alternative_admin.id}) else: # No alternative admin found - redirect to company deletion confirmation flash('No other administrator or supervisor found. Company deletion required.', 'warning') @@ -1083,7 +1077,6 @@ def delete_user(user_id): # Clear task and subtask assignments Task.query.filter_by(assigned_to_id=user_id).update({'assigned_to_id': None}) SubTask.query.filter_by(assigned_to_id=user_id).update({'assigned_to_id': None}) - KanbanCard.query.filter_by(assigned_to_id=user_id).update({'assigned_to_id': None}) # Now safe to delete the user db.session.delete(user) @@ -1153,19 +1146,12 @@ def confirm_company_deletion(user_id): # Delete all company-related data in the correct order # First, clear foreign key references that could cause constraint violations - # 1. Clear task references in kanban cards (they reference tasks) - KanbanCard.query.filter(KanbanCard.column_id.in_( - db.session.query(KanbanColumn.id).filter(KanbanColumn.board_id.in_( - db.session.query(KanbanBoard.id).filter(KanbanBoard.company_id == company.id) - )) - )).update({'task_id': None}, synchronize_session=False) - - # 2. Delete time entries (they reference tasks and users) + # 1. Delete time entries (they reference tasks and users) TimeEntry.query.filter(TimeEntry.user_id.in_( db.session.query(User.id).filter(User.company_id == company.id) )).delete(synchronize_session=False) - # 3. Delete user preferences and dashboards + # 2. Delete user preferences and dashboards UserPreferences.query.filter(UserPreferences.user_id.in_( db.session.query(User.id).filter(User.company_id == company.id) )).delete(synchronize_session=False) @@ -1174,57 +1160,42 @@ def confirm_company_deletion(user_id): db.session.query(User.id).filter(User.company_id == company.id) )).delete(synchronize_session=False) - # 4. Delete work configs + # 3. Delete work configs WorkConfig.query.filter(WorkConfig.user_id.in_( db.session.query(User.id).filter(User.company_id == company.id) )).delete(synchronize_session=False) - # 5. Delete kanban cards (now safe since task references are cleared) - KanbanCard.query.filter(KanbanCard.column_id.in_( - db.session.query(KanbanColumn.id).filter(KanbanColumn.board_id.in_( - db.session.query(KanbanBoard.id).filter(KanbanBoard.company_id == company.id) - )) - )).delete(synchronize_session=False) - - # 6. Delete kanban columns (they depend on boards) - KanbanColumn.query.filter(KanbanColumn.board_id.in_( - db.session.query(KanbanBoard.id).filter(KanbanBoard.company_id == company.id) - )).delete(synchronize_session=False) - - # 7. Delete kanban boards - KanbanBoard.query.filter_by(company_id=company.id).delete() - - # 8. Delete subtasks (they depend on tasks) + # 4. Delete subtasks (they depend on tasks) SubTask.query.filter(SubTask.task_id.in_( db.session.query(Task.id).filter(Task.project_id.in_( db.session.query(Project.id).filter(Project.company_id == company.id) )) )).delete(synchronize_session=False) - # 9. Delete tasks (now safe since kanban cards and subtasks are deleted) + # 5. Delete tasks (now safe since subtasks are deleted) Task.query.filter(Task.project_id.in_( db.session.query(Project.id).filter(Project.company_id == company.id) )).delete(synchronize_session=False) - # 10. Delete projects + # 6. Delete projects Project.query.filter_by(company_id=company.id).delete() - # 11. Delete project categories + # 7. Delete project categories ProjectCategory.query.filter_by(company_id=company.id).delete() - # 12. Delete company work config + # 8. Delete company work config CompanyWorkConfig.query.filter_by(company_id=company.id).delete() - # 13. Delete teams + # 9. Delete teams Team.query.filter_by(company_id=company.id).delete() - # 14. Delete users + # 10. Delete users User.query.filter_by(company_id=company.id).delete() - # 15. Delete system events for this company + # 11. Delete system events for this company SystemEvent.query.filter_by(company_id=company.id).delete() - # 16. Finally, delete the company itself + # 12. Finally, delete the company itself db.session.delete(company) db.session.commit() @@ -1254,7 +1225,6 @@ def confirm_company_deletion(user_id): teams = Team.query.filter_by(company_id=company.id).all() projects = Project.query.filter_by(company_id=company.id).all() categories = ProjectCategory.query.filter_by(company_id=company.id).all() - kanban_boards = KanbanBoard.query.filter_by(company_id=company.id).all() # Get tasks for all projects in the company project_ids = [p.id for p in projects] @@ -1277,7 +1247,6 @@ def confirm_company_deletion(user_id): teams=teams, projects=projects, categories=categories, - kanban_boards=kanban_boards, tasks=tasks, time_entries_count=time_entries_count, total_hours_tracked=total_hours_tracked) @@ -2128,12 +2097,6 @@ def system_admin_delete_user(user_id): # Transfer ownership of project categories to alternative admin ProjectCategory.query.filter_by(created_by_id=user_id).update({'created_by_id': alternative_admin.id}) - - # Transfer ownership of kanban boards to alternative admin - KanbanBoard.query.filter_by(created_by_id=user_id).update({'created_by_id': alternative_admin.id}) - - # Transfer ownership of kanban cards to alternative admin - KanbanCard.query.filter_by(created_by_id=user_id).update({'created_by_id': alternative_admin.id}) else: # No alternative admin found - redirect to company deletion confirmation flash('No other administrator or supervisor found in the same company. Company deletion required.', 'warning') @@ -2150,7 +2113,6 @@ def system_admin_delete_user(user_id): # Clear task and subtask assignments Task.query.filter_by(assigned_to_id=user_id).update({'assigned_to_id': None}) SubTask.query.filter_by(assigned_to_id=user_id).update({'assigned_to_id': None}) - KanbanCard.query.filter_by(assigned_to_id=user_id).update({'assigned_to_id': None}) # Now safe to delete the user db.session.delete(user) @@ -3674,157 +3636,14 @@ def manage_project_tasks(project_id): tasks=tasks, team_members=team_members) -@app.route('/admin/projects//kanban') -@role_required(Role.TEAM_MEMBER) -@company_required -def project_kanban(project_id): - project = Project.query.filter_by(id=project_id, company_id=g.user.company_id).first_or_404() - # Check if user has access to this project - if not project.is_user_allowed(g.user): - flash('You do not have access to this project.', 'error') - return redirect(url_for('admin_projects')) - - # Get all Kanban boards for this company (unified boards) - boards = KanbanBoard.query.filter_by(company_id=g.user.company_id, is_active=True).order_by(KanbanBoard.created_at.desc()).all() - - # Get team members for assignment dropdown - if project.team_id: - team_members = User.query.filter_by(team_id=project.team_id, company_id=g.user.company_id).all() - else: - team_members = User.query.filter_by(company_id=g.user.company_id).all() - - # Get tasks for task assignment dropdown - tasks = Task.query.filter_by(project_id=project_id).order_by(Task.name).all() - - # Get available projects for card assignment dropdown - available_projects = [] - if g.user.role in [Role.ADMIN, Role.SUPERVISOR]: - # Admins and Supervisors can see all company projects - all_projects = Project.query.filter_by(company_id=g.user.company_id, is_active=True).order_by(Project.name).all() - available_projects = [p for p in all_projects if p.is_user_allowed(g.user)] - elif g.user.team_id: - # Team members see team projects + unassigned projects - all_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) - ).order_by(Project.name).all() - available_projects = [p for p in all_projects if p.is_user_allowed(g.user)] - else: - # Unassigned users see only unassigned projects - all_projects = Project.query.filter_by( - company_id=g.user.company_id, - team_id=None, - is_active=True - ).order_by(Project.name).all() - available_projects = [p for p in all_projects if p.is_user_allowed(g.user)] - - return render_template('project_kanban.html', - title=f'Kanban - {project.name}', - project=project, - boards=boards, - team_members=team_members, - tasks=tasks, - available_projects=available_projects) - -@app.route('/kanban') -@role_required(Role.TEAM_MEMBER) -@company_required -def kanban_overview(): - # Check if create=true parameter is present - create_board = request.args.get('create') == 'true' - - # Check if board parameter is present (for viewing a specific board) - board_id = request.args.get('board') - if board_id: - try: - board_id = int(board_id) - # Verify board exists and user has access - board = KanbanBoard.query.filter_by(id=board_id, company_id=g.user.company_id, is_active=True).first() - if board: - # Get team members for assignment dropdown - team_members = User.query.filter_by(company_id=g.user.company_id).all() - - # Get all boards for the dropdown (so users can switch between boards) - all_boards = KanbanBoard.query.filter_by(company_id=g.user.company_id, is_active=True).order_by(KanbanBoard.created_at.desc()).all() - - # Get available projects for card assignment dropdown - available_projects = [] - if g.user.role in [Role.ADMIN, Role.SUPERVISOR]: - # Admins and Supervisors can see all company projects - all_projects = Project.query.filter_by(company_id=g.user.company_id, is_active=True).order_by(Project.name).all() - available_projects = [p for p in all_projects if p.is_user_allowed(g.user)] - elif g.user.team_id: - # Team members see team projects + unassigned projects - all_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) - ).order_by(Project.name).all() - available_projects = [p for p in all_projects if p.is_user_allowed(g.user)] - else: - # Unassigned users see only unassigned projects - all_projects = Project.query.filter_by( - company_id=g.user.company_id, - team_id=None, - is_active=True - ).order_by(Project.name).all() - available_projects = [p for p in all_projects if p.is_user_allowed(g.user)] - - # Render the board view template - return render_template('project_kanban.html', - title=f'Kanban - {board.name}', - project=None, # No specific project context - boards=all_boards, # Pass all boards for dropdown - team_members=team_members, - tasks=[], # No project-specific tasks - available_projects=available_projects, - selected_board_id=board_id) - else: - flash('Board not found or access denied.', 'error') - return redirect(url_for('kanban_overview')) - except ValueError: - flash('Invalid board ID.', 'error') - return redirect(url_for('kanban_overview')) - - # Get all projects the user has access to (for context) - if g.user.role in [Role.ADMIN, Role.SUPERVISOR]: - # Admins and Supervisors can see all company projects - projects = Project.query.filter_by(company_id=g.user.company_id, is_active=True).order_by(Project.name).all() - elif g.user.team_id: - # Team members see team projects + unassigned projects - 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) - ).order_by(Project.name).all() - else: - # Unassigned users see only unassigned projects - projects = Project.query.filter_by( - company_id=g.user.company_id, - team_id=None, - is_active=True - ).order_by(Project.name).all() - - # Get all company Kanban boards (unified boards) - boards = KanbanBoard.query.filter_by( - company_id=g.user.company_id, - is_active=True - ).order_by(KanbanBoard.created_at.desc()).all() - - return render_template('kanban_overview.html', - title='Kanban Overview', - boards=boards, - projects=projects, - create_board=create_board) # Unified Task Management Route @app.route('/tasks') @role_required(Role.TEAM_MEMBER) @company_required def unified_task_management(): - """Unified task management interface combining tasks and kanban""" + """Unified task management interface""" # Get all projects the user has access to (for filtering and task creation) if g.user.role in [Role.ADMIN, Role.SUPERVISOR]: @@ -4069,7 +3888,7 @@ def delete_task(task_id): @role_required(Role.TEAM_MEMBER) @company_required def get_unified_tasks(): - """Get all tasks for unified kanban view""" + """Get all tasks for unified task view""" try: # Base query for tasks in user's company query = Task.query.join(Project).filter(Project.company_id == g.user.company_id) @@ -4136,7 +3955,7 @@ def get_unified_tasks(): @role_required(Role.TEAM_MEMBER) @company_required def update_task_status(task_id): - """Update task status (for kanban card movement)""" + """Update task status""" try: task = Task.query.join(Project).filter( Task.id == task_id, @@ -4183,92 +4002,6 @@ def update_task_status(task_id): logger.error(f"Error updating task status: {str(e)}") return jsonify({'success': False, 'message': str(e)}) -@app.route('/api/admin/migrate-kanban-cards', methods=['POST']) -@role_required(Role.ADMIN) -@company_required -def migrate_kanban_cards(): - """Admin endpoint to migrate orphaned KanbanCards to Tasks""" - try: - from models import KanbanCard, Task, TaskStatus, TaskPriority, KanbanColumn - - def map_column_to_task_status(column_name): - """Map Kanban column name to Task status""" - column_name = column_name.upper().strip() - - if any(keyword in column_name for keyword in ['TODO', 'TO DO', 'BACKLOG', 'PLANNED', 'NOT STARTED']): - return TaskStatus.NOT_STARTED - elif any(keyword in column_name for keyword in ['DOING', 'IN PROGRESS', 'ACTIVE', 'WORKING']): - return TaskStatus.IN_PROGRESS - elif any(keyword in column_name for keyword in ['HOLD', 'BLOCKED', 'WAITING', 'PAUSED']): - return TaskStatus.ON_HOLD - elif any(keyword in column_name for keyword in ['DONE', 'COMPLETE', 'FINISHED', 'CLOSED']): - return TaskStatus.COMPLETED - else: - return TaskStatus.NOT_STARTED - - def get_task_priority_from_card(card): - """Determine task priority from card properties""" - text_to_check = f"{card.title} {card.description or ''}".upper() - - if any(keyword in text_to_check for keyword in ['URGENT', 'CRITICAL', 'ASAP', '!!!', 'HIGH PRIORITY']): - return TaskPriority.URGENT - elif any(keyword in text_to_check for keyword in ['HIGH', 'IMPORTANT', '!!']): - return TaskPriority.HIGH - elif any(keyword in text_to_check for keyword in ['LOW', 'MINOR', 'NICE TO HAVE']): - return TaskPriority.LOW - else: - return TaskPriority.MEDIUM - - # Find orphaned KanbanCards in user's company - orphaned_cards = db.session.query(KanbanCard).join(KanbanColumn).join(KanbanBoard).filter( - KanbanBoard.company_id == g.user.company_id, - KanbanCard.task_id.is_(None) - ).all() - - if not orphaned_cards: - return jsonify({'success': True, 'message': 'No orphaned KanbanCards found', 'converted_count': 0}) - - converted_count = 0 - - for card in orphaned_cards: - column = KanbanColumn.query.get(card.column_id) - if not column: - continue - - task_status = map_column_to_task_status(column.name) - task_priority = get_task_priority_from_card(card) - - task = Task( - name=card.title, - description=card.description or '', - status=task_status, - priority=task_priority, - project_id=card.project_id, - assigned_to_id=card.assigned_to_id, - due_date=card.due_date, - completed_date=card.completed_date if task_status == TaskStatus.COMPLETED else None, - created_by_id=card.created_by_id, - created_at=card.created_at, - ) - - db.session.add(task) - db.session.flush() - - card.task_id = task.id - converted_count += 1 - - db.session.commit() - - return jsonify({ - 'success': True, - 'message': f'Successfully converted {converted_count} KanbanCards to Tasks', - 'converted_count': converted_count - }) - - except Exception as e: - db.session.rollback() - logger.error(f"Error in migrate_kanban_cards: {str(e)}") - return jsonify({'success': False, 'message': str(e)}) # Sprint Management APIs @app.route('/api/sprints') @@ -4739,632 +4472,6 @@ def delete_category(category_id): db.session.rollback() return jsonify({'success': False, 'message': str(e)}) -# Kanban API Routes -@app.route('/api/kanban/stats') -@role_required(Role.TEAM_MEMBER) -@company_required -def get_kanban_stats(): - try: - # Get all projects the user has access to - if g.user.role in [Role.ADMIN, Role.SUPERVISOR]: - projects = Project.query.filter_by(company_id=g.user.company_id, is_active=True).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) - ).all() - else: - projects = Project.query.filter_by( - company_id=g.user.company_id, - team_id=None, - is_active=True - ).all() - - # Count company boards and cards - boards = KanbanBoard.query.filter_by(company_id=g.user.company_id, is_active=True).all() - total_boards = len(boards) - total_cards = 0 - - for board in boards: - for column in board.columns: - total_cards += len([card for card in column.cards if card.is_active]) - - return jsonify({ - 'success': True, - 'stats': { - 'total_boards': total_boards, - 'total_cards': total_cards, - 'total_projects': len(projects) - } - }) - - except Exception as e: - return jsonify({'success': False, 'message': str(e)}) - -@app.route('/api/kanban/boards', methods=['GET']) -@role_required(Role.TEAM_MEMBER) -@company_required -def get_kanban_boards(): - try: - # Optional project_id filter for backward compatibility - project_id = request.args.get('project_id') - - # Base query for company boards - boards_query = KanbanBoard.query.filter_by(company_id=g.user.company_id, is_active=True) - - # If project_id is provided, verify access and filter - if project_id: - project = Project.query.filter_by(id=project_id, company_id=g.user.company_id).first() - if not project or not project.is_user_allowed(g.user): - return jsonify({'success': False, 'message': 'Project not found or access denied'}) - # For backward compatibility, we can still filter by project context - # but boards are now company-wide - - boards = boards_query.all() - - boards_data = [] - for board in boards: - boards_data.append({ - 'id': board.id, - 'name': board.name, - 'description': board.description, - 'is_default': board.is_default, - 'created_at': board.created_at.isoformat(), - 'column_count': len(board.columns) - }) - - return jsonify({'success': True, 'boards': boards_data}) - - except Exception as e: - return jsonify({'success': False, 'message': str(e)}) - -@app.route('/api/kanban/boards', methods=['POST']) -@role_required(Role.TEAM_LEADER) -@company_required -def create_kanban_board(): - try: - data = request.get_json() - - # Optional project_id for backward compatibility - project_id = data.get('project_id') - if project_id: - project_id = int(project_id) - # Verify project access if provided - project = Project.query.filter_by(id=project_id, company_id=g.user.company_id).first() - if not project or not project.is_user_allowed(g.user): - return jsonify({'success': False, 'message': 'Project not found or access denied'}) - - name = data.get('name') - if not name: - return jsonify({'success': False, 'message': 'Board name is required'}) - - # Check if board name already exists in company - existing = KanbanBoard.query.filter_by(company_id=g.user.company_id, name=name).first() - if existing: - return jsonify({'success': False, 'message': 'Board name already exists in this company'}) - - # Create board - board = KanbanBoard( - name=name, - description=data.get('description', ''), - company_id=g.user.company_id, - is_default=data.get('is_default') in ['true', 'on', True], - created_by_id=g.user.id - ) - - db.session.add(board) - db.session.flush() # Get board ID for columns - - # Create default columns - default_columns = [ - {'name': 'To Do', 'position': 1, 'color': '#6c757d'}, - {'name': 'In Progress', 'position': 2, 'color': '#007bff'}, - {'name': 'Done', 'position': 3, 'color': '#28a745'} - ] - - for col_data in default_columns: - column = KanbanColumn( - name=col_data['name'], - position=col_data['position'], - color=col_data['color'], - board_id=board.id - ) - db.session.add(column) - - db.session.commit() - - return jsonify({'success': True, 'message': 'Board created successfully', 'board_id': board.id}) - - except Exception as e: - db.session.rollback() - return jsonify({'success': False, 'message': str(e)}) - -@app.route('/api/kanban/boards/', methods=['GET']) -@role_required(Role.TEAM_MEMBER) -@company_required -def get_kanban_board(board_id): - try: - board = KanbanBoard.query.filter( - KanbanBoard.id == board_id, - KanbanBoard.company_id == g.user.company_id - ).first() - - if not board: - return jsonify({'success': False, 'message': 'Board not found or access denied'}) - - columns_data = [] - for column in board.columns: - if not column.is_active: - continue - - cards_data = [] - for card in column.cards: - if not card.is_active: - continue - - cards_data.append({ - 'id': card.id, - 'title': card.title, - 'description': card.description, - 'position': card.position, - 'color': card.color, - 'assigned_to': { - 'id': card.assigned_to.id, - 'username': card.assigned_to.username - } if card.assigned_to else None, - 'project': { - 'id': card.project.id, - 'name': card.project.name, - 'code': card.project.code - } if card.project else None, - 'task_id': card.task_id, - 'task_name': card.task.name if card.task else None, - 'due_date': card.due_date.isoformat() if card.due_date else None, - 'created_at': card.created_at.isoformat() - }) - - columns_data.append({ - 'id': column.id, - 'name': column.name, - 'description': column.description, - 'position': column.position, - 'color': column.color, - 'wip_limit': column.wip_limit, - 'card_count': column.card_count, - 'is_over_wip_limit': column.is_over_wip_limit, - 'cards': cards_data - }) - - board_data = { - 'id': board.id, - 'name': board.name, - 'description': board.description, - 'company': { - 'id': board.company.id, - 'name': board.company.name - }, - 'columns': columns_data - } - - return jsonify({'success': True, 'board': board_data}) - - except Exception as e: - return jsonify({'success': False, 'message': str(e)}) - -@app.route('/api/kanban/cards', methods=['POST']) -@role_required(Role.TEAM_MEMBER) -@company_required -def create_kanban_card(): - try: - data = request.get_json() - column_id = data.get('column_id') - - # Verify column access - column = KanbanColumn.query.join(KanbanBoard).filter( - KanbanColumn.id == column_id, - KanbanBoard.company_id == g.user.company_id - ).first() - - if not column: - return jsonify({'success': False, 'message': 'Column not found or access denied'}) - - title = data.get('title') - if not title: - return jsonify({'success': False, 'message': 'Card title is required'}) - - # Calculate position (add to end of column) - max_position = db.session.query(func.max(KanbanCard.position)).filter_by( - column_id=column_id, is_active=True - ).scalar() or 0 - - # Parse due date - due_date = None - if data.get('due_date'): - due_date = datetime.strptime(data.get('due_date'), '%Y-%m-%d').date() - - # Verify project access if project_id is provided - project_id = None - if data.get('project_id'): - project_id = int(data.get('project_id')) - project = Project.query.filter_by(id=project_id, company_id=g.user.company_id).first() - if not project or not project.is_user_allowed(g.user): - return jsonify({'success': False, 'message': 'Project not found or access denied'}) - - # Create card - card = KanbanCard( - title=title, - description=data.get('description', ''), - position=max_position + 1, - color=data.get('color'), - column_id=column_id, - project_id=project_id, - task_id=int(data.get('task_id')) if data.get('task_id') else None, - assigned_to_id=int(data.get('assigned_to_id')) if data.get('assigned_to_id') else None, - due_date=due_date, - created_by_id=g.user.id - ) - - db.session.add(card) - db.session.commit() - - return jsonify({'success': True, 'message': 'Card created successfully', 'card_id': card.id}) - - except Exception as e: - db.session.rollback() - return jsonify({'success': False, 'message': str(e)}) - -@app.route('/api/kanban/cards//move', methods=['PUT']) -@role_required(Role.TEAM_MEMBER) -@company_required -def move_kanban_card(card_id): - try: - data = request.get_json() - new_column_id = data.get('column_id') - new_position = data.get('position') - - # Verify card access - card = KanbanCard.query.join(KanbanColumn).join(KanbanBoard).filter( - KanbanCard.id == card_id, - KanbanBoard.company_id == g.user.company_id - ).first() - - if not card: - return jsonify({'success': False, 'message': 'Card not found or access denied'}) - - # Verify new column access - new_column = KanbanColumn.query.join(KanbanBoard).filter( - KanbanColumn.id == new_column_id, - KanbanBoard.company_id == g.user.company_id - ).first() - - if not new_column: - return jsonify({'success': False, 'message': 'Target column not found or access denied'}) - - old_column_id = card.column_id - old_position = card.position - - # Update positions in old column (if moving to different column) - if old_column_id != new_column_id: - # Shift cards down in old column - cards_to_shift = KanbanCard.query.filter( - KanbanCard.column_id == old_column_id, - KanbanCard.position > old_position, - KanbanCard.is_active == True - ).all() - - for c in cards_to_shift: - c.position -= 1 - - # Update positions in new column - cards_to_shift = KanbanCard.query.filter( - KanbanCard.column_id == new_column_id, - KanbanCard.position >= new_position, - KanbanCard.is_active == True, - KanbanCard.id != card_id - ).all() - - for c in cards_to_shift: - c.position += 1 - - # Update card - card.column_id = new_column_id - card.position = new_position - card.updated_at = datetime.now() - - db.session.commit() - - return jsonify({'success': True, 'message': 'Card moved successfully'}) - - except Exception as e: - db.session.rollback() - return jsonify({'success': False, 'message': str(e)}) - -@app.route('/api/kanban/cards/', methods=['PUT']) -@role_required(Role.TEAM_MEMBER) -@company_required -def update_kanban_card(card_id): - try: - card = KanbanCard.query.join(KanbanColumn).join(KanbanBoard).filter( - KanbanCard.id == card_id, - KanbanBoard.company_id == g.user.company_id - ).first() - - if not card: - return jsonify({'success': False, 'message': 'Card not found or access denied'}) - - data = request.get_json() - - # Update card fields - if 'title' in data: - card.title = data['title'] - if 'description' in data: - card.description = data['description'] - if 'color' in data: - card.color = data['color'] - if 'assigned_to_id' in data: - card.assigned_to_id = int(data['assigned_to_id']) if data['assigned_to_id'] else None - if 'task_id' in data: - card.task_id = int(data['task_id']) if data['task_id'] else None - if 'due_date' in data: - card.due_date = datetime.strptime(data['due_date'], '%Y-%m-%d').date() if data['due_date'] else None - if 'project_id' in data: - # Verify project access if project_id is provided - if data['project_id']: - project_id = int(data['project_id']) - project = Project.query.filter_by(id=project_id, company_id=g.user.company_id).first() - if not project or not project.is_user_allowed(g.user): - return jsonify({'success': False, 'message': 'Project not found or access denied'}) - card.project_id = project_id - else: - card.project_id = None - - card.updated_at = datetime.now() - db.session.commit() - - return jsonify({'success': True, 'message': 'Card updated successfully'}) - - except Exception as e: - db.session.rollback() - return jsonify({'success': False, 'message': str(e)}) - -@app.route('/api/kanban/cards/', methods=['DELETE']) -@role_required(Role.TEAM_MEMBER) -@company_required -def delete_kanban_card(card_id): - try: - card = KanbanCard.query.join(KanbanColumn).join(KanbanBoard).filter( - KanbanCard.id == card_id, - KanbanBoard.company_id == g.user.company_id - ).first() - - if not card: - return jsonify({'success': False, 'message': 'Card not found or access denied'}) - - column_id = card.column_id - position = card.position - - # Soft delete - card.is_active = False - card.updated_at = datetime.now() - - # Shift remaining cards up - cards_to_shift = KanbanCard.query.filter( - KanbanCard.column_id == column_id, - KanbanCard.position > position, - KanbanCard.is_active == True - ).all() - - for c in cards_to_shift: - c.position -= 1 - - db.session.commit() - - return jsonify({'success': True, 'message': 'Card deleted successfully'}) - - except Exception as e: - db.session.rollback() - return jsonify({'success': False, 'message': str(e)}) - -# Kanban Column Management API -@app.route('/api/kanban/columns', methods=['POST']) -@role_required(Role.TEAM_LEADER) -@company_required -def create_kanban_column(): - try: - data = request.get_json() - board_id = int(data.get('board_id')) - - # Verify board access - board = KanbanBoard.query.filter( - KanbanBoard.id == board_id, - KanbanBoard.company_id == g.user.company_id - ).first() - - if not board: - return jsonify({'success': False, 'message': 'Board not found or access denied'}) - - name = data.get('name') - if not name: - return jsonify({'success': False, 'message': 'Column name is required'}) - - # Check if column name already exists in board - existing = KanbanColumn.query.filter_by(board_id=board_id, name=name).first() - if existing: - return jsonify({'success': False, 'message': 'Column name already exists in this board'}) - - # Calculate position (add to end) - max_position = db.session.query(func.max(KanbanColumn.position)).filter_by( - board_id=board_id, is_active=True - ).scalar() or 0 - - # Create column - column = KanbanColumn( - name=name, - description=data.get('description', ''), - position=max_position + 1, - color=data.get('color', '#6c757d'), - wip_limit=int(data.get('wip_limit')) if data.get('wip_limit') else None, - board_id=board_id - ) - - db.session.add(column) - db.session.commit() - - return jsonify({'success': True, 'message': 'Column created successfully', 'column_id': column.id}) - - except Exception as e: - db.session.rollback() - return jsonify({'success': False, 'message': str(e)}) - -@app.route('/api/kanban/columns/', methods=['PUT']) -@role_required(Role.TEAM_LEADER) -@company_required -def update_kanban_column(column_id): - try: - column = KanbanColumn.query.join(KanbanBoard).filter( - KanbanColumn.id == column_id, - KanbanBoard.company_id == g.user.company_id - ).first() - - if not column: - return jsonify({'success': False, 'message': 'Column not found or access denied'}) - - data = request.get_json() - - # Check for name conflicts (excluding current column) - if 'name' in data: - existing = KanbanColumn.query.filter( - KanbanColumn.board_id == column.board_id, - KanbanColumn.name == data['name'], - KanbanColumn.id != column_id - ).first() - if existing: - return jsonify({'success': False, 'message': 'Column name already exists in this board'}) - - # Update column fields - if 'name' in data: - column.name = data['name'] - if 'description' in data: - column.description = data['description'] - if 'color' in data: - column.color = data['color'] - if 'wip_limit' in data: - column.wip_limit = int(data['wip_limit']) if data['wip_limit'] else None - - column.updated_at = datetime.now() - db.session.commit() - - return jsonify({'success': True, 'message': 'Column updated successfully'}) - - except Exception as e: - db.session.rollback() - return jsonify({'success': False, 'message': str(e)}) - -@app.route('/api/kanban/columns//move', methods=['PUT']) -@role_required(Role.TEAM_LEADER) -@company_required -def move_kanban_column(column_id): - try: - data = request.get_json() - new_position = int(data.get('position')) - - # Verify column access - column = KanbanColumn.query.join(KanbanBoard).filter( - KanbanColumn.id == column_id, - KanbanBoard.company_id == g.user.company_id - ).first() - - if not column: - return jsonify({'success': False, 'message': 'Column not found or access denied'}) - - old_position = column.position - board_id = column.board_id - - if old_position == new_position: - return jsonify({'success': True, 'message': 'Column position unchanged'}) - - # Update positions of other columns - if old_position < new_position: - # Moving right: shift columns left - columns_to_shift = KanbanColumn.query.filter( - KanbanColumn.board_id == board_id, - KanbanColumn.position > old_position, - KanbanColumn.position <= new_position, - KanbanColumn.is_active == True, - KanbanColumn.id != column_id - ).all() - - for c in columns_to_shift: - c.position -= 1 - else: - # Moving left: shift columns right - columns_to_shift = KanbanColumn.query.filter( - KanbanColumn.board_id == board_id, - KanbanColumn.position >= new_position, - KanbanColumn.position < old_position, - KanbanColumn.is_active == True, - KanbanColumn.id != column_id - ).all() - - for c in columns_to_shift: - c.position += 1 - - # Update the moved column - column.position = new_position - column.updated_at = datetime.now() - - db.session.commit() - - return jsonify({'success': True, 'message': 'Column moved successfully'}) - - except Exception as e: - db.session.rollback() - return jsonify({'success': False, 'message': str(e)}) - -@app.route('/api/kanban/columns/', methods=['DELETE']) -@role_required(Role.TEAM_LEADER) -@company_required -def delete_kanban_column(column_id): - try: - column = KanbanColumn.query.join(KanbanBoard).filter( - KanbanColumn.id == column_id, - KanbanBoard.company_id == g.user.company_id - ).first() - - if not column: - return jsonify({'success': False, 'message': 'Column not found or access denied'}) - - # Check if column has active cards - active_cards = KanbanCard.query.filter_by(column_id=column_id, is_active=True).count() - if active_cards > 0: - return jsonify({'success': False, 'message': f'Cannot delete column with {active_cards} active cards. Move or delete cards first.'}) - - board_id = column.board_id - position = column.position - - # Soft delete the column - column.is_active = False - column.updated_at = datetime.now() - - # Shift remaining columns left - columns_to_shift = KanbanColumn.query.filter( - KanbanColumn.board_id == board_id, - KanbanColumn.position > position, - KanbanColumn.is_active == True - ).all() - - for c in columns_to_shift: - c.position -= 1 - - db.session.commit() - - return jsonify({'success': True, 'message': 'Column deleted successfully'}) - - except Exception as e: - db.session.rollback() - return jsonify({'success': False, 'message': str(e)}) - # Dashboard API Endpoints @app.route('/api/dashboard') @role_required(Role.TEAM_MEMBER) @@ -5820,28 +4927,6 @@ def get_widget_data(widget_id): '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 - company-wide boards - boards = KanbanBoard.query.filter_by( - company_id=g.user.company_id, - is_active=True - ).limit(3).all() - - 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, - 'company_name': board.company.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]: diff --git a/migrate_db.py b/migrate_db.py index 8e31419..c846492 100644 --- a/migrate_db.py +++ b/migrate_db.py @@ -15,9 +15,8 @@ try: from app import app, db 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, WidgetType, UserDashboard, DashboardWidget, - WidgetTemplate) + ProjectCategory, Task, SubTask, TaskStatus, TaskPriority, Announcement, SystemEvent, + WidgetType, UserDashboard, DashboardWidget, WidgetTemplate) from werkzeug.security import generate_password_hash FLASK_AVAILABLE = True except ImportError: @@ -74,7 +73,6 @@ def run_all_migrations(db_path=None): migrate_work_config_data(db_path) migrate_task_system(db_path) migrate_system_events(db_path) - migrate_kanban_system(db_path) migrate_dashboard_system(db_path) if FLASK_AVAILABLE: @@ -886,107 +884,6 @@ def create_all_tables(cursor): print("All tables created") -def migrate_kanban_system(db_file=None): - """Migrate to add Kanban board system.""" - db_path = get_db_path(db_file) - - print(f"Migrating Kanban 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 kanban_board table already exists - cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='kanban_board'") - if cursor.fetchone(): - print("Kanban tables already exist. Skipping migration.") - return True - - print("Creating Kanban board tables...") - - # Create kanban_board table - cursor.execute(""" - CREATE TABLE kanban_board ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name VARCHAR(100) NOT NULL, - description TEXT, - project_id INTEGER NOT NULL, - is_active BOOLEAN DEFAULT 1, - is_default BOOLEAN DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - created_by_id INTEGER NOT NULL, - FOREIGN KEY (project_id) REFERENCES project (id), - FOREIGN KEY (created_by_id) REFERENCES user (id), - UNIQUE(project_id, name) - ) - """) - - # Create kanban_column table - cursor.execute(""" - CREATE TABLE kanban_column ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name VARCHAR(100) NOT NULL, - description TEXT, - position INTEGER NOT NULL, - color VARCHAR(7) DEFAULT '#6c757d', - wip_limit INTEGER, - is_active BOOLEAN DEFAULT 1, - board_id INTEGER NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (board_id) REFERENCES kanban_board (id), - UNIQUE(board_id, name) - ) - """) - - # Create kanban_card table - cursor.execute(""" - CREATE TABLE kanban_card ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - title VARCHAR(200) NOT NULL, - description TEXT, - position INTEGER NOT NULL, - color VARCHAR(7), - is_active BOOLEAN DEFAULT 1, - column_id INTEGER NOT NULL, - task_id INTEGER, - assigned_to_id INTEGER, - due_date DATE, - completed_date DATE, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - created_by_id INTEGER NOT NULL, - FOREIGN KEY (column_id) REFERENCES kanban_column (id), - FOREIGN KEY (task_id) REFERENCES task (id), - FOREIGN KEY (assigned_to_id) REFERENCES user (id), - FOREIGN KEY (created_by_id) REFERENCES user (id) - ) - """) - - # Create indexes for better performance - cursor.execute("CREATE INDEX idx_kanban_board_project ON kanban_board(project_id)") - cursor.execute("CREATE INDEX idx_kanban_column_board ON kanban_column(board_id)") - cursor.execute("CREATE INDEX idx_kanban_card_column ON kanban_card(column_id)") - cursor.execute("CREATE INDEX idx_kanban_card_task ON kanban_card(task_id)") - cursor.execute("CREATE INDEX idx_kanban_card_assigned ON kanban_card(assigned_to_id)") - - conn.commit() - print("Kanban system migration completed successfully!") - return True - - except Exception as e: - print(f"Error during Kanban system migration: {e}") - conn.rollback() - raise - finally: - conn.close() - - def migrate_dashboard_system(db_file=None): """Migrate to add Dashboard widget system.""" db_path = get_db_path(db_file) @@ -1091,7 +988,6 @@ def migrate_dashboard_system(db_file=None): # 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 @@ -1147,8 +1043,6 @@ def main(): help='Run only basic table migrations') parser.add_argument('--system-events', '-s', action='store_true', 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') @@ -1183,8 +1077,6 @@ def main(): elif args.system_events: migrate_system_events(db_path) - elif args.kanban: - migrate_kanban_system(db_path) elif args.dashboard: migrate_dashboard_system(db_path) diff --git a/models.py b/models.py index 0f6a42a..b191a71 100644 --- a/models.py +++ b/models.py @@ -726,136 +726,6 @@ class SystemEvent(db.Model): 'health_status': 'healthy' if recent_errors == 0 else 'issues' if recent_errors < 5 else 'critical' } -# Kanban Board models -class KanbanBoard(db.Model): - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(100), nullable=False) - description = db.Column(db.Text, nullable=True) - - # Company association for multi-tenancy (removed project-specific constraint) - company_id = db.Column(db.Integer, db.ForeignKey('company.id'), nullable=False) - - # Board settings - is_active = db.Column(db.Boolean, default=True) - is_default = db.Column(db.Boolean, default=False) # Default board for company - - # Metadata - created_at = db.Column(db.DateTime, default=datetime.now) - updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now) - created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) - - # Relationships - company = db.relationship('Company', backref='kanban_boards') - created_by = db.relationship('User', foreign_keys=[created_by_id]) - columns = db.relationship('KanbanColumn', backref='board', lazy=True, cascade='all, delete-orphan', order_by='KanbanColumn.position') - - # Unique constraint per company - __table_args__ = (db.UniqueConstraint('company_id', 'name', name='uq_kanban_board_name_per_company'),) - - def __repr__(self): - return f'' - -class KanbanColumn(db.Model): - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(100), nullable=False) - description = db.Column(db.Text, nullable=True) - - # Column settings - position = db.Column(db.Integer, nullable=False) # Order in board - color = db.Column(db.String(7), default='#6c757d') # Hex color - wip_limit = db.Column(db.Integer, nullable=True) # Work in progress limit - is_active = db.Column(db.Boolean, default=True) - - # Board association - board_id = db.Column(db.Integer, db.ForeignKey('kanban_board.id'), nullable=False) - - # Metadata - created_at = db.Column(db.DateTime, default=datetime.now) - updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now) - - # Relationships - cards = db.relationship('KanbanCard', backref='column', lazy=True, cascade='all, delete-orphan', order_by='KanbanCard.position') - - # Unique constraint per board - __table_args__ = (db.UniqueConstraint('board_id', 'name', name='uq_kanban_column_name_per_board'),) - - def __repr__(self): - return f'' - - @property - def card_count(self): - """Get number of cards in this column""" - return len([card for card in self.cards if card.is_active]) - - @property - def is_over_wip_limit(self): - """Check if column is over WIP limit""" - if not self.wip_limit: - return False - return self.card_count > self.wip_limit - -class KanbanCard(db.Model): - id = db.Column(db.Integer, primary_key=True) - title = db.Column(db.String(200), nullable=False) - description = db.Column(db.Text, nullable=True) - - # Card settings - position = db.Column(db.Integer, nullable=False) # Order in column - color = db.Column(db.String(7), nullable=True) # Optional custom color - is_active = db.Column(db.Boolean, default=True) - - # Column association - column_id = db.Column(db.Integer, db.ForeignKey('kanban_column.id'), nullable=False) - - # Project context for cross-project support - project_id = db.Column(db.Integer, db.ForeignKey('project.id'), nullable=True) - - # Optional task association - task_id = db.Column(db.Integer, db.ForeignKey('task.id'), nullable=True) - - # Card assignment - assigned_to_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True) - - # Card dates - due_date = db.Column(db.Date, nullable=True) - completed_date = db.Column(db.Date, nullable=True) - - # Metadata - created_at = db.Column(db.DateTime, default=datetime.now) - updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now) - created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) - - # Relationships - project = db.relationship('Project', backref='kanban_cards') - task = db.relationship('Task', backref='kanban_cards') - assigned_to = db.relationship('User', foreign_keys=[assigned_to_id], backref='assigned_kanban_cards') - created_by = db.relationship('User', foreign_keys=[created_by_id]) - - def __repr__(self): - return f'' - - def can_user_access(self, user): - """Check if a user can access this card""" - # Check company membership first - if self.column.board.company_id != user.company_id: - return False - - # If card has project context, check project permissions - if self.project_id: - return self.project.is_user_allowed(user) - - # If no project context, allow access to anyone in the company - return True - - @property - def project_code(self): - """Get project code for display purposes""" - return self.project.code if self.project else None - - @property - def project_name(self): - """Get project name for display purposes""" - return self.project.name if self.project else None # Sprint Management System class SprintStatus(enum.Enum): @@ -984,7 +854,6 @@ class WidgetType(enum.Enum): # Task Management Widgets ASSIGNED_TASKS = "assigned_tasks" TASK_PRIORITY = "task_priority" - KANBAN_SUMMARY = "kanban_summary" TASK_TRENDS = "task_trends" # Analytics Widgets diff --git a/static/css/style.css b/static/css/style.css index 7c5683a..e6408e7 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -2041,61 +2041,6 @@ input[type="time"]::-webkit-datetime-edit { gap: 0.5rem; } -/* Kanban Common Styles */ -.kanban-container { - padding: 1rem; -} - -.kanban-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 2rem; - padding-bottom: 1rem; - border-bottom: 2px solid #dee2e6; -} - -.kanban-board { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 1rem; - margin-bottom: 2rem; -} - -.kanban-column { - background: #f8f9fa; - border-radius: 8px; - padding: 1rem; - min-height: 500px; -} - -.column-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1rem; - padding-bottom: 0.5rem; - border-bottom: 2px solid #ddd; -} - -.column-header h3 { - margin: 0; - font-size: 1.1rem; - color: #333; -} - -.column-content { - min-height: 400px; - padding: 0.5rem 0; -} - -.task-count { - background: #007bff; - color: white; - padding: 0.25rem 0.5rem; - border-radius: 12px; - font-size: 0.8rem; -} /* Card Components - Enhanced */ .card { @@ -2315,9 +2260,6 @@ input[type="time"]::-webkit-datetime-edit { grid-template-columns: repeat(2, 1fr); } - .kanban-board { - grid-template-columns: repeat(2, 1fr); - } } @media (max-width: 768px) { @@ -2339,9 +2281,6 @@ input[type="time"]::-webkit-datetime-edit { grid-template-columns: 1fr; } - .kanban-board { - grid-template-columns: 1fr; - } .filter-row { flex-direction: column; diff --git a/templates/admin_projects.html b/templates/admin_projects.html index 3bc752b..25174b3 100644 --- a/templates/admin_projects.html +++ b/templates/admin_projects.html @@ -93,7 +93,6 @@ Edit Tasks - Kanban {% if g.user.role == Role.ADMIN and project.time_entries|length == 0 %}
diff --git a/templates/confirm_company_deletion.html b/templates/confirm_company_deletion.html index f1fbde1..d488c22 100644 --- a/templates/confirm_company_deletion.html +++ b/templates/confirm_company_deletion.html @@ -169,32 +169,6 @@ {% endif %} - - {% if kanban_boards %} -
-

📋 Kanban Boards ({{ kanban_boards|length }})

- - - - - - - - - - - {% for board in kanban_boards %} - - - - - - - {% endfor %} - -
Board NameColumnsCardsCreated By
{{ board.name }}{{ board.columns|length }}{{ board.columns|map(attribute='cards')|map('length')|sum }}{{ board.created_by.username }}
-
- {% endif %} {% if time_entries_count > 0 %} diff --git a/templates/dashboard.html b/templates/dashboard.html index 6efa5ee..413db2f 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -116,15 +116,6 @@ -
-
- -
-
Kanban Summary
-

Mini kanban board for quick task management

-
-
-
@@ -535,33 +526,6 @@ color: #333; } -/* Kanban Summary Widget */ -.kanban-summary { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); - gap: 0.5rem; - padding: 0.5rem; -} - -.kanban-column-summary { - text-align: center; - padding: 0.5rem; - background: #f8f9fa; - border-radius: 4px; - border-left: 3px solid #007bff; -} - -.column-name { - font-size: 0.8rem; - color: #666; - margin-bottom: 0.25rem; -} - -.column-count { - font-size: 1.2rem; - font-weight: 600; - color: #333; -} /* Project Progress Widget */ .project-progress-list { @@ -887,7 +851,6 @@ const widgetSizes = { 'break_reminder': 'large', 'project_progress': 'wide', 'task_priority': 'medium', - 'kanban_summary': 'large', 'productivity_metrics': 'medium', 'time_distribution': 'wide', 'team_activity': 'wide', @@ -1034,8 +997,6 @@ function renderWidgetContent(widget) { return renderAssignedTasksWidget(widget); case 'task_priority': return renderTaskPriorityWidget(widget); - case 'kanban_summary': - return renderKanbanSummaryWidget(widget); case 'productivity_metrics': return renderProductivityMetricsWidget(widget); case 'time_distribution': @@ -1154,14 +1115,6 @@ function renderTaskPriorityWidget(widget) { `; } -function renderKanbanSummaryWidget(widget) { - loadWidgetData(widget.id); - return ` -
-
Loading kanban...
-
- `; -} function renderProductivityMetricsWidget(widget) { loadWidgetData(widget.id); @@ -1294,7 +1247,6 @@ function showConfigurationModal() { 'project_progress': 'Project Progress', 'assigned_tasks': 'Assigned Tasks', 'task_priority': 'Task Priority', - 'kanban_summary': 'Kanban Summary', 'productivity_metrics': 'Productivity Metrics', 'time_distribution': 'Time Distribution' }; @@ -1536,9 +1488,6 @@ function updateWidgetContent(widgetId, data) { } else if (data.priority_tasks) { // Update task priority widget updateTaskPriorityWidget(widgetId, data.priority_tasks); - } else if (data.kanban_boards) { - // Update kanban summary widget - updateKanbanSummaryWidget(widgetId, data.kanban_boards); } else if (data.project_progress) { // Update project progress widget updateProjectProgressWidget(widgetId, data.project_progress); @@ -1698,26 +1647,6 @@ function updateTaskPriorityWidget(widgetId, priorityTasks) { contentElement.innerHTML = tasksHtml; } -function updateKanbanSummaryWidget(widgetId, kanbanBoards) { - const contentElement = document.getElementById(`widget-content-${widgetId}`); - if (!contentElement) return; - - let kanbanHtml = '
'; - kanbanBoards.forEach(board => { - kanbanHtml += ` -
-
${board.name}
-
${board.project_name}
-
- ${board.total_cards} cards - ${board.columns} columns -
-
- `; - }); - kanbanHtml += '
'; - contentElement.innerHTML = kanbanHtml; -} function updateProjectProgressWidget(widgetId, projectProgress) { const contentElement = document.getElementById(`widget-content-${widgetId}`); diff --git a/templates/kanban_overview.html b/templates/kanban_overview.html deleted file mode 100644 index 3b361c1..0000000 --- a/templates/kanban_overview.html +++ /dev/null @@ -1,695 +0,0 @@ -{% extends "layout.html" %} - -{% block content %} -
- - - {% if create_board %} - -
-
-

Create New Kanban Board

- -
- -
- - -
-
- - -
-
- -
-
- -
- -
- {% endif %} - - {% if boards %} -
- {% for board in boards %} -
-
-
-

- {{ board.name }} - {% if board.is_default %} - Default - {% endif %} -

- {% if board.description %} -

{{ board.description }}

- {% endif %} -
-
- Open Board - {% if g.user.role.value in ['Team Leader', 'Supervisor', 'Administrator', 'System Administrator'] %} - - {% endif %} -
-
- -
-
-
- {{ board.columns|length }} - Columns -
-
- {% set board_cards = 0 %} - {% for column in board.columns %} - {% set board_cards = board_cards + column.cards|selectattr('is_active')|list|length %} - {% endfor %} - {{ board_cards }} - Cards -
-
- {% set project_contexts = [] %} - {% for column in board.columns %} - {% for card in column.cards %} - {% if card.is_active and card.project_id and card.project_id not in project_contexts %} - {% set _ = project_contexts.append(card.project_id) %} - {% endif %} - {% endfor %} - {% endfor %} - {{ project_contexts|length }} - Projects -
-
-
- - -
- {% if board.columns %} -
Board Preview
-
- {% for column in board.columns %} - {% if loop.index <= 4 %} -
-
- {{ column.name }} - {{ column.cards|selectattr('is_active')|list|length }} -
-
- {% set active_cards = column.cards|selectattr('is_active')|list %} - {% for card in active_cards %} - {% if loop.index <= 3 %} -
-
- {% if card.project %} - [{{ card.project.code }}] - {% endif %} - {% if card.title|length > 25 %} - {{ card.title|truncate(25, True) }} - {% else %} - {{ card.title }} - {% endif %} -
- {% if card.assigned_to %} -
{{ card.assigned_to.username }}
- {% endif %} -
- {% endif %} - {% endfor %} - {% if active_cards|length > 3 %} -
+{{ active_cards|length - 3 }} more
- {% endif %} -
-
- {% endif %} - {% endfor %} - {% if board.columns|length > 4 %} -
- +{{ board.columns|length - 4 }} more columns -
- {% endif %} -
- {% endif %} -
-
- {% endfor %} -
- - -
-
-

{{ boards|length }}

-

Company Boards

-
-
- {% set total_cards = 0 %} - {% for board in boards %} - {% for column in board.columns %} - {% set total_cards = total_cards + column.cards|selectattr('is_active')|list|length %} - {% endfor %} - {% endfor %} -

{{ total_cards }}

-

Total Cards

-
-
- {% set total_projects = [] %} - {% for board in boards %} - {% for column in board.columns %} - {% for card in column.cards %} - {% if card.is_active and card.project_id and card.project_id not in total_projects %} - {% set _ = total_projects.append(card.project_id) %} - {% endif %} - {% endfor %} - {% endfor %} - {% endfor %} -

{{ total_projects|length }}

-

Active Projects

-
-
- - {% else %} - -
-
-
📋
-

No Kanban Boards Yet

-

Create unified boards to organize tasks from any project.

-
-

Getting Started:

-
    -
  1. Click the "Create Board" button
  2. -
  3. Set up columns for your workflow (To Do, In Progress, Done)
  4. -
  5. Add cards from any project to organize your work
  6. -
-
- {% if g.user.role.value in ['Team Leader', 'Supervisor', 'Administrator', 'System Administrator'] %} - - {% endif %} -
-
- {% endif %} -
- - - - - -{% endblock %} \ No newline at end of file diff --git a/templates/project_kanban.html b/templates/project_kanban.html deleted file mode 100644 index 51a3863..0000000 --- a/templates/project_kanban.html +++ /dev/null @@ -1,1377 +0,0 @@ -{% extends "layout.html" %} - -{% block content %} -
-
-
-

Unified Kanban Board

-

Organize tasks from any project on shared boards

- {% if project %} -
- Project Context: - {{ project.code }} - {{ project.name }} - {% if project.category %} - - {{ project.category.icon or '📁' }} {{ project.category.name }} - - {% endif %} -
- {% endif %} -
-
- {% if g.user.role.value in ['Team Leader', 'Supervisor', 'Administrator', 'System Administrator'] %} - - - {% endif %} - {% if project %} - Back to Projects - Task View - {% else %} - Back to Kanban Overview - {% endif %} -
-
- - - {% if boards %} -
- - -
- {% endif %} - - - - - - {% if not boards %} -
-
-

No Kanban Boards Created Yet

-

Create your first Kanban board to start organizing tasks visually.

- {% if g.user.role.value in ['Team Leader', 'Supervisor', 'Administrator', 'System Administrator'] %} - - {% endif %} -
-
- {% endif %} -
- - - - - - - - - - - - - - - -{% endblock %} \ No newline at end of file diff --git a/templates/sprint_management.html b/templates/sprint_management.html index f03553a..4259b3c 100644 --- a/templates/sprint_management.html +++ b/templates/sprint_management.html @@ -109,11 +109,21 @@
- +
+ + + +
+
- +
+ + + +
+
@@ -227,9 +237,6 @@ margin-bottom: 2rem; } -.sprint-card { - /* Sprint card inherits from .management-card */ -} .sprint-card-header { display: flex; @@ -294,6 +301,69 @@ +/* Hybrid Date Input Styles */ +.hybrid-date-input { + position: relative; + display: flex; + align-items: center; + gap: 0.25rem; +} + +.hybrid-date-input.compact { + display: inline-flex; +} + +.date-input-native { + position: absolute; + left: 0; + top: 0; + width: calc(100% - 35px); /* Leave space for calendar button */ + height: 100%; + opacity: 0; + cursor: pointer; + z-index: 2; + pointer-events: auto; +} + +.date-input-formatted { + flex: 1; + padding: 0.5rem; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; + background: white; + position: relative; + z-index: 2; +} + +.calendar-picker-btn { + background: #f8f9fa; + border: 1px solid #ddd; + border-radius: 4px; + padding: 0.5rem; + cursor: pointer; + font-size: 14px; + transition: background-color 0.2s; + z-index: 3; + position: relative; +} + +.calendar-picker-btn:hover { + background: #e9ecef; +} + +.calendar-picker-btn.compact { + padding: 0.375rem 0.5rem; + font-size: 12px; +} + +.hybrid-date-input.compact .date-input-formatted { + padding: 0.375rem; + font-size: 12px; + width: 100px; + flex: none; +} + @media (max-width: 768px) { .sprint-metrics { grid-template-columns: repeat(2, 1fr); @@ -302,6 +372,206 @@