mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-11 23:10:35 -05:00
feat: Implement comprehensive module system with visibility controls
- Add centralized module registry system (ModuleRegistry) for managing module metadata, dependencies, and visibility across the application - Create module helper utilities with decorators (@module_enabled) and helper functions for route protection and template access - Add database migration (092) to add missing module visibility flags to settings and users tables for granular control - Extend Settings and User models with additional module visibility flags for CRM, Finance, Tools, and Advanced features - Implement admin module management UI for system-wide module enable/disable controls - Add module checks to routes (calendar, contacts, deals, expenses, invoices, leads, custom_reports) to enforce visibility rules - Update scheduled report service and report templates to respect module visibility settings - Bump version to 4.8.0 in setup.py - Add comprehensive documentation for module integration planning and implementation analysis
This commit is contained in:
@@ -453,6 +453,9 @@ def create_app(config=None):
|
||||
app.jinja_env.globals.update(_=_gettext, ngettext=_ngettext)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Add Python built-ins that are useful in templates
|
||||
app.jinja_env.globals.update(getattr=getattr)
|
||||
|
||||
# Log effective database URL (mask password)
|
||||
db_url = app.config.get("SQLALCHEMY_DATABASE_URI", "")
|
||||
@@ -1235,6 +1238,11 @@ def create_app(config=None):
|
||||
|
||||
register_template_filters(app)
|
||||
|
||||
# Initialize module registry and helpers
|
||||
from app.utils.module_helpers import init_module_helpers
|
||||
|
||||
init_module_helpers(app)
|
||||
|
||||
# Register CLI commands
|
||||
from app.utils.cli import register_cli_commands
|
||||
|
||||
|
||||
@@ -199,7 +199,10 @@ class CalendarEvent(db.Model):
|
||||
logger.info(f"Querying time entries for user {user_id}")
|
||||
time_entries = (
|
||||
TimeEntry.query.filter(
|
||||
TimeEntry.user_id == user_id, TimeEntry.start_time >= start_date, TimeEntry.start_time <= end_date
|
||||
TimeEntry.user_id == user_id,
|
||||
TimeEntry.start_time >= start_date,
|
||||
TimeEntry.start_time <= end_date,
|
||||
TimeEntry.end_time.isnot(None), # Only include completed entries (CalDAV entries have end_time)
|
||||
)
|
||||
.order_by(TimeEntry.start_time)
|
||||
.all()
|
||||
@@ -218,8 +221,10 @@ class CalendarEvent(db.Model):
|
||||
"taskId": entry.task_id,
|
||||
"notes": entry.notes,
|
||||
"type": "time_entry",
|
||||
"source": entry.source, # Include source to identify CalDAV entries (source="auto")
|
||||
}
|
||||
for entry in time_entries
|
||||
if entry.start_time and entry.end_time # Ensure both times are set for proper display
|
||||
]
|
||||
else:
|
||||
print(f"MODEL - Not including time entries (include_time_entries=False)")
|
||||
|
||||
@@ -78,6 +78,30 @@ class Settings(db.Model):
|
||||
|
||||
# Tools & Data section
|
||||
ui_allow_tools = db.Column(db.Boolean, default=True, nullable=False)
|
||||
ui_allow_integrations = db.Column(db.Boolean, default=True, nullable=False)
|
||||
ui_allow_import_export = db.Column(db.Boolean, default=True, nullable=False)
|
||||
ui_allow_saved_filters = db.Column(db.Boolean, default=True, nullable=False)
|
||||
|
||||
# CRM section (additional)
|
||||
ui_allow_contacts = db.Column(db.Boolean, default=True, nullable=False)
|
||||
ui_allow_deals = db.Column(db.Boolean, default=True, nullable=False)
|
||||
ui_allow_leads = db.Column(db.Boolean, default=True, nullable=False)
|
||||
|
||||
# Finance section (additional)
|
||||
ui_allow_invoices = db.Column(db.Boolean, default=True, nullable=False)
|
||||
ui_allow_expenses = db.Column(db.Boolean, default=True, nullable=False)
|
||||
|
||||
# Time Tracking section (additional)
|
||||
ui_allow_time_entry_templates = db.Column(db.Boolean, default=True, nullable=False)
|
||||
|
||||
# Advanced features
|
||||
ui_allow_workflows = db.Column(db.Boolean, default=True, nullable=False)
|
||||
ui_allow_time_approvals = db.Column(db.Boolean, default=True, nullable=False)
|
||||
ui_allow_activity_feed = db.Column(db.Boolean, default=True, nullable=False)
|
||||
ui_allow_recurring_tasks = db.Column(db.Boolean, default=True, nullable=False)
|
||||
ui_allow_team_chat = db.Column(db.Boolean, default=True, nullable=False)
|
||||
ui_allow_client_portal = db.Column(db.Boolean, default=True, nullable=False)
|
||||
ui_allow_kiosk = db.Column(db.Boolean, default=True, nullable=False)
|
||||
|
||||
# Kiosk mode settings
|
||||
kiosk_mode_enabled = db.Column(db.Boolean, default=False, nullable=False)
|
||||
@@ -436,6 +460,22 @@ class Settings(db.Model):
|
||||
"ui_allow_inventory": getattr(self, "ui_allow_inventory", True),
|
||||
"ui_allow_analytics": getattr(self, "ui_allow_analytics", True),
|
||||
"ui_allow_tools": getattr(self, "ui_allow_tools", True),
|
||||
"ui_allow_integrations": getattr(self, "ui_allow_integrations", True),
|
||||
"ui_allow_import_export": getattr(self, "ui_allow_import_export", True),
|
||||
"ui_allow_saved_filters": getattr(self, "ui_allow_saved_filters", True),
|
||||
"ui_allow_contacts": getattr(self, "ui_allow_contacts", True),
|
||||
"ui_allow_deals": getattr(self, "ui_allow_deals", True),
|
||||
"ui_allow_leads": getattr(self, "ui_allow_leads", True),
|
||||
"ui_allow_invoices": getattr(self, "ui_allow_invoices", True),
|
||||
"ui_allow_expenses": getattr(self, "ui_allow_expenses", True),
|
||||
"ui_allow_time_entry_templates": getattr(self, "ui_allow_time_entry_templates", True),
|
||||
"ui_allow_workflows": getattr(self, "ui_allow_workflows", True),
|
||||
"ui_allow_time_approvals": getattr(self, "ui_allow_time_approvals", True),
|
||||
"ui_allow_activity_feed": getattr(self, "ui_allow_activity_feed", True),
|
||||
"ui_allow_recurring_tasks": getattr(self, "ui_allow_recurring_tasks", True),
|
||||
"ui_allow_team_chat": getattr(self, "ui_allow_team_chat", True),
|
||||
"ui_allow_client_portal": getattr(self, "ui_allow_client_portal", True),
|
||||
"ui_allow_kiosk": getattr(self, "ui_allow_kiosk", True),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -91,6 +91,30 @@ class User(UserMixin, db.Model):
|
||||
|
||||
# Tools & Data section
|
||||
ui_show_tools = db.Column(db.Boolean, default=True, nullable=False) # Show/hide Tools & Data section
|
||||
ui_show_integrations = db.Column(db.Boolean, default=True, nullable=False) # Show/hide Integrations
|
||||
ui_show_import_export = db.Column(db.Boolean, default=True, nullable=False) # Show/hide Import/Export
|
||||
ui_show_saved_filters = db.Column(db.Boolean, default=True, nullable=False) # Show/hide Saved Filters
|
||||
|
||||
# CRM section (additional)
|
||||
ui_show_contacts = db.Column(db.Boolean, default=True, nullable=False) # Show/hide Contacts
|
||||
ui_show_deals = db.Column(db.Boolean, default=True, nullable=False) # Show/hide Deals
|
||||
ui_show_leads = db.Column(db.Boolean, default=True, nullable=False) # Show/hide Leads
|
||||
|
||||
# Finance section (additional)
|
||||
ui_show_invoices = db.Column(db.Boolean, default=True, nullable=False) # Show/hide Invoices
|
||||
ui_show_expenses = db.Column(db.Boolean, default=True, nullable=False) # Show/hide Expenses
|
||||
|
||||
# Time Tracking section (additional)
|
||||
ui_show_time_entry_templates = db.Column(db.Boolean, default=True, nullable=False) # Show/hide Time Entry Templates
|
||||
|
||||
# Advanced features
|
||||
ui_show_workflows = db.Column(db.Boolean, default=True, nullable=False) # Show/hide Workflows
|
||||
ui_show_time_approvals = db.Column(db.Boolean, default=True, nullable=False) # Show/hide Time Approvals
|
||||
ui_show_activity_feed = db.Column(db.Boolean, default=True, nullable=False) # Show/hide Activity Feed
|
||||
ui_show_recurring_tasks = db.Column(db.Boolean, default=True, nullable=False) # Show/hide Recurring Tasks
|
||||
ui_show_team_chat = db.Column(db.Boolean, default=True, nullable=False) # Show/hide Team Chat
|
||||
ui_show_client_portal = db.Column(db.Boolean, default=True, nullable=False) # Show/hide Client Portal
|
||||
ui_show_kiosk = db.Column(db.Boolean, default=True, nullable=False) # Show/hide Kiosk Mode
|
||||
|
||||
# Relationships
|
||||
time_entries = db.relationship("TimeEntry", backref="user", lazy="dynamic", cascade="all, delete-orphan")
|
||||
|
||||
@@ -429,6 +429,56 @@ def clear_cache():
|
||||
return render_template("admin/clear_cache.html")
|
||||
|
||||
|
||||
@admin_bp.route("/admin/modules", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@admin_or_permission_required("manage_settings")
|
||||
def manage_modules():
|
||||
"""Manage module visibility settings"""
|
||||
from app.utils.module_registry import ModuleRegistry, ModuleCategory
|
||||
|
||||
# Initialize registry
|
||||
ModuleRegistry.initialize_defaults()
|
||||
|
||||
settings_obj = Settings.get_settings()
|
||||
|
||||
if request.method == "POST":
|
||||
# Update all module flags dynamically
|
||||
updated_count = 0
|
||||
for module_id, module in ModuleRegistry.get_all().items():
|
||||
if module.settings_flag:
|
||||
flag_name = module.settings_flag
|
||||
if hasattr(settings_obj, flag_name):
|
||||
new_value = request.form.get(flag_name) == "on"
|
||||
old_value = getattr(settings_obj, flag_name, True)
|
||||
if new_value != old_value:
|
||||
setattr(settings_obj, flag_name, new_value)
|
||||
updated_count += 1
|
||||
|
||||
if updated_count > 0:
|
||||
if not safe_commit("admin_update_module_settings"):
|
||||
flash(_("Could not update module settings due to a database error."), "error")
|
||||
else:
|
||||
flash(_("Module settings updated successfully"), "success")
|
||||
else:
|
||||
flash(_("No changes to save"), "info")
|
||||
|
||||
return redirect(url_for("admin.manage_modules"))
|
||||
|
||||
# Group modules by category for display
|
||||
modules_by_category = {}
|
||||
for category in ModuleCategory:
|
||||
modules = ModuleRegistry.get_by_category(category)
|
||||
if modules: # Only include categories with modules
|
||||
modules_by_category[category] = modules
|
||||
|
||||
return render_template(
|
||||
"admin/modules.html",
|
||||
modules_by_category=modules_by_category,
|
||||
settings=settings_obj,
|
||||
ModuleCategory=ModuleCategory,
|
||||
)
|
||||
|
||||
|
||||
@admin_bp.route("/admin/settings", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@admin_or_permission_required("manage_settings")
|
||||
@@ -557,6 +607,46 @@ def settings():
|
||||
# Tools & Data
|
||||
if hasattr(settings_obj, "ui_allow_tools"):
|
||||
settings_obj.ui_allow_tools = request.form.get("ui_allow_tools") == "on"
|
||||
if hasattr(settings_obj, "ui_allow_integrations"):
|
||||
settings_obj.ui_allow_integrations = request.form.get("ui_allow_integrations") == "on"
|
||||
if hasattr(settings_obj, "ui_allow_import_export"):
|
||||
settings_obj.ui_allow_import_export = request.form.get("ui_allow_import_export") == "on"
|
||||
if hasattr(settings_obj, "ui_allow_saved_filters"):
|
||||
settings_obj.ui_allow_saved_filters = request.form.get("ui_allow_saved_filters") == "on"
|
||||
|
||||
# CRM (additional)
|
||||
if hasattr(settings_obj, "ui_allow_contacts"):
|
||||
settings_obj.ui_allow_contacts = request.form.get("ui_allow_contacts") == "on"
|
||||
if hasattr(settings_obj, "ui_allow_deals"):
|
||||
settings_obj.ui_allow_deals = request.form.get("ui_allow_deals") == "on"
|
||||
if hasattr(settings_obj, "ui_allow_leads"):
|
||||
settings_obj.ui_allow_leads = request.form.get("ui_allow_leads") == "on"
|
||||
|
||||
# Finance (additional)
|
||||
if hasattr(settings_obj, "ui_allow_invoices"):
|
||||
settings_obj.ui_allow_invoices = request.form.get("ui_allow_invoices") == "on"
|
||||
if hasattr(settings_obj, "ui_allow_expenses"):
|
||||
settings_obj.ui_allow_expenses = request.form.get("ui_allow_expenses") == "on"
|
||||
|
||||
# Time Tracking (additional)
|
||||
if hasattr(settings_obj, "ui_allow_time_entry_templates"):
|
||||
settings_obj.ui_allow_time_entry_templates = request.form.get("ui_allow_time_entry_templates") == "on"
|
||||
|
||||
# Advanced features
|
||||
if hasattr(settings_obj, "ui_allow_workflows"):
|
||||
settings_obj.ui_allow_workflows = request.form.get("ui_allow_workflows") == "on"
|
||||
if hasattr(settings_obj, "ui_allow_time_approvals"):
|
||||
settings_obj.ui_allow_time_approvals = request.form.get("ui_allow_time_approvals") == "on"
|
||||
if hasattr(settings_obj, "ui_allow_activity_feed"):
|
||||
settings_obj.ui_allow_activity_feed = request.form.get("ui_allow_activity_feed") == "on"
|
||||
if hasattr(settings_obj, "ui_allow_recurring_tasks"):
|
||||
settings_obj.ui_allow_recurring_tasks = request.form.get("ui_allow_recurring_tasks") == "on"
|
||||
if hasattr(settings_obj, "ui_allow_team_chat"):
|
||||
settings_obj.ui_allow_team_chat = request.form.get("ui_allow_team_chat") == "on"
|
||||
if hasattr(settings_obj, "ui_allow_client_portal"):
|
||||
settings_obj.ui_allow_client_portal = request.form.get("ui_allow_client_portal") == "on"
|
||||
if hasattr(settings_obj, "ui_allow_kiosk"):
|
||||
settings_obj.ui_allow_kiosk = request.form.get("ui_allow_kiosk") == "on"
|
||||
except Exception as e:
|
||||
# Log any errors but don't fail silently
|
||||
import logging
|
||||
|
||||
@@ -8,6 +8,7 @@ from datetime import datetime, timedelta
|
||||
from app.utils.db import safe_commit
|
||||
from app.utils.timezone import now_in_app_timezone
|
||||
from app.utils.permissions import check_permission
|
||||
from app.utils.module_helpers import module_enabled
|
||||
import os
|
||||
|
||||
calendar_bp = Blueprint("calendar", __name__)
|
||||
@@ -15,6 +16,7 @@ calendar_bp = Blueprint("calendar", __name__)
|
||||
|
||||
@calendar_bp.route("/calendar")
|
||||
@login_required
|
||||
@module_enabled("calendar")
|
||||
def view_calendar():
|
||||
"""Display the calendar view with events, tasks, and time entries"""
|
||||
view_type = request.args.get("view", "month") # day, week, month
|
||||
@@ -40,6 +42,7 @@ def view_calendar():
|
||||
|
||||
@calendar_bp.route("/api/calendar/events")
|
||||
@login_required
|
||||
@module_enabled("calendar")
|
||||
def get_events():
|
||||
"""API endpoint to fetch calendar events for a date range"""
|
||||
start_str = request.args.get("start")
|
||||
@@ -102,6 +105,7 @@ def get_events():
|
||||
|
||||
@calendar_bp.route("/api/calendar/events", methods=["POST"])
|
||||
@login_required
|
||||
@module_enabled("calendar")
|
||||
def create_event():
|
||||
"""Create a new calendar event"""
|
||||
data = request.get_json()
|
||||
@@ -160,6 +164,7 @@ def create_event():
|
||||
|
||||
@calendar_bp.route("/api/calendar/events/<int:event_id>", methods=["GET"])
|
||||
@login_required
|
||||
@module_enabled("calendar")
|
||||
def get_event(event_id):
|
||||
"""Get a specific calendar event"""
|
||||
event = CalendarEvent.query.get_or_404(event_id)
|
||||
@@ -411,6 +416,7 @@ def edit_event(event_id):
|
||||
|
||||
@calendar_bp.route("/calendar/integrations")
|
||||
@login_required
|
||||
@module_enabled("calendar")
|
||||
def list_integrations():
|
||||
"""List calendar integrations - redirect to main integrations page"""
|
||||
# Redirect to main integrations page to avoid duplication
|
||||
|
||||
@@ -7,6 +7,7 @@ from app import db
|
||||
from app.models import Contact, Client, ContactCommunication
|
||||
from app.utils.db import safe_commit
|
||||
from app.utils.timezone import parse_local_datetime
|
||||
from app.utils.module_helpers import module_enabled
|
||||
from datetime import datetime
|
||||
|
||||
contacts_bp = Blueprint("contacts", __name__)
|
||||
@@ -14,6 +15,7 @@ contacts_bp = Blueprint("contacts", __name__)
|
||||
|
||||
@contacts_bp.route("/clients/<int:client_id>/contacts")
|
||||
@login_required
|
||||
@module_enabled("contacts")
|
||||
def list_contacts(client_id):
|
||||
"""List all contacts for a client"""
|
||||
client = Client.query.get_or_404(client_id)
|
||||
|
||||
@@ -20,6 +20,10 @@ custom_reports_bp = Blueprint("custom_reports", __name__)
|
||||
@login_required
|
||||
def report_builder(view_id=None):
|
||||
"""Custom report builder page. If view_id is provided, load that saved view for editing."""
|
||||
# Also check for view_id in query parameters as fallback
|
||||
if not view_id:
|
||||
view_id = request.args.get('view_id', type=int)
|
||||
|
||||
saved_views = SavedReportView.query.filter_by(owner_id=current_user.id).all()
|
||||
|
||||
# Load saved view if editing
|
||||
@@ -96,7 +100,8 @@ def save_report_view():
|
||||
|
||||
# Extract iterative report generation settings
|
||||
iterative_report_generation = data.get("iterative_report_generation", False)
|
||||
iterative_custom_field_name = data.get("iterative_custom_field_name", "").strip() or None
|
||||
iterative_custom_field_name_raw = data.get("iterative_custom_field_name")
|
||||
iterative_custom_field_name = (iterative_custom_field_name_raw or "").strip() or None if iterative_custom_field_name_raw else None
|
||||
|
||||
# If view_id is provided, update existing report
|
||||
if view_id:
|
||||
@@ -279,13 +284,15 @@ def generate_report_data(config, user_id=None):
|
||||
|
||||
if unpaid_only:
|
||||
# Use unpaid hours service
|
||||
# Only filter by user_id if explicitly specified in filters, not by default
|
||||
# This allows admins to see all unpaid entries globally
|
||||
unpaid_service = UnpaidHoursService()
|
||||
entries = unpaid_service.get_unpaid_time_entries(
|
||||
start_date=start_dt,
|
||||
end_date=end_dt,
|
||||
project_id=filters.get("project_id"),
|
||||
client_id=filters.get("client_id"),
|
||||
user_id=filters.get("user_id") or user_id,
|
||||
user_id=filters.get("user_id"), # Only use if explicitly specified
|
||||
custom_field_filter=custom_field_filter,
|
||||
)
|
||||
else:
|
||||
@@ -422,7 +429,7 @@ def list_saved_views():
|
||||
@custom_reports_bp.route("/reports/builder/<int:view_id>/edit", methods=["GET"])
|
||||
@login_required
|
||||
def edit_saved_view(view_id):
|
||||
"""Edit a saved report view - redirects to builder with view_id."""
|
||||
"""Edit a saved report view - redirects to builder with view_id in path."""
|
||||
saved_view = SavedReportView.query.get_or_404(view_id)
|
||||
|
||||
# Check permission
|
||||
@@ -430,7 +437,7 @@ def edit_saved_view(view_id):
|
||||
flash(_("You do not have permission to edit this report."), "error")
|
||||
return redirect(url_for("custom_reports.list_saved_views"))
|
||||
|
||||
# Redirect to builder with edit mode
|
||||
# Redirect to builder with edit mode using the /edit path pattern
|
||||
return redirect(url_for("custom_reports.report_builder", view_id=view_id))
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from app import db
|
||||
from app.models import Deal, DealActivity, Client, Contact, Lead, Quote, Project
|
||||
from app.utils.db import safe_commit
|
||||
from app.utils.timezone import parse_local_datetime
|
||||
from app.utils.module_helpers import module_enabled
|
||||
from datetime import datetime, date
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
@@ -18,6 +19,7 @@ PIPELINE_STAGES = ["prospecting", "qualification", "proposal", "negotiation", "c
|
||||
|
||||
@deals_bp.route("/deals")
|
||||
@login_required
|
||||
@module_enabled("deals")
|
||||
def list_deals():
|
||||
"""List all deals with pipeline view"""
|
||||
status = request.args.get("status", "open")
|
||||
|
||||
@@ -6,6 +6,7 @@ from app.models import Expense, Project, Client, User
|
||||
from datetime import datetime, date, timedelta
|
||||
from decimal import Decimal
|
||||
from app.utils.db import safe_commit
|
||||
from app.utils.module_helpers import module_enabled
|
||||
from app.utils.ocr import scan_receipt, get_suggested_expense_data, is_ocr_available
|
||||
import csv
|
||||
import io
|
||||
@@ -27,6 +28,7 @@ def allowed_file(filename):
|
||||
|
||||
@expenses_bp.route("/expenses")
|
||||
@login_required
|
||||
@module_enabled("expenses")
|
||||
def list_expenses():
|
||||
"""List all expenses with filters"""
|
||||
# Track page view
|
||||
|
||||
@@ -2,6 +2,7 @@ from flask import Blueprint, render_template, request, redirect, url_for, flash,
|
||||
from flask_babel import gettext as _
|
||||
from flask_login import login_required, current_user
|
||||
from app import db, log_event, track_event
|
||||
from app.utils.module_helpers import module_enabled
|
||||
from app.models import (
|
||||
User,
|
||||
Project,
|
||||
@@ -36,6 +37,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@invoices_bp.route("/invoices")
|
||||
@login_required
|
||||
@module_enabled("invoices")
|
||||
def list_invoices():
|
||||
"""List all invoices - REFACTORED to use service layer with eager loading"""
|
||||
# Track invoice page viewed
|
||||
|
||||
@@ -7,6 +7,7 @@ from app import db
|
||||
from app.models import Lead, LeadActivity, Client, Deal
|
||||
from app.utils.db import safe_commit
|
||||
from app.utils.timezone import parse_local_datetime
|
||||
from app.utils.module_helpers import module_enabled
|
||||
from datetime import datetime
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
@@ -18,6 +19,7 @@ LEAD_STATUSES = ["new", "contacted", "qualified", "converted", "lost"]
|
||||
|
||||
@leads_bp.route("/leads")
|
||||
@login_required
|
||||
@module_enabled("leads")
|
||||
def list_leads():
|
||||
"""List all leads"""
|
||||
status = request.args.get("status", "")
|
||||
|
||||
@@ -116,8 +116,19 @@ class ScheduledReportService:
|
||||
config = {}
|
||||
|
||||
# Check if we should split by custom field
|
||||
if schedule.split_by_salesman and schedule.salesman_field_name:
|
||||
return self._generate_and_send_custom_field_reports(schedule, saved_view, config)
|
||||
# Use iterative_report_generation from saved_view if enabled, otherwise check schedule.split_by_salesman
|
||||
if saved_view.iterative_report_generation and saved_view.iterative_custom_field_name:
|
||||
# Use iterative report generation from saved view
|
||||
return self._generate_and_send_custom_field_reports(
|
||||
schedule, saved_view, config,
|
||||
custom_field_name=saved_view.iterative_custom_field_name
|
||||
)
|
||||
elif schedule.split_by_salesman and schedule.salesman_field_name:
|
||||
# Use legacy split_by_salesman from schedule
|
||||
return self._generate_and_send_custom_field_reports(
|
||||
schedule, saved_view, config,
|
||||
custom_field_name=schedule.salesman_field_name
|
||||
)
|
||||
|
||||
# Validate config before proceeding
|
||||
if not isinstance(config, dict):
|
||||
@@ -309,7 +320,8 @@ class ScheduledReportService:
|
||||
return {"success": False, "message": f"Error deleting schedule: {str(e)}"}
|
||||
|
||||
def _generate_and_send_custom_field_reports(
|
||||
self, schedule: ReportEmailSchedule, saved_view: SavedReportView, config: Dict[str, Any]
|
||||
self, schedule: ReportEmailSchedule, saved_view: SavedReportView, config: Dict[str, Any],
|
||||
custom_field_name: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate and send reports split by custom field value.
|
||||
@@ -317,6 +329,12 @@ class ScheduledReportService:
|
||||
This generates individual reports for each unique value of the specified
|
||||
custom field and sends them to the configured recipients.
|
||||
|
||||
Args:
|
||||
schedule: ReportEmailSchedule object
|
||||
saved_view: SavedReportView object
|
||||
config: Report configuration dict
|
||||
custom_field_name: Custom field name to iterate over (if None, uses schedule.salesman_field_name or "salesman")
|
||||
|
||||
Returns:
|
||||
dict with 'success', 'message', and 'sent_count' keys
|
||||
"""
|
||||
@@ -327,8 +345,9 @@ class ScheduledReportService:
|
||||
import app.routes.custom_reports as custom_reports_module
|
||||
generate_report_data = custom_reports_module.generate_report_data
|
||||
|
||||
# Get custom field name
|
||||
custom_field_name = schedule.salesman_field_name or "salesman"
|
||||
# Get custom field name - use provided parameter, or fall back to schedule or default
|
||||
if not custom_field_name:
|
||||
custom_field_name = schedule.salesman_field_name or saved_view.iterative_custom_field_name or "salesman"
|
||||
|
||||
# Get date range from config or use defaults
|
||||
# Config can have filters at top level or nested
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ _('Module Management') }} - {{ _('Admin') }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-3xl font-bold text-text-light dark:text-text-dark mb-2">{{ _('Module Management') }}</h1>
|
||||
<p class="text-text-muted-light dark:text-text-muted-dark">{{ _('Enable or disable modules and features system-wide. Disabled modules will be hidden from all users.') }}</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ url_for('admin.manage_modules') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="space-y-6">
|
||||
{% for category, modules in modules_by_category.items() %}
|
||||
<div class="bg-card-light dark:bg-card-dark rounded-lg shadow-sm border border-border-light dark:border-border-dark p-6">
|
||||
<h2 class="text-xl font-semibold text-text-light dark:text-text-dark mb-4">
|
||||
{% if category == ModuleCategory.TIME_TRACKING %}
|
||||
<i class="fas fa-clock mr-2"></i>{{ _('Time Tracking') }}
|
||||
{% elif category == ModuleCategory.PROJECT_MANAGEMENT %}
|
||||
<i class="fas fa-project-diagram mr-2"></i>{{ _('Project Management') }}
|
||||
{% elif category == ModuleCategory.CRM %}
|
||||
<i class="fas fa-handshake mr-2"></i>{{ _('CRM') }}
|
||||
{% elif category == ModuleCategory.FINANCE %}
|
||||
<i class="fas fa-dollar-sign mr-2"></i>{{ _('Finance & Expenses') }}
|
||||
{% elif category == ModuleCategory.INVENTORY %}
|
||||
<i class="fas fa-boxes mr-2"></i>{{ _('Inventory') }}
|
||||
{% elif category == ModuleCategory.ANALYTICS %}
|
||||
<i class="fas fa-chart-line mr-2"></i>{{ _('Analytics') }}
|
||||
{% elif category == ModuleCategory.TOOLS %}
|
||||
<i class="fas fa-tools mr-2"></i>{{ _('Tools & Data') }}
|
||||
{% elif category == ModuleCategory.ADVANCED %}
|
||||
<i class="fas fa-rocket mr-2"></i>{{ _('Advanced Features') }}
|
||||
{% else %}
|
||||
{{ category.value|title }}
|
||||
{% endif %}
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{% for module in modules %}
|
||||
<div class="flex items-start space-x-3 p-3 rounded-lg {% if module.category == ModuleCategory.CORE %}bg-background-light dark:bg-background-dark{% else %}hover:bg-background-light dark:hover:bg-background-dark{% endif %}">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center space-x-2">
|
||||
{% if module.icon %}
|
||||
<i class="fas {{ module.icon }} text-primary"></i>
|
||||
{% endif %}
|
||||
<label class="font-medium text-text-light dark:text-text-dark cursor-pointer flex items-center">
|
||||
{% if module.category == ModuleCategory.CORE %}
|
||||
<input type="checkbox"
|
||||
name="{{ module.settings_flag }}"
|
||||
{% if module.settings_flag and getattr(settings, module.settings_flag, True) %}checked{% endif %}
|
||||
disabled
|
||||
class="mr-2 rounded border-border-light dark:border-border-dark text-primary focus:ring-primary">
|
||||
<span class="text-sm text-text-muted-light dark:text-text-muted-dark">({{ _('Core') }})</span>
|
||||
{% else %}
|
||||
<input type="checkbox"
|
||||
name="{{ module.settings_flag }}"
|
||||
{% if module.settings_flag and getattr(settings, module.settings_flag, True) %}checked{% endif %}
|
||||
class="mr-2 rounded border-border-light dark:border-border-dark text-primary focus:ring-primary">
|
||||
{% endif %}
|
||||
<span>{{ module.name }}</span>
|
||||
</label>
|
||||
</div>
|
||||
{% if module.description %}
|
||||
<p class="text-sm text-text-muted-light dark:text-text-muted-dark mt-1 ml-6">{{ module.description }}</p>
|
||||
{% endif %}
|
||||
{% if module.dependencies %}
|
||||
<div class="mt-2 ml-6">
|
||||
<p class="text-xs text-text-muted-light dark:text-text-muted-dark">
|
||||
<i class="fas fa-link mr-1"></i>{{ _('Depends on') }}:
|
||||
{% for dep_id in module.dependencies %}
|
||||
{% set dep_module = None %}
|
||||
{% for cat, mods in modules_by_category.items() %}
|
||||
{% for mod in mods %}
|
||||
{% if mod.id == dep_id %}
|
||||
{% set dep_module = mod %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% if dep_module %}
|
||||
{{ dep_module.name }}{% if not loop.last %}, {% endif %}
|
||||
{% else %}
|
||||
{{ dep_id }}{% if not loop.last %}, {% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end space-x-4">
|
||||
<a href="{{ url_for('admin.settings') }}" class="px-4 py-2 rounded-lg border border-border-light dark:border-border-dark text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark">
|
||||
{{ _('Cancel') }}
|
||||
</a>
|
||||
<button type="submit" class="px-4 py-2 rounded-lg bg-primary text-white hover:bg-primary-dark">
|
||||
<i class="fas fa-save mr-2"></i>{{ _('Save Changes') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<div class="flex items-start">
|
||||
<i class="fas fa-info-circle text-blue-600 dark:text-blue-400 mt-1 mr-3"></i>
|
||||
<div class="text-sm text-blue-800 dark:text-blue-200">
|
||||
<p class="font-semibold mb-1">{{ _('Note') }}:</p>
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
<li>{{ _('Core modules cannot be disabled as they are required for the application to function.') }}</li>
|
||||
<li>{{ _('Disabling a module will hide it from all users, but existing data will be preserved.') }}</li>
|
||||
<li>{{ _('If a module has dependencies, those must be enabled for the module to work properly.') }}</li>
|
||||
<li>{{ _('Individual users can further customize their view in their profile settings.') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -626,20 +626,32 @@ document.getElementById('iterativeReportGeneration')?.addEventListener('change',
|
||||
document.getElementById('saveForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const name = document.getElementById('reportName').value.trim();
|
||||
const scope = document.getElementById('reportScope').value;
|
||||
const submitButton = e.target.querySelector('button[type="submit"]');
|
||||
const originalButtonText = submitButton.innerHTML;
|
||||
|
||||
// Validate name
|
||||
if (!name) {
|
||||
alert('{{ _("Please enter a report name") }}');
|
||||
return;
|
||||
}
|
||||
// Disable button and show loading state
|
||||
submitButton.disabled = true;
|
||||
submitButton.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>{{ _("Saving...") }}';
|
||||
|
||||
// Validate data source is selected
|
||||
if (!reportConfig.data_source) {
|
||||
alert('{{ _("Please select a data source first") }}');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const name = document.getElementById('reportName').value.trim();
|
||||
const scope = document.getElementById('reportScope').value;
|
||||
|
||||
// Validate name
|
||||
if (!name) {
|
||||
alert('{{ _("Please enter a report name") }}');
|
||||
submitButton.disabled = false;
|
||||
submitButton.innerHTML = originalButtonText;
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate data source is selected
|
||||
if (!reportConfig.data_source) {
|
||||
alert('{{ _("Please select a data source first") }}');
|
||||
submitButton.disabled = false;
|
||||
submitButton.innerHTML = originalButtonText;
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect filters
|
||||
reportConfig.filters = {
|
||||
@@ -744,6 +756,9 @@ document.getElementById('saveForm').addEventListener('submit', async (e) => {
|
||||
} else {
|
||||
alert(errorMsg);
|
||||
}
|
||||
// Re-enable button on error
|
||||
submitButton.disabled = false;
|
||||
submitButton.innerHTML = originalButtonText;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving report:', error);
|
||||
@@ -755,6 +770,9 @@ document.getElementById('saveForm').addEventListener('submit', async (e) => {
|
||||
} else {
|
||||
alert(errorMsg);
|
||||
}
|
||||
// Re-enable button on error
|
||||
submitButton.disabled = false;
|
||||
submitButton.innerHTML = originalButtonText;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
<a href="{{ url_for('custom_reports.view_custom_report', view_id=view.id) }}" class="text-blue-600 hover:text-blue-800" title="{{ _('View') }}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="{{ url_for('custom_reports.report_builder', view_id=view.id) }}" class="text-green-600 hover:text-green-800" title="{{ _('Edit') }}">
|
||||
<a href="{{ url_for('custom_reports.edit_saved_view', view_id=view.id) }}" class="text-green-600 hover:text-green-800" title="{{ _('Edit') }}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<form method="POST" action="{{ url_for('custom_reports.delete_saved_view', view_id=view.id) }}" class="inline" onsubmit="return confirm('{{ _('Are you sure you want to delete this report view?') }}')">
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
Module Helper Utilities
|
||||
|
||||
Provides decorators and helper functions for checking module availability
|
||||
and protecting routes based on module flags.
|
||||
"""
|
||||
from functools import wraps
|
||||
from flask import abort, redirect, url_for, flash, current_app
|
||||
from flask_login import current_user
|
||||
from flask_babel import gettext as _
|
||||
from app.models import Settings
|
||||
from app.utils.module_registry import ModuleRegistry
|
||||
|
||||
|
||||
def module_enabled(module_id: str, redirect_to: str = None):
|
||||
"""
|
||||
Decorator to require a module to be enabled for a route.
|
||||
|
||||
Args:
|
||||
module_id: The module ID to check
|
||||
redirect_to: Optional route name to redirect to if module is disabled
|
||||
|
||||
Usage:
|
||||
@module_enabled("calendar")
|
||||
def view_calendar():
|
||||
return render_template("calendar/view.html")
|
||||
"""
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not current_user.is_authenticated:
|
||||
if redirect_to:
|
||||
return redirect(url_for(redirect_to))
|
||||
abort(403)
|
||||
|
||||
settings = Settings.get_settings()
|
||||
if not ModuleRegistry.is_enabled(module_id, settings, current_user):
|
||||
if current_user.is_admin:
|
||||
module = ModuleRegistry.get(module_id)
|
||||
module_name = module.name if module else module_id
|
||||
flash(
|
||||
_("Module '%(module)s' is disabled. Enable it in Settings.", module=module_name),
|
||||
"warning"
|
||||
)
|
||||
if redirect_to:
|
||||
return redirect(url_for(redirect_to))
|
||||
return redirect(url_for('admin.settings'))
|
||||
abort(403)
|
||||
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
|
||||
def is_module_enabled(module_id: str) -> bool:
|
||||
"""
|
||||
Check if a module is enabled for the current user.
|
||||
|
||||
Args:
|
||||
module_id: The module ID to check
|
||||
|
||||
Returns:
|
||||
True if module is enabled, False otherwise
|
||||
"""
|
||||
if not current_user.is_authenticated:
|
||||
return False
|
||||
|
||||
try:
|
||||
settings = Settings.get_settings()
|
||||
return ModuleRegistry.is_enabled(module_id, settings, current_user)
|
||||
except Exception:
|
||||
# If we can't check, default to False for safety
|
||||
return False
|
||||
|
||||
|
||||
def get_enabled_modules(category=None):
|
||||
"""
|
||||
Get all enabled modules, optionally filtered by category.
|
||||
|
||||
Args:
|
||||
category: Optional ModuleCategory to filter by
|
||||
|
||||
Returns:
|
||||
List of enabled ModuleDefinition objects
|
||||
"""
|
||||
if not current_user.is_authenticated:
|
||||
return []
|
||||
|
||||
try:
|
||||
settings = Settings.get_settings()
|
||||
modules = ModuleRegistry.get_enabled_modules(settings, current_user)
|
||||
|
||||
if category:
|
||||
from app.utils.module_registry import ModuleCategory
|
||||
if isinstance(category, str):
|
||||
try:
|
||||
category = ModuleCategory(category)
|
||||
except ValueError:
|
||||
return []
|
||||
modules = [m for m in modules if m.category == category]
|
||||
|
||||
return modules
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def init_module_helpers(app):
|
||||
"""
|
||||
Initialize module helper functions for use in templates and routes.
|
||||
|
||||
This should be called during app initialization.
|
||||
"""
|
||||
# Initialize module registry
|
||||
ModuleRegistry.initialize_defaults()
|
||||
|
||||
@app.context_processor
|
||||
def inject_module_helpers():
|
||||
"""Make module helpers available in templates"""
|
||||
from app.utils.module_registry import ModuleCategory
|
||||
return {
|
||||
"is_module_enabled": is_module_enabled,
|
||||
"get_enabled_modules": get_enabled_modules,
|
||||
"get_modules_by_category": lambda cat: ModuleRegistry.get_by_category(cat),
|
||||
"ModuleCategory": ModuleCategory,
|
||||
}
|
||||
|
||||
# Also make it available as a global function
|
||||
app.jinja_env.globals['is_module_enabled'] = is_module_enabled
|
||||
app.jinja_env.globals['get_enabled_modules'] = get_enabled_modules
|
||||
|
||||
@@ -0,0 +1,704 @@
|
||||
"""
|
||||
Module Registry System
|
||||
|
||||
Centralized registry for managing module metadata, dependencies, and visibility.
|
||||
"""
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional, Dict
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ModuleCategory(Enum):
|
||||
"""Module categories for organization"""
|
||||
CORE = "core"
|
||||
TIME_TRACKING = "time_tracking"
|
||||
PROJECT_MANAGEMENT = "project_management"
|
||||
CRM = "crm"
|
||||
FINANCE = "finance"
|
||||
INVENTORY = "inventory"
|
||||
ANALYTICS = "analytics"
|
||||
TOOLS = "tools"
|
||||
ADMIN = "admin"
|
||||
ADVANCED = "advanced"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModuleDefinition:
|
||||
"""Definition of a module with its metadata and configuration"""
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
category: ModuleCategory
|
||||
blueprint_name: str
|
||||
default_enabled: bool = True
|
||||
requires_admin: bool = False
|
||||
dependencies: List[str] = field(default_factory=list) # Module IDs this depends on
|
||||
settings_flag: Optional[str] = None # Settings.ui_allow_* field name
|
||||
user_flag: Optional[str] = None # User.ui_show_* field name
|
||||
routes: List[str] = field(default_factory=list) # Route endpoints
|
||||
icon: Optional[str] = None # FontAwesome icon class
|
||||
order: int = 0 # Display order in navigation
|
||||
|
||||
def __post_init__(self):
|
||||
"""Validate and normalize module definition"""
|
||||
if self.dependencies is None:
|
||||
self.dependencies = []
|
||||
if self.routes is None:
|
||||
self.routes = []
|
||||
|
||||
|
||||
class ModuleRegistry:
|
||||
"""Centralized registry for all application modules"""
|
||||
|
||||
_modules: Dict[str, ModuleDefinition] = {}
|
||||
_initialized: bool = False
|
||||
|
||||
@classmethod
|
||||
def register(cls, module: ModuleDefinition):
|
||||
"""Register a module definition"""
|
||||
cls._modules[module.id] = module
|
||||
|
||||
@classmethod
|
||||
def get(cls, module_id: str) -> Optional[ModuleDefinition]:
|
||||
"""Get a module definition by ID"""
|
||||
return cls._modules.get(module_id)
|
||||
|
||||
@classmethod
|
||||
def get_all(cls) -> Dict[str, ModuleDefinition]:
|
||||
"""Get all registered modules"""
|
||||
return cls._modules.copy()
|
||||
|
||||
@classmethod
|
||||
def get_by_category(cls, category: ModuleCategory) -> List[ModuleDefinition]:
|
||||
"""Get all modules in a specific category, sorted by order"""
|
||||
modules = [m for m in cls._modules.values() if m.category == category]
|
||||
return sorted(modules, key=lambda m: m.order)
|
||||
|
||||
@classmethod
|
||||
def is_enabled(cls, module_id: str, settings=None, user=None) -> bool:
|
||||
"""
|
||||
Check if a module is enabled for a user.
|
||||
|
||||
Args:
|
||||
module_id: The module ID to check
|
||||
settings: Settings instance (optional, will fetch if not provided)
|
||||
user: User instance (optional, will use current_user if not provided)
|
||||
|
||||
Returns:
|
||||
True if module is enabled, False otherwise
|
||||
"""
|
||||
module = cls.get(module_id)
|
||||
if not module:
|
||||
return False
|
||||
|
||||
# Core modules are always enabled
|
||||
if module.category == ModuleCategory.CORE:
|
||||
return True
|
||||
|
||||
# Admin-only modules require admin access
|
||||
if module.requires_admin:
|
||||
if user is None:
|
||||
from flask_login import current_user
|
||||
user = current_user
|
||||
if not user or not getattr(user, 'is_authenticated', False):
|
||||
return False
|
||||
if not getattr(user, 'is_admin', False):
|
||||
return False
|
||||
|
||||
# Fetch settings if not provided
|
||||
if settings is None:
|
||||
try:
|
||||
from app.models import Settings
|
||||
settings = Settings.get_settings()
|
||||
except Exception:
|
||||
# If we can't get settings, use defaults
|
||||
return module.default_enabled
|
||||
|
||||
# Check system-wide setting
|
||||
if module.settings_flag:
|
||||
flag_value = getattr(settings, module.settings_flag, None)
|
||||
if flag_value is False:
|
||||
return False
|
||||
|
||||
# Fetch user if not provided
|
||||
if user is None:
|
||||
from flask_login import current_user
|
||||
user = current_user
|
||||
|
||||
# Check user-specific setting
|
||||
if module.user_flag and user and getattr(user, 'is_authenticated', False):
|
||||
flag_value = getattr(user, module.user_flag, None)
|
||||
if flag_value is False:
|
||||
return False
|
||||
|
||||
# Check dependencies recursively
|
||||
for dep_id in module.dependencies:
|
||||
if not cls.is_enabled(dep_id, settings, user):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def get_enabled_modules(cls, settings=None, user=None) -> List[ModuleDefinition]:
|
||||
"""Get all enabled modules for a user"""
|
||||
enabled = []
|
||||
for module in cls._modules.values():
|
||||
if cls.is_enabled(module.id, settings, user):
|
||||
enabled.append(module)
|
||||
return sorted(enabled, key=lambda m: (m.category.value, m.order))
|
||||
|
||||
@classmethod
|
||||
def initialize_defaults(cls):
|
||||
"""Initialize the registry with all default module definitions"""
|
||||
if cls._initialized:
|
||||
return
|
||||
|
||||
# Core modules (always enabled)
|
||||
cls.register(ModuleDefinition(
|
||||
id="auth",
|
||||
name="Authentication",
|
||||
description="User authentication and profile management",
|
||||
category=ModuleCategory.CORE,
|
||||
blueprint_name="auth",
|
||||
default_enabled=True,
|
||||
icon="fa-user-circle",
|
||||
order=0
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="main",
|
||||
name="Dashboard",
|
||||
description="Main dashboard",
|
||||
category=ModuleCategory.CORE,
|
||||
blueprint_name="main",
|
||||
default_enabled=True,
|
||||
icon="fa-tachometer-alt",
|
||||
order=1
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="projects",
|
||||
name="Projects",
|
||||
description="Project management",
|
||||
category=ModuleCategory.CORE,
|
||||
blueprint_name="projects",
|
||||
default_enabled=True,
|
||||
icon="fa-folder",
|
||||
order=2
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="timer",
|
||||
name="Time Tracking",
|
||||
description="Time entry and timer management",
|
||||
category=ModuleCategory.CORE,
|
||||
blueprint_name="timer",
|
||||
default_enabled=True,
|
||||
icon="fa-clock",
|
||||
order=3
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="tasks",
|
||||
name="Tasks",
|
||||
description="Task management",
|
||||
category=ModuleCategory.CORE,
|
||||
blueprint_name="tasks",
|
||||
default_enabled=True,
|
||||
dependencies=["projects"],
|
||||
icon="fa-tasks",
|
||||
order=4
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="clients",
|
||||
name="Clients",
|
||||
description="Client management",
|
||||
category=ModuleCategory.CORE,
|
||||
blueprint_name="clients",
|
||||
default_enabled=True,
|
||||
icon="fa-users",
|
||||
order=5
|
||||
))
|
||||
|
||||
# Time Tracking Features
|
||||
cls.register(ModuleDefinition(
|
||||
id="calendar",
|
||||
name="Calendar",
|
||||
description="Calendar view and integrations",
|
||||
category=ModuleCategory.TIME_TRACKING,
|
||||
blueprint_name="calendar",
|
||||
default_enabled=True,
|
||||
settings_flag="ui_allow_calendar",
|
||||
user_flag="ui_show_calendar",
|
||||
icon="fa-calendar-alt",
|
||||
order=10
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="project_templates",
|
||||
name="Project Templates",
|
||||
description="Project template system",
|
||||
category=ModuleCategory.PROJECT_MANAGEMENT,
|
||||
blueprint_name="project_templates",
|
||||
default_enabled=True,
|
||||
dependencies=["projects"],
|
||||
settings_flag="ui_allow_project_templates",
|
||||
user_flag="ui_show_project_templates",
|
||||
icon="fa-layer-group",
|
||||
order=11
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="gantt",
|
||||
name="Gantt Chart",
|
||||
description="Gantt chart visualization",
|
||||
category=ModuleCategory.PROJECT_MANAGEMENT,
|
||||
blueprint_name="gantt",
|
||||
default_enabled=True,
|
||||
dependencies=["tasks"],
|
||||
settings_flag="ui_allow_gantt_chart",
|
||||
user_flag="ui_show_gantt_chart",
|
||||
icon="fa-project-diagram",
|
||||
order=12
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="kanban",
|
||||
name="Kanban Board",
|
||||
description="Kanban task board",
|
||||
category=ModuleCategory.PROJECT_MANAGEMENT,
|
||||
blueprint_name="kanban",
|
||||
default_enabled=True,
|
||||
dependencies=["tasks"],
|
||||
settings_flag="ui_allow_kanban_board",
|
||||
user_flag="ui_show_kanban_board",
|
||||
icon="fa-columns",
|
||||
order=13
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="weekly_goals",
|
||||
name="Weekly Goals",
|
||||
description="Weekly time goals tracking",
|
||||
category=ModuleCategory.TIME_TRACKING,
|
||||
blueprint_name="weekly_goals",
|
||||
default_enabled=True,
|
||||
settings_flag="ui_allow_weekly_goals",
|
||||
user_flag="ui_show_weekly_goals",
|
||||
icon="fa-bullseye",
|
||||
order=14
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="issues",
|
||||
name="Issues",
|
||||
description="Issue and bug tracking",
|
||||
category=ModuleCategory.PROJECT_MANAGEMENT,
|
||||
blueprint_name="issues",
|
||||
default_enabled=True,
|
||||
settings_flag="ui_allow_issues",
|
||||
user_flag="ui_show_issues",
|
||||
icon="fa-bug",
|
||||
order=15
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="time_entry_templates",
|
||||
name="Time Entry Templates",
|
||||
description="Reusable time entry templates",
|
||||
category=ModuleCategory.TIME_TRACKING,
|
||||
blueprint_name="time_entry_templates",
|
||||
default_enabled=True,
|
||||
settings_flag="ui_allow_time_entry_templates",
|
||||
user_flag="ui_show_time_entry_templates",
|
||||
icon="fa-clipboard-list",
|
||||
order=16
|
||||
))
|
||||
|
||||
# CRM Features
|
||||
cls.register(ModuleDefinition(
|
||||
id="quotes",
|
||||
name="Quotes",
|
||||
description="Quote management",
|
||||
category=ModuleCategory.CRM,
|
||||
blueprint_name="quotes",
|
||||
default_enabled=True,
|
||||
dependencies=["clients"],
|
||||
settings_flag="ui_allow_quotes",
|
||||
user_flag="ui_show_quotes",
|
||||
icon="fa-file-contract",
|
||||
order=20
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="contacts",
|
||||
name="Contacts",
|
||||
description="Contact management",
|
||||
category=ModuleCategory.CRM,
|
||||
blueprint_name="contacts",
|
||||
default_enabled=True,
|
||||
dependencies=["clients"],
|
||||
settings_flag="ui_allow_contacts",
|
||||
user_flag="ui_show_contacts",
|
||||
icon="fa-address-book",
|
||||
order=21
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="deals",
|
||||
name="Deals",
|
||||
description="Deal pipeline management",
|
||||
category=ModuleCategory.CRM,
|
||||
blueprint_name="deals",
|
||||
default_enabled=True,
|
||||
dependencies=["clients"],
|
||||
settings_flag="ui_allow_deals",
|
||||
user_flag="ui_show_deals",
|
||||
icon="fa-handshake",
|
||||
order=22
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="leads",
|
||||
name="Leads",
|
||||
description="Lead management",
|
||||
category=ModuleCategory.CRM,
|
||||
blueprint_name="leads",
|
||||
default_enabled=True,
|
||||
dependencies=["clients"],
|
||||
settings_flag="ui_allow_leads",
|
||||
user_flag="ui_show_leads",
|
||||
icon="fa-user-tag",
|
||||
order=23
|
||||
))
|
||||
|
||||
# Finance & Expenses
|
||||
cls.register(ModuleDefinition(
|
||||
id="reports",
|
||||
name="Reports",
|
||||
description="Standard reports",
|
||||
category=ModuleCategory.FINANCE,
|
||||
blueprint_name="reports",
|
||||
default_enabled=True,
|
||||
settings_flag="ui_allow_reports",
|
||||
user_flag="ui_show_reports",
|
||||
icon="fa-chart-bar",
|
||||
order=30
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="custom_reports",
|
||||
name="Report Builder",
|
||||
description="Custom report builder",
|
||||
category=ModuleCategory.FINANCE,
|
||||
blueprint_name="custom_reports",
|
||||
default_enabled=True,
|
||||
settings_flag="ui_allow_report_builder",
|
||||
user_flag="ui_show_report_builder",
|
||||
icon="fa-magic",
|
||||
order=31
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="scheduled_reports",
|
||||
name="Scheduled Reports",
|
||||
description="Automated report scheduling",
|
||||
category=ModuleCategory.FINANCE,
|
||||
blueprint_name="scheduled_reports",
|
||||
default_enabled=True,
|
||||
settings_flag="ui_allow_scheduled_reports",
|
||||
user_flag="ui_show_scheduled_reports",
|
||||
icon="fa-clock",
|
||||
order=32
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="invoices",
|
||||
name="Invoices",
|
||||
description="Invoice management",
|
||||
category=ModuleCategory.FINANCE,
|
||||
blueprint_name="invoices",
|
||||
default_enabled=True,
|
||||
dependencies=["projects"],
|
||||
settings_flag="ui_allow_invoices",
|
||||
user_flag="ui_show_invoices",
|
||||
icon="fa-file-invoice",
|
||||
order=33
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="invoice_approvals",
|
||||
name="Invoice Approvals",
|
||||
description="Invoice approval workflow",
|
||||
category=ModuleCategory.FINANCE,
|
||||
blueprint_name="invoice_approvals",
|
||||
default_enabled=True,
|
||||
dependencies=["invoices"],
|
||||
settings_flag="ui_allow_invoice_approvals",
|
||||
user_flag="ui_show_invoice_approvals",
|
||||
icon="fa-check-circle",
|
||||
order=34
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="recurring_invoices",
|
||||
name="Recurring Invoices",
|
||||
description="Recurring invoice management",
|
||||
category=ModuleCategory.FINANCE,
|
||||
blueprint_name="recurring_invoices",
|
||||
default_enabled=True,
|
||||
dependencies=["invoices"],
|
||||
settings_flag="ui_allow_recurring_invoices",
|
||||
user_flag="ui_show_recurring_invoices",
|
||||
icon="fa-sync-alt",
|
||||
order=35
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="payments",
|
||||
name="Payments",
|
||||
description="Payment tracking",
|
||||
category=ModuleCategory.FINANCE,
|
||||
blueprint_name="payments",
|
||||
default_enabled=True,
|
||||
dependencies=["invoices"],
|
||||
settings_flag="ui_allow_payments",
|
||||
user_flag="ui_show_payments",
|
||||
icon="fa-credit-card",
|
||||
order=36
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="payment_gateways",
|
||||
name="Payment Gateways",
|
||||
description="Payment gateway integration",
|
||||
category=ModuleCategory.FINANCE,
|
||||
blueprint_name="payment_gateways",
|
||||
default_enabled=True,
|
||||
dependencies=["payments"],
|
||||
settings_flag="ui_allow_payment_gateways",
|
||||
user_flag="ui_show_payment_gateways",
|
||||
icon="fa-credit-card",
|
||||
order=37
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="expenses",
|
||||
name="Expenses",
|
||||
description="Expense tracking",
|
||||
category=ModuleCategory.FINANCE,
|
||||
blueprint_name="expenses",
|
||||
default_enabled=True,
|
||||
dependencies=["projects"],
|
||||
settings_flag="ui_allow_expenses",
|
||||
user_flag="ui_show_expenses",
|
||||
icon="fa-receipt",
|
||||
order=38
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="mileage",
|
||||
name="Mileage",
|
||||
description="Mileage tracking",
|
||||
category=ModuleCategory.FINANCE,
|
||||
blueprint_name="mileage",
|
||||
default_enabled=True,
|
||||
settings_flag="ui_allow_mileage",
|
||||
user_flag="ui_show_mileage",
|
||||
icon="fa-car",
|
||||
order=39
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="per_diem",
|
||||
name="Per Diem",
|
||||
description="Per diem expense tracking",
|
||||
category=ModuleCategory.FINANCE,
|
||||
blueprint_name="per_diem",
|
||||
default_enabled=True,
|
||||
settings_flag="ui_allow_per_diem",
|
||||
user_flag="ui_show_per_diem",
|
||||
icon="fa-utensils",
|
||||
order=40
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="budget_alerts",
|
||||
name="Budget Alerts",
|
||||
description="Project budget monitoring",
|
||||
category=ModuleCategory.FINANCE,
|
||||
blueprint_name="budget_alerts",
|
||||
default_enabled=True,
|
||||
dependencies=["projects"],
|
||||
settings_flag="ui_allow_budget_alerts",
|
||||
user_flag="ui_show_budget_alerts",
|
||||
icon="fa-exclamation-triangle",
|
||||
order=41
|
||||
))
|
||||
|
||||
# Inventory
|
||||
cls.register(ModuleDefinition(
|
||||
id="inventory",
|
||||
name="Inventory",
|
||||
description="Inventory management",
|
||||
category=ModuleCategory.INVENTORY,
|
||||
blueprint_name="inventory",
|
||||
default_enabled=True,
|
||||
settings_flag="ui_allow_inventory",
|
||||
user_flag="ui_show_inventory",
|
||||
icon="fa-boxes",
|
||||
order=50
|
||||
))
|
||||
|
||||
# Analytics
|
||||
cls.register(ModuleDefinition(
|
||||
id="analytics",
|
||||
name="Analytics",
|
||||
description="Analytics dashboard",
|
||||
category=ModuleCategory.ANALYTICS,
|
||||
blueprint_name="analytics",
|
||||
default_enabled=True,
|
||||
settings_flag="ui_allow_analytics",
|
||||
user_flag="ui_show_analytics",
|
||||
icon="fa-chart-line",
|
||||
order=60
|
||||
))
|
||||
|
||||
# Tools & Data
|
||||
cls.register(ModuleDefinition(
|
||||
id="integrations",
|
||||
name="Integrations",
|
||||
description="External integrations",
|
||||
category=ModuleCategory.TOOLS,
|
||||
blueprint_name="integrations",
|
||||
default_enabled=True,
|
||||
settings_flag="ui_allow_integrations",
|
||||
user_flag="ui_show_integrations",
|
||||
icon="fa-plug",
|
||||
order=70
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="import_export",
|
||||
name="Import/Export",
|
||||
description="Data import and export",
|
||||
category=ModuleCategory.TOOLS,
|
||||
blueprint_name="import_export",
|
||||
default_enabled=True,
|
||||
settings_flag="ui_allow_import_export",
|
||||
user_flag="ui_show_import_export",
|
||||
icon="fa-exchange-alt",
|
||||
order=71
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="saved_filters",
|
||||
name="Saved Filters",
|
||||
description="Saved filter management",
|
||||
category=ModuleCategory.TOOLS,
|
||||
blueprint_name="saved_filters",
|
||||
default_enabled=True,
|
||||
settings_flag="ui_allow_saved_filters",
|
||||
user_flag="ui_show_saved_filters",
|
||||
icon="fa-filter",
|
||||
order=72
|
||||
))
|
||||
|
||||
# Advanced Features
|
||||
cls.register(ModuleDefinition(
|
||||
id="workflows",
|
||||
name="Workflows",
|
||||
description="Automation workflows",
|
||||
category=ModuleCategory.ADVANCED,
|
||||
blueprint_name="workflows",
|
||||
default_enabled=True,
|
||||
settings_flag="ui_allow_workflows",
|
||||
user_flag="ui_show_workflows",
|
||||
icon="fa-sitemap",
|
||||
order=80
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="time_approvals",
|
||||
name="Time Approvals",
|
||||
description="Time entry approval workflow",
|
||||
category=ModuleCategory.ADVANCED,
|
||||
blueprint_name="time_approvals",
|
||||
default_enabled=True,
|
||||
dependencies=["timer"],
|
||||
settings_flag="ui_allow_time_approvals",
|
||||
user_flag="ui_show_time_approvals",
|
||||
icon="fa-check-double",
|
||||
order=81
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="activity_feed",
|
||||
name="Activity Feed",
|
||||
description="Activity stream",
|
||||
category=ModuleCategory.ADVANCED,
|
||||
blueprint_name="activity_feed",
|
||||
default_enabled=True,
|
||||
settings_flag="ui_allow_activity_feed",
|
||||
user_flag="ui_show_activity_feed",
|
||||
icon="fa-stream",
|
||||
order=82
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="recurring_tasks",
|
||||
name="Recurring Tasks",
|
||||
description="Automated recurring tasks",
|
||||
category=ModuleCategory.ADVANCED,
|
||||
blueprint_name="recurring_tasks",
|
||||
default_enabled=True,
|
||||
dependencies=["tasks"],
|
||||
settings_flag="ui_allow_recurring_tasks",
|
||||
user_flag="ui_show_recurring_tasks",
|
||||
icon="fa-redo",
|
||||
order=83
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="team_chat",
|
||||
name="Team Chat",
|
||||
description="Team messaging",
|
||||
category=ModuleCategory.ADVANCED,
|
||||
blueprint_name="team_chat",
|
||||
default_enabled=True,
|
||||
settings_flag="ui_allow_team_chat",
|
||||
user_flag="ui_show_team_chat",
|
||||
icon="fa-comments",
|
||||
order=84
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="client_portal",
|
||||
name="Client Portal",
|
||||
description="Client-facing portal",
|
||||
category=ModuleCategory.ADVANCED,
|
||||
blueprint_name="client_portal",
|
||||
default_enabled=True,
|
||||
dependencies=["clients"],
|
||||
settings_flag="ui_allow_client_portal",
|
||||
user_flag="ui_show_client_portal",
|
||||
icon="fa-door-open",
|
||||
order=85
|
||||
))
|
||||
|
||||
cls.register(ModuleDefinition(
|
||||
id="kiosk",
|
||||
name="Kiosk Mode",
|
||||
description="Kiosk interface",
|
||||
category=ModuleCategory.ADVANCED,
|
||||
blueprint_name="kiosk",
|
||||
default_enabled=True,
|
||||
settings_flag="ui_allow_kiosk",
|
||||
user_flag="ui_show_kiosk",
|
||||
icon="fa-desktop",
|
||||
order=86
|
||||
))
|
||||
|
||||
cls._initialized = True
|
||||
|
||||
@@ -0,0 +1,504 @@
|
||||
# Incomplete Implementations Analysis
|
||||
|
||||
**Date:** 2025-01-27
|
||||
**Version:** 4.7.1
|
||||
**Status:** Comprehensive Analysis
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This document provides a comprehensive analysis of incomplete implementations, missing features, and areas requiring additional work in the TimeTracker application. The analysis covers both backend (Python/Flask) and frontend (JavaScript) implementations.
|
||||
|
||||
**Key Findings:**
|
||||
- **268 pass statements** found in backend code (indicating incomplete implementations)
|
||||
- **4 NotImplementedError** exceptions in integrations
|
||||
- **Multiple incomplete integrations** with placeholder implementations
|
||||
- **Frontend features** with TODO comments and incomplete functionality
|
||||
- **Missing API endpoints** for some features
|
||||
- **Incomplete permission checks** in several routes
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Backend Incomplete Implementations](#backend-incomplete-implementations)
|
||||
2. [Frontend Incomplete Implementations](#frontend-incomplete-implementations)
|
||||
3. [Integration Incomplete Implementations](#integration-incomplete-implementations)
|
||||
4. [Missing Features](#missing-features)
|
||||
5. [API Endpoints Missing](#api-endpoints-missing)
|
||||
6. [Priority Recommendations](#priority-recommendations)
|
||||
|
||||
---
|
||||
|
||||
## Backend Incomplete Implementations
|
||||
|
||||
### 1. Routes with `pass` Statements
|
||||
|
||||
#### 1.1 Issues Module (`app/routes/issues.py`)
|
||||
- **Line 60**: Permission filtering for non-admin users is incomplete
|
||||
```python
|
||||
if not current_user.is_admin:
|
||||
# Get user's accessible client IDs (through projects they have access to)
|
||||
# For simplicity, we'll show all issues but filter in template if needed
|
||||
# In a real implementation, you'd want to filter by user permissions here
|
||||
pass
|
||||
```
|
||||
**Impact:** Non-admin users may see issues they shouldn't have access to.
|
||||
**Priority:** High
|
||||
|
||||
#### 1.2 Push Notifications (`app/routes/push_notifications.py`)
|
||||
- **Line 27**: Push subscription storage incomplete
|
||||
```python
|
||||
if not hasattr(current_user, "push_subscription"):
|
||||
# Add push_subscription field to User model if needed
|
||||
pass
|
||||
```
|
||||
**Impact:** Push notifications feature is not fully functional.
|
||||
**Priority:** Medium
|
||||
|
||||
#### 1.3 Expenses Module (`app/routes/expenses.py`)
|
||||
- Multiple `pass` statements in exception handlers (lines 82, 89, 150, 156, 270, 471, 516, 575, 797, 803, 896, 902, 990, 996, 1058, 1250)
|
||||
- **Impact:** Error handling may not be comprehensive.
|
||||
**Priority:** Low-Medium
|
||||
|
||||
#### 1.4 Deals Module (`app/routes/deals.py`)
|
||||
- Lines 45, 77, 114, 193, 244, 272: Exception handlers with `pass`
|
||||
- **Impact:** Error handling incomplete.
|
||||
**Priority:** Low
|
||||
|
||||
#### 1.5 Leads Module (`app/routes/leads.py`)
|
||||
- Lines 45, 84, 142, 258: Exception handlers with `pass`
|
||||
- **Impact:** Error handling incomplete.
|
||||
**Priority:** Low
|
||||
|
||||
#### 1.6 Admin Module (`app/routes/admin.py`)
|
||||
- Multiple `pass` statements (lines 115, 554, 657, 764, 880, 886, 972, 978, 1091, 1187, 1466, 1917, 2030)
|
||||
- **Impact:** Various admin features may have incomplete error handling.
|
||||
**Priority:** Medium
|
||||
|
||||
#### 1.7 Calendar Module (`app/routes/calendar.py`)
|
||||
- Lines 379, 385: Exception handlers with `pass`
|
||||
- **Impact:** Calendar error handling incomplete.
|
||||
**Priority:** Low
|
||||
|
||||
#### 1.8 Projects Module (`app/routes/projects.py`)
|
||||
- Lines 265, 273, 1340, 1346, 1552, 1558, 1873, 1889: Exception handlers with `pass`
|
||||
- **Impact:** Project operations may have incomplete error handling.
|
||||
**Priority:** Low-Medium
|
||||
|
||||
#### 1.9 Timer Module (`app/routes/timer.py`)
|
||||
- Lines 1804, 1822, 1833, 1842, 1920: Exception handlers with `pass`
|
||||
- **Impact:** Timer operations may have incomplete error handling.
|
||||
**Priority:** Medium
|
||||
|
||||
#### 1.10 API Routes (`app/routes/api_v1.py`)
|
||||
- Multiple `pass` statements (lines 1459, 1466, 1755, 1979, 2232, 2398, 2406, 3674, 3796, 3945, 4280, 4294, 4301, 4471)
|
||||
- **Impact:** API endpoints may have incomplete error handling.
|
||||
**Priority:** Medium
|
||||
|
||||
### 2. Utility Modules with Incomplete Implementations
|
||||
|
||||
#### 2.1 Webhook Service (`app/utils/webhook_service.py`)
|
||||
- **Status:** Implementation appears complete, but webhook signature verification is not fully implemented in all integrations.
|
||||
|
||||
#### 2.2 Telemetry (`app/utils/telemetry.py`)
|
||||
- **Status:** Implementation appears complete.
|
||||
|
||||
#### 2.3 PostHog Features (`app/utils/posthog_features.py`)
|
||||
- **Line 202-205**: Placeholder implementations
|
||||
```python
|
||||
# This is a placeholder - implement based on your needs
|
||||
pass
|
||||
```
|
||||
**Impact:** PostHog feature flags may not be fully functional.
|
||||
**Priority:** Low
|
||||
|
||||
#### 2.4 Environment Validation (`app/utils/env_validation.py`)
|
||||
- **Line 14**: `pass` statement
|
||||
- **Impact:** Environment validation may be incomplete.
|
||||
**Priority:** Low
|
||||
|
||||
#### 2.5 Data Import (`app/utils/data_import.py`)
|
||||
- Multiple `pass` statements (lines 19, 558, 698, 710, 718)
|
||||
- **Impact:** Data import functionality may be incomplete.
|
||||
**Priority:** Medium
|
||||
|
||||
#### 2.6 Excel Export (`app/utils/excel_export.py`)
|
||||
- Multiple `pass` statements (lines 97, 209, 407, 528)
|
||||
- **Impact:** Excel export may have incomplete error handling.
|
||||
**Priority:** Low
|
||||
|
||||
#### 2.7 Backup (`app/utils/backup.py`)
|
||||
- Multiple `pass` statements (lines 170, 198, 213, 221, 332, 340)
|
||||
- **Impact:** Backup operations may have incomplete error handling.
|
||||
**Priority:** Medium
|
||||
|
||||
### 3. Model Incomplete Implementations
|
||||
|
||||
#### 3.1 Custom Field Definitions (`app/models/custom_field_definition.py`)
|
||||
- Multiple `pass` statements in exception handlers (lines 69, 86, 113, 130, 157, 174)
|
||||
- **Impact:** Custom field validation may be incomplete.
|
||||
**Priority:** Low
|
||||
|
||||
#### 3.2 Invoice Model (`app/models/invoice.py`)
|
||||
- Lines 202, 244: `pass` statements
|
||||
- **Impact:** Invoice operations may have incomplete error handling.
|
||||
**Priority:** Low
|
||||
|
||||
#### 3.3 Import/Export Model (`app/models/import_export.py`)
|
||||
- Lines 67, 98: `pass` statements
|
||||
- **Impact:** Import/export operations may be incomplete.
|
||||
**Priority:** Medium
|
||||
|
||||
---
|
||||
|
||||
## Frontend Incomplete Implementations
|
||||
|
||||
### 1. Offline Sync (`app/static/offline-sync.js`)
|
||||
|
||||
#### 1.1 Task Sync
|
||||
- **Line 375-378**: Task synchronization not implemented
|
||||
```javascript
|
||||
async syncTasks() {
|
||||
// Similar implementation for tasks
|
||||
// TODO: Implement task sync
|
||||
}
|
||||
```
|
||||
**Impact:** Tasks cannot be synced when offline.
|
||||
**Priority:** Medium
|
||||
|
||||
#### 1.2 Project Sync
|
||||
- **Line 380-383**: Project synchronization not implemented
|
||||
```javascript
|
||||
async syncProjects() {
|
||||
// Similar implementation for projects
|
||||
// TODO: Implement project sync
|
||||
}
|
||||
```
|
||||
**Impact:** Projects cannot be synced when offline.
|
||||
**Priority:** Medium
|
||||
|
||||
### 2. Enhanced UI (`app/static/enhanced-ui.js`)
|
||||
|
||||
#### 2.1 Toast Manager Info Method
|
||||
- **Line 873-876**: Info method is empty
|
||||
```javascript
|
||||
info(message, duration) {
|
||||
// Empty implementation
|
||||
}
|
||||
```
|
||||
**Impact:** Info toast notifications may not work.
|
||||
**Priority:** Low
|
||||
|
||||
#### 2.2 Form Auto-Save
|
||||
- **Line 1238**: Incomplete form auto-save initialization
|
||||
```javascript
|
||||
document.querySelectorAll
|
||||
new FormAutoSave(form, {
|
||||
```
|
||||
**Impact:** Form auto-save may not be properly initialized.
|
||||
**Priority:** Medium
|
||||
|
||||
### 3. Error Handling (`app/static/error-handling-enhanced.js`)
|
||||
|
||||
#### 3.1 Feature Fallbacks
|
||||
- **Lines 718-730**: Fallback implementations are incomplete
|
||||
```javascript
|
||||
setupFeatureFallbacks() {
|
||||
// Fallback for fetch if not available
|
||||
if (typeof fetch === 'undefined') {
|
||||
console.warn('Fetch API not available, using XMLHttpRequest fallback');
|
||||
// Implement XMLHttpRequest-based fetch polyfill if needed
|
||||
}
|
||||
|
||||
// Fallback for localStorage
|
||||
if (typeof Storage === 'undefined') {
|
||||
console.warn('LocalStorage not available, using memory storage');
|
||||
// Implement in-memory storage fallback
|
||||
}
|
||||
}
|
||||
```
|
||||
**Impact:** Older browsers may not have proper fallbacks.
|
||||
**Priority:** Low
|
||||
|
||||
### 4. Smart Notifications (`app/static/smart-notifications.js`)
|
||||
|
||||
#### 4.1 Check Methods
|
||||
- **Lines 192, 227, 267**: Methods have incomplete implementations
|
||||
- `checkIdleTime()` - May not fully check idle time
|
||||
- `checkDeadlines()` - May not fully check deadlines
|
||||
- `checkDailySummary()` - May not fully check daily summaries
|
||||
**Impact:** Smart notifications may not work as expected.
|
||||
**Priority:** Medium
|
||||
|
||||
---
|
||||
|
||||
## Integration Incomplete Implementations
|
||||
|
||||
### 1. CalDAV Integration (`app/integrations/caldav_calendar.py`)
|
||||
|
||||
#### 1.1 OAuth Methods
|
||||
- **Lines 378, 381, 384**: OAuth methods raise `NotImplementedError`
|
||||
```python
|
||||
def get_authorization_url(self, redirect_uri: str, state: str = None) -> str:
|
||||
raise NotImplementedError("CalDAV does not use OAuth in this integration. Use the CalDAV setup form.")
|
||||
|
||||
def exchange_code_for_tokens(self, code: str, redirect_uri: str) -> Dict[str, Any]:
|
||||
raise NotImplementedError("CalDAV does not use OAuth in this integration.")
|
||||
|
||||
def refresh_access_token(self) -> Dict[str, Any]:
|
||||
raise NotImplementedError("CalDAV does not use OAuth token refresh in this integration.")
|
||||
```
|
||||
**Status:** This is intentional - CalDAV uses basic auth, not OAuth.
|
||||
**Impact:** None - this is by design.
|
||||
**Priority:** N/A
|
||||
|
||||
#### 1.2 Sync Direction
|
||||
- **Line 663**: Bidirectional sync not implemented
|
||||
```python
|
||||
return {"success": False, "message": "Sync direction not implemented for CalDAV yet."}
|
||||
```
|
||||
**Impact:** Cannot sync from TimeTracker to CalDAV calendar.
|
||||
**Priority:** Medium
|
||||
|
||||
### 2. GitHub Integration (`app/integrations/github.py`)
|
||||
|
||||
#### 2.1 Webhook Signature Verification
|
||||
- **Line 248-249**: Webhook signature verification is incomplete
|
||||
```python
|
||||
if signature:
|
||||
# Signature verification would go here
|
||||
pass
|
||||
```
|
||||
**Impact:** GitHub webhooks may not be properly secured.
|
||||
**Priority:** High
|
||||
|
||||
### 3. Trello Integration (`app/integrations/trello.py`)
|
||||
|
||||
#### 3.1 Sync Direction
|
||||
- **Status:** Bidirectional sync may not be fully implemented.
|
||||
**Impact:** Changes in TimeTracker may not sync back to Trello.
|
||||
**Priority:** Medium
|
||||
|
||||
### 4. Xero Integration (`app/integrations/xero.py`)
|
||||
|
||||
#### 4.1 Invoice/Expense Creation
|
||||
- **Status:** Implementation appears complete but may need testing.
|
||||
**Impact:** Unknown - needs verification.
|
||||
**Priority:** Low
|
||||
|
||||
### 5. QuickBooks Integration (`app/integrations/quickbooks.py`)
|
||||
|
||||
#### 5.1 Invoice/Expense Creation
|
||||
- **Lines 291, 301**: Hardcoded values for customer and account references
|
||||
```python
|
||||
# Add customer reference (would need customer mapping)
|
||||
# qb_invoice["CustomerRef"] = {"value": customer_qb_id}
|
||||
```
|
||||
**Impact:** Invoices/expenses may not be properly linked to QuickBooks entities.
|
||||
**Priority:** High
|
||||
|
||||
### 6. Other Integrations
|
||||
|
||||
#### 6.1 Google Calendar (`app/integrations/google_calendar.py`)
|
||||
- **Line 108**: `pass` statement in exception handler
|
||||
- **Impact:** Error handling may be incomplete.
|
||||
**Priority:** Low
|
||||
|
||||
#### 6.2 Outlook Calendar (`app/integrations/outlook_calendar.py`)
|
||||
- **Line 117**: `pass` statement in exception handler
|
||||
- **Impact:** Error handling may be incomplete.
|
||||
**Priority:** Low
|
||||
|
||||
#### 6.3 Microsoft Teams (`app/integrations/microsoft_teams.py`)
|
||||
- **Line 116**: `pass` statement in exception handler
|
||||
- **Impact:** Error handling may be incomplete.
|
||||
**Priority:** Low
|
||||
|
||||
#### 6.4 Asana (`app/integrations/asana.py`)
|
||||
- **Line 91**: `pass` statement in exception handler
|
||||
- **Impact:** Error handling may be incomplete.
|
||||
**Priority:** Low
|
||||
|
||||
#### 6.5 GitLab (`app/integrations/gitlab.py`)
|
||||
- **Line 108**: `pass` statement in exception handler
|
||||
- **Impact:** Error handling may be incomplete.
|
||||
**Priority:** Low
|
||||
|
||||
---
|
||||
|
||||
## Missing Features
|
||||
|
||||
### 1. API Endpoints
|
||||
|
||||
#### 1.1 Search API
|
||||
- **Location:** Referenced in `enhanced-ui.js` line 1216
|
||||
- **Status:** Endpoint `/api/search` is referenced but may not exist
|
||||
- **Impact:** Enhanced search feature may not work.
|
||||
**Priority:** High
|
||||
|
||||
#### 1.2 Activity Feed API
|
||||
- **Status:** API exists but may need additional endpoints for real-time updates.
|
||||
**Priority:** Low
|
||||
|
||||
### 2. Frontend Features
|
||||
|
||||
#### 2.1 Service Worker
|
||||
- **File:** `app/static/service-worker.js`
|
||||
- **Status:** Basic implementation exists but may need enhancement for full PWA functionality.
|
||||
**Priority:** Medium
|
||||
|
||||
#### 2.2 Kiosk Mode
|
||||
- **File:** `app/routes/kiosk.py`
|
||||
- **Status:** Routes exist but may need additional features.
|
||||
**Priority:** Low
|
||||
|
||||
### 3. Backend Features
|
||||
|
||||
#### 3.1 Team Chat
|
||||
- **File:** `app/routes/team_chat.py`
|
||||
- **Line 116**: `pass` statement
|
||||
- **Status:** Team chat feature may be incomplete.
|
||||
**Priority:** Low
|
||||
|
||||
#### 3.2 Kanban Board
|
||||
- **File:** `app/routes/kanban.py`
|
||||
- **Status:** Implementation appears complete but may need additional features.
|
||||
**Priority:** Low
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints Missing
|
||||
|
||||
### 1. Search Endpoint
|
||||
- **Expected:** `/api/search`
|
||||
- **Referenced in:** `app/static/enhanced-ui.js:1216`
|
||||
- **Priority:** High
|
||||
|
||||
### 2. Real-time Activity Feed
|
||||
- **Expected:** WebSocket or SSE endpoint for real-time activity updates
|
||||
- **Priority:** Low
|
||||
|
||||
### 3. Push Notification Endpoints
|
||||
- **Status:** Basic endpoints exist but may need additional functionality.
|
||||
- **Priority:** Medium
|
||||
|
||||
---
|
||||
|
||||
## Priority Recommendations
|
||||
|
||||
### High Priority
|
||||
|
||||
1. **Issues Module Permission Filtering** (`app/routes/issues.py:60`)
|
||||
- Implement proper permission filtering for non-admin users
|
||||
- **Estimated Effort:** 2-4 hours
|
||||
|
||||
2. **GitHub Webhook Signature Verification** (`app/integrations/github.py:248`)
|
||||
- Implement proper webhook signature verification
|
||||
- **Estimated Effort:** 2-3 hours
|
||||
|
||||
3. **QuickBooks Customer/Account Mapping** (`app/integrations/quickbooks.py:291, 301`)
|
||||
- Implement proper mapping for customers and accounts
|
||||
- **Estimated Effort:** 4-6 hours
|
||||
|
||||
4. **Search API Endpoint** (`/api/search`)
|
||||
- Implement the search API endpoint referenced in frontend
|
||||
- **Estimated Effort:** 4-8 hours
|
||||
|
||||
### Medium Priority
|
||||
|
||||
1. **Offline Sync for Tasks and Projects** (`app/static/offline-sync.js:375, 380`)
|
||||
- Implement task and project synchronization
|
||||
- **Estimated Effort:** 8-12 hours
|
||||
|
||||
2. **CalDAV Bidirectional Sync** (`app/integrations/caldav_calendar.py:663`)
|
||||
- Implement sync from TimeTracker to CalDAV
|
||||
- **Estimated Effort:** 6-10 hours
|
||||
|
||||
3. **Form Auto-Save Initialization** (`app/static/enhanced-ui.js:1238`)
|
||||
- Fix form auto-save initialization
|
||||
- **Estimated Effort:** 2-4 hours
|
||||
|
||||
4. **Smart Notifications** (`app/static/smart-notifications.js`)
|
||||
- Complete implementation of notification checks
|
||||
- **Estimated Effort:** 4-6 hours
|
||||
|
||||
5. **Push Notifications Storage** (`app/routes/push_notifications.py:27`)
|
||||
- Implement proper push subscription storage
|
||||
- **Estimated Effort:** 3-5 hours
|
||||
|
||||
6. **Backup Error Handling** (`app/utils/backup.py`)
|
||||
- Complete error handling for backup operations
|
||||
- **Estimated Effort:** 4-6 hours
|
||||
|
||||
### Low Priority
|
||||
|
||||
1. **Exception Handler Completions**
|
||||
- Replace `pass` statements with proper error handling
|
||||
- **Estimated Effort:** 20-30 hours (across all files)
|
||||
|
||||
2. **Feature Fallbacks** (`app/static/error-handling-enhanced.js:718`)
|
||||
- Implement proper fallbacks for older browsers
|
||||
- **Estimated Effort:** 6-8 hours
|
||||
|
||||
3. **Toast Manager Info Method** (`app/static/enhanced-ui.js:873`)
|
||||
- Implement info toast notifications
|
||||
- **Estimated Effort:** 1-2 hours
|
||||
|
||||
4. **PostHog Feature Flags** (`app/utils/posthog_features.py:202`)
|
||||
- Complete PostHog feature flag implementation
|
||||
- **Estimated Effort:** 4-6 hours
|
||||
|
||||
---
|
||||
|
||||
## Testing Recommendations
|
||||
|
||||
### 1. Integration Testing
|
||||
- Test all integration connectors with real services
|
||||
- Verify webhook handling in all integrations
|
||||
- Test OAuth flows for all integrations
|
||||
|
||||
### 2. Offline Functionality
|
||||
- Test offline sync for all entity types
|
||||
- Verify service worker functionality
|
||||
- Test PWA installation and offline mode
|
||||
|
||||
### 3. Permission Testing
|
||||
- Test permission filtering in issues module
|
||||
- Verify role-based access control across all modules
|
||||
- Test audit log access permissions
|
||||
|
||||
### 4. Error Handling
|
||||
- Test all exception handlers
|
||||
- Verify error messages are user-friendly
|
||||
- Test error recovery mechanisms
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The TimeTracker application has a solid foundation with most core features implemented. However, there are several areas that need attention:
|
||||
|
||||
1. **Security:** Webhook signature verification and permission filtering need completion
|
||||
2. **Offline Support:** Task and project synchronization need implementation
|
||||
3. **Integrations:** Some integrations need bidirectional sync and proper entity mapping
|
||||
4. **Error Handling:** Many exception handlers need proper implementation
|
||||
5. **API Completeness:** Search API endpoint needs implementation
|
||||
|
||||
Most incomplete implementations are in error handling and edge cases, which is common in large codebases. The high-priority items should be addressed first to ensure security and core functionality.
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- This analysis is based on static code analysis and may not reflect runtime behavior
|
||||
- Some `pass` statements may be intentional placeholders for future features
|
||||
- Error handling with `pass` may be acceptable if errors are logged elsewhere
|
||||
- Integration implementations may be complete but need testing with real services
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2025-01-27
|
||||
**Next Review:** After addressing high-priority items
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
# Module Integration Implementation Summary
|
||||
|
||||
**Date:** 2025-01-27
|
||||
**Status:** ✅ Implementation Complete
|
||||
|
||||
## Overview
|
||||
|
||||
This document summarizes the implementation of the module integration and visibility control system for TimeTracker.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Completed Components
|
||||
|
||||
### 1. Module Registry System (`app/utils/module_registry.py`)
|
||||
|
||||
**Status:** ✅ Complete
|
||||
|
||||
- Created centralized `ModuleRegistry` class
|
||||
- Defined `ModuleDefinition` dataclass with metadata
|
||||
- Registered 50+ modules with:
|
||||
- Module IDs and names
|
||||
- Categories (Core, Time Tracking, CRM, Finance, etc.)
|
||||
- Dependencies
|
||||
- Settings and user flags
|
||||
- Icons and display order
|
||||
- Implemented `is_enabled()` method for checking module availability
|
||||
- Supports dependency checking
|
||||
|
||||
**Key Features:**
|
||||
- Automatic initialization with `initialize_defaults()`
|
||||
- Category-based organization
|
||||
- Dependency validation
|
||||
- Core modules always enabled
|
||||
|
||||
### 2. Module Helpers (`app/utils/module_helpers.py`)
|
||||
|
||||
**Status:** ✅ Complete
|
||||
|
||||
- Created `@module_enabled()` decorator for route protection
|
||||
- Implemented `is_module_enabled()` helper function
|
||||
- Added template context processors
|
||||
- Integrated with Flask app initialization
|
||||
|
||||
**Usage Example:**
|
||||
```python
|
||||
@calendar_bp.route("/calendar")
|
||||
@login_required
|
||||
@module_enabled("calendar")
|
||||
def view_calendar():
|
||||
# Route implementation
|
||||
pass
|
||||
```
|
||||
|
||||
### 3. Database Models Updated
|
||||
|
||||
**Status:** ✅ Complete
|
||||
|
||||
#### Settings Model (`app/models/settings.py`)
|
||||
Added 17 new UI flags:
|
||||
- `ui_allow_integrations`
|
||||
- `ui_allow_import_export`
|
||||
- `ui_allow_saved_filters`
|
||||
- `ui_allow_contacts`
|
||||
- `ui_allow_deals`
|
||||
- `ui_allow_leads`
|
||||
- `ui_allow_invoices`
|
||||
- `ui_allow_expenses`
|
||||
- `ui_allow_time_entry_templates`
|
||||
- `ui_allow_workflows`
|
||||
- `ui_allow_time_approvals`
|
||||
- `ui_allow_activity_feed`
|
||||
- `ui_allow_recurring_tasks`
|
||||
- `ui_allow_team_chat`
|
||||
- `ui_allow_client_portal`
|
||||
- `ui_allow_kiosk`
|
||||
|
||||
#### User Model (`app/models/user.py`)
|
||||
Added corresponding 17 `ui_show_*` flags for per-user customization.
|
||||
|
||||
### 4. Database Migration
|
||||
|
||||
**Status:** ✅ Complete
|
||||
|
||||
Created migration: `migrations/versions/092_add_missing_module_visibility_flags.py`
|
||||
|
||||
- Adds all missing columns to `settings` table
|
||||
- Adds all missing columns to `users` table
|
||||
- Includes proper defaults (True for backward compatibility)
|
||||
- Includes downgrade support
|
||||
|
||||
### 5. Admin UI for Module Management
|
||||
|
||||
**Status:** ✅ Complete
|
||||
|
||||
**Route:** `/admin/modules`
|
||||
|
||||
**Features:**
|
||||
- Visual interface for enabling/disabling modules
|
||||
- Grouped by category
|
||||
- Shows module descriptions
|
||||
- Displays dependencies
|
||||
- Core modules marked as non-disabled
|
||||
- Bulk update support
|
||||
|
||||
**Template:** `app/templates/admin/modules.html`
|
||||
|
||||
### 6. App Initialization
|
||||
|
||||
**Status:** ✅ Complete
|
||||
|
||||
Updated `app/__init__.py` to:
|
||||
- Initialize module registry on startup
|
||||
- Register module helpers in template context
|
||||
- Make module checking available globally
|
||||
|
||||
### 7. Route Protection Examples
|
||||
|
||||
**Status:** ✅ Partial (Examples Added)
|
||||
|
||||
Added `@module_enabled()` decorator to:
|
||||
- Calendar routes (main routes)
|
||||
- Invoices routes (main route)
|
||||
|
||||
**Note:** All optional module routes should have this decorator. This is a pattern that can be applied to remaining routes.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Implementation Checklist
|
||||
|
||||
- [x] Module registry system created
|
||||
- [x] All modules registered with metadata
|
||||
- [x] Module checking utilities implemented
|
||||
- [x] Route decorator created
|
||||
- [x] Settings model updated with all flags
|
||||
- [x] User model updated with all flags
|
||||
- [x] Database migration created
|
||||
- [x] Admin UI created
|
||||
- [x] App initialization updated
|
||||
- [x] Example route protection added
|
||||
- [ ] All routes protected (ongoing - pattern established)
|
||||
- [ ] Navigation refactored (can be done incrementally)
|
||||
- [ ] Documentation updated
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Next Steps (Optional Enhancements)
|
||||
|
||||
### 1. Complete Route Protection
|
||||
|
||||
Apply `@module_enabled()` decorator to all optional module routes:
|
||||
|
||||
**High Priority:**
|
||||
- `app/routes/contacts.py`
|
||||
- `app/routes/deals.py`
|
||||
- `app/routes/leads.py`
|
||||
- `app/routes/expenses.py`
|
||||
- `app/routes/workflows.py`
|
||||
- `app/routes/time_approvals.py`
|
||||
- `app/routes/team_chat.py`
|
||||
|
||||
**Medium Priority:**
|
||||
- All remaining calendar routes
|
||||
- All remaining invoice routes
|
||||
- Other optional module routes
|
||||
|
||||
### 2. Navigation Refactoring
|
||||
|
||||
Refactor `app/templates/base.html` to use module registry:
|
||||
|
||||
```python
|
||||
# Instead of hardcoded checks:
|
||||
{% if settings.ui_allow_calendar and current_user.ui_show_calendar %}
|
||||
|
||||
# Use module registry:
|
||||
{% if is_module_enabled("calendar") %}
|
||||
```
|
||||
|
||||
### 3. User Profile Settings
|
||||
|
||||
Add UI in user profile to customize module visibility (per-user flags).
|
||||
|
||||
### 4. Dependency Validation
|
||||
|
||||
Add validation in admin UI to prevent disabling modules that others depend on.
|
||||
|
||||
---
|
||||
|
||||
## 📊 Statistics
|
||||
|
||||
- **Total Modules Registered:** 50+
|
||||
- **Modules with Flags:** 50+ (100%)
|
||||
- **Categories:** 9
|
||||
- **Core Modules:** 6 (always enabled)
|
||||
- **Optional Modules:** 44+
|
||||
- **New Flags Added:** 34 (17 Settings + 17 User)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Benefits Achieved
|
||||
|
||||
1. ✅ **Centralized Management** - Single source of truth for module metadata
|
||||
2. ✅ **Easy Configuration** - Admin can enable/disable modules from UI
|
||||
3. ✅ **Route Protection** - Routes can be protected by module flags
|
||||
4. ✅ **Dependency Tracking** - Module dependencies are defined and checked
|
||||
5. ✅ **Extensible** - Easy to add new modules
|
||||
6. ✅ **Backward Compatible** - All flags default to True
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Usage Examples
|
||||
|
||||
### Checking Module Availability in Routes
|
||||
|
||||
```python
|
||||
from app.utils.module_helpers import module_enabled
|
||||
|
||||
@calendar_bp.route("/calendar")
|
||||
@login_required
|
||||
@module_enabled("calendar")
|
||||
def view_calendar():
|
||||
return render_template("calendar/view.html")
|
||||
```
|
||||
|
||||
### Checking in Templates
|
||||
|
||||
```jinja2
|
||||
{% if is_module_enabled("calendar") %}
|
||||
<a href="{{ url_for('calendar.view_calendar') }}">Calendar</a>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### Getting Enabled Modules
|
||||
|
||||
```python
|
||||
from app.utils.module_helpers import get_enabled_modules
|
||||
from app.utils.module_registry import ModuleCategory
|
||||
|
||||
# Get all enabled modules
|
||||
enabled = get_enabled_modules()
|
||||
|
||||
# Get enabled modules by category
|
||||
finance_modules = get_enabled_modules(ModuleCategory.FINANCE)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Migration Instructions
|
||||
|
||||
1. **Run the migration:**
|
||||
```bash
|
||||
flask db upgrade
|
||||
```
|
||||
|
||||
2. **Verify flags were added:**
|
||||
- Check `settings` table has new `ui_allow_*` columns
|
||||
- Check `users` table has new `ui_show_*` columns
|
||||
|
||||
3. **Access module management:**
|
||||
- Navigate to `/admin/modules`
|
||||
- Configure module visibility
|
||||
- Save changes
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Known Issues / Limitations
|
||||
|
||||
1. **Route Protection:** Not all routes are protected yet (pattern established, can be applied incrementally)
|
||||
2. **Navigation:** Still uses hardcoded checks (can be refactored incrementally)
|
||||
3. **Dependency Validation:** Admin UI doesn't prevent disabling required dependencies (can be added)
|
||||
|
||||
---
|
||||
|
||||
## 📚 Related Documentation
|
||||
|
||||
- `docs/development/MODULE_INTEGRATION_PLAN.md` - Original plan
|
||||
- `docs/development/MODULE_STRUCTURE_ANALYSIS.md` - Module analysis
|
||||
- `app/utils/module_registry.py` - Registry implementation
|
||||
- `app/utils/module_helpers.py` - Helper functions
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2025-01-27
|
||||
|
||||
@@ -0,0 +1,553 @@
|
||||
# Module Integration & Visibility Control Plan
|
||||
|
||||
**Date:** 2025-01-27
|
||||
**Status:** Planning Phase
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This document outlines a comprehensive plan to improve module integration in TimeTracker and implement a centralized system for enabling/disabling modules and menu items from admin settings. The goal is to create a more maintainable, flexible architecture that allows administrators to customize the application based on their needs.
|
||||
|
||||
---
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### Module Inventory
|
||||
|
||||
TimeTracker currently has **50+ modules/features** organized into the following categories:
|
||||
|
||||
#### Core Modules (Always Enabled)
|
||||
- **Authentication** (`auth`) - Login, logout, profile
|
||||
- **Dashboard** (`main`) - Main dashboard
|
||||
- **Projects** (`projects`) - Project management
|
||||
- **Time Tracking** (`timer`) - Time entry, timers
|
||||
- **Tasks** (`tasks`) - Task management
|
||||
- **Clients** (`clients`) - Client management
|
||||
|
||||
#### Optional Modules (Can Be Disabled)
|
||||
1. **Calendar** (`calendar`) - Calendar view, integrations
|
||||
2. **Project Templates** (`project_templates`) - Template system
|
||||
3. **Gantt Chart** (`gantt`) - Gantt visualization
|
||||
4. **Kanban Board** (`kanban`) - Kanban task board
|
||||
5. **Weekly Goals** (`weekly_goals`) - Goal tracking
|
||||
6. **Issues** (`issues`) - Issue/bug tracking
|
||||
7. **CRM Features:**
|
||||
- **Quotes** (`quotes`) - Quote management
|
||||
- **Contacts** (`contacts`) - Contact management
|
||||
- **Deals** (`deals`) - Deal pipeline
|
||||
- **Leads** (`leads`) - Lead management
|
||||
8. **Finance & Expenses:**
|
||||
- **Reports** (`reports`) - Standard reports
|
||||
- **Report Builder** (`custom_reports`) - Custom report builder
|
||||
- **Scheduled Reports** (`scheduled_reports`) - Automated reports
|
||||
- **Invoices** (`invoices`) - Invoice management
|
||||
- **Invoice Approvals** (`invoice_approvals`) - Approval workflow
|
||||
- **Recurring Invoices** (`recurring_invoices`) - Recurring billing
|
||||
- **Payments** (`payments`) - Payment tracking
|
||||
- **Payment Gateways** (`payment_gateways`) - Gateway integration
|
||||
- **Expenses** (`expenses`) - Expense tracking
|
||||
- **Mileage** (`mileage`) - Mileage tracking
|
||||
- **Per Diem** (`per_diem`) - Per diem expenses
|
||||
- **Budget Alerts** (`budget_alerts`) - Budget monitoring
|
||||
9. **Inventory** (`inventory`) - Inventory management
|
||||
10. **Analytics** (`analytics`) - Analytics dashboard
|
||||
11. **Tools & Data:**
|
||||
- **Integrations** (`integrations`) - External integrations
|
||||
- **Import/Export** (`import_export`) - Data import/export
|
||||
- **Saved Filters** (`saved_filters`) - Filter management
|
||||
12. **Admin Features:**
|
||||
- **User Management** (`admin`) - User administration
|
||||
- **Permissions** (`permissions`) - RBAC system
|
||||
- **Settings** (`settings`) - System settings
|
||||
- **Audit Logs** (`audit_logs`) - Activity logging
|
||||
- **Webhooks** (`webhooks`) - Webhook management
|
||||
- **Custom Fields** (`custom_field_definitions`) - Field definitions
|
||||
- **Link Templates** (`link_templates`) - Link templates
|
||||
- **Time Entry Templates** (`time_entry_templates`) - Time templates
|
||||
13. **Advanced Features:**
|
||||
- **Workflows** (`workflows`) - Automation workflows
|
||||
- **Time Approvals** (`time_approvals`) - Time approval workflow
|
||||
- **Activity Feed** (`activity_feed`) - Activity stream
|
||||
- **Recurring Tasks** (`recurring_tasks`) - Automated tasks
|
||||
- **Team Chat** (`team_chat`) - Team messaging
|
||||
- **Client Portal** (`client_portal`) - Client-facing portal
|
||||
- **Kiosk Mode** (`kiosk`) - Kiosk interface
|
||||
|
||||
### Current Architecture Issues
|
||||
|
||||
1. **No Centralized Module Registry**
|
||||
- Blueprints registered directly in `app/__init__.py`
|
||||
- No single source of truth for module metadata
|
||||
- Hard to track module dependencies
|
||||
|
||||
2. **Inconsistent Visibility Control**
|
||||
- Some modules have `ui_allow_*` flags in Settings
|
||||
- Some modules have `ui_show_*` flags in User
|
||||
- Many modules have no flags at all
|
||||
- No route-level protection
|
||||
|
||||
3. **Complex Navigation Logic**
|
||||
- Navigation menu has hardcoded endpoint checks
|
||||
- Conditional rendering scattered throughout templates
|
||||
- Difficult to add/remove menu items
|
||||
|
||||
4. **Missing Module Flags**
|
||||
- CRM features (deals, leads, contacts) have no flags
|
||||
- Many admin features have no flags
|
||||
- Advanced features have no flags
|
||||
|
||||
5. **No Module Dependencies**
|
||||
- No way to express that one module depends on another
|
||||
- No validation when disabling modules
|
||||
|
||||
---
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
### Phase 1: Module Registry System
|
||||
|
||||
Create a centralized module registry that defines:
|
||||
- Module metadata (name, description, category)
|
||||
- Dependencies between modules
|
||||
- Default visibility settings
|
||||
- Route associations
|
||||
|
||||
**File:** `app/utils/module_registry.py`
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional, Dict
|
||||
from enum import Enum
|
||||
|
||||
class ModuleCategory(Enum):
|
||||
CORE = "core"
|
||||
TIME_TRACKING = "time_tracking"
|
||||
PROJECT_MANAGEMENT = "project_management"
|
||||
CRM = "crm"
|
||||
FINANCE = "finance"
|
||||
INVENTORY = "inventory"
|
||||
ANALYTICS = "analytics"
|
||||
TOOLS = "tools"
|
||||
ADMIN = "admin"
|
||||
ADVANCED = "advanced"
|
||||
|
||||
@dataclass
|
||||
class ModuleDefinition:
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
category: ModuleCategory
|
||||
blueprint_name: str
|
||||
default_enabled: bool = True
|
||||
requires_admin: bool = False
|
||||
dependencies: List[str] = None # Module IDs this depends on
|
||||
settings_flag: Optional[str] = None # Settings.ui_allow_* field name
|
||||
user_flag: Optional[str] = None # User.ui_show_* field name
|
||||
routes: List[str] = None # Route endpoints
|
||||
|
||||
def __post_init__(self):
|
||||
if self.dependencies is None:
|
||||
self.dependencies = []
|
||||
if self.routes is None:
|
||||
self.routes = []
|
||||
|
||||
class ModuleRegistry:
|
||||
_modules: Dict[str, ModuleDefinition] = {}
|
||||
|
||||
@classmethod
|
||||
def register(cls, module: ModuleDefinition):
|
||||
cls._modules[module.id] = module
|
||||
|
||||
@classmethod
|
||||
def get(cls, module_id: str) -> Optional[ModuleDefinition]:
|
||||
return cls._modules.get(module_id)
|
||||
|
||||
@classmethod
|
||||
def get_all(cls) -> Dict[str, ModuleDefinition]:
|
||||
return cls._modules.copy()
|
||||
|
||||
@classmethod
|
||||
def get_by_category(cls, category: ModuleCategory) -> List[ModuleDefinition]:
|
||||
return [m for m in cls._modules.values() if m.category == category]
|
||||
|
||||
@classmethod
|
||||
def is_enabled(cls, module_id: str, settings, user) -> bool:
|
||||
"""Check if a module is enabled for a user"""
|
||||
module = cls.get(module_id)
|
||||
if not module:
|
||||
return False
|
||||
|
||||
# Check system-wide setting
|
||||
if module.settings_flag:
|
||||
if not getattr(settings, module.settings_flag, True):
|
||||
return False
|
||||
|
||||
# Check user-specific setting
|
||||
if module.user_flag:
|
||||
if not getattr(user, module.user_flag, True):
|
||||
return False
|
||||
|
||||
# Check dependencies
|
||||
for dep_id in module.dependencies:
|
||||
if not cls.is_enabled(dep_id, settings, user):
|
||||
return False
|
||||
|
||||
return True
|
||||
```
|
||||
|
||||
### Phase 2: Add Missing UI Flags
|
||||
|
||||
Add missing `ui_allow_*` flags to Settings model and `ui_show_*` flags to User model for all modules.
|
||||
|
||||
**Settings Model Additions:**
|
||||
```python
|
||||
# CRM section (missing)
|
||||
ui_allow_contacts = db.Column(db.Boolean, default=True, nullable=False)
|
||||
ui_allow_deals = db.Column(db.Boolean, default=True, nullable=False)
|
||||
ui_allow_leads = db.Column(db.Boolean, default=True, nullable=False)
|
||||
|
||||
# Admin section (missing)
|
||||
ui_allow_workflows = db.Column(db.Boolean, default=True, nullable=False)
|
||||
ui_allow_time_approvals = db.Column(db.Boolean, default=True, nullable=False)
|
||||
ui_allow_activity_feed = db.Column(db.Boolean, default=True, nullable=False)
|
||||
ui_allow_recurring_tasks = db.Column(db.Boolean, default=True, nullable=False)
|
||||
ui_allow_team_chat = db.Column(db.Boolean, default=True, nullable=False)
|
||||
ui_allow_client_portal = db.Column(db.Boolean, default=True, nullable=False)
|
||||
ui_allow_kiosk = db.Column(db.Boolean, default=True, nullable=False)
|
||||
```
|
||||
|
||||
**User Model Additions:**
|
||||
```python
|
||||
# CRM section (missing)
|
||||
ui_show_contacts = db.Column(db.Boolean, default=True, nullable=False)
|
||||
ui_show_deals = db.Column(db.Boolean, default=True, nullable=False)
|
||||
ui_show_leads = db.Column(db.Boolean, default=True, nullable=False)
|
||||
|
||||
# Admin section (missing)
|
||||
ui_show_workflows = db.Column(db.Boolean, default=True, nullable=False)
|
||||
ui_show_time_approvals = db.Column(db.Boolean, default=True, nullable=False)
|
||||
ui_show_activity_feed = db.Column(db.Boolean, default=True, nullable=False)
|
||||
ui_show_recurring_tasks = db.Column(db.Boolean, default=True, nullable=False)
|
||||
ui_show_team_chat = db.Column(db.Boolean, default=True, nullable=False)
|
||||
ui_show_client_portal = db.Column(db.Boolean, default=True, nullable=False)
|
||||
ui_show_kiosk = db.Column(db.Boolean, default=True, nullable=False)
|
||||
```
|
||||
|
||||
### Phase 3: Module Checking Utilities
|
||||
|
||||
Create utilities for checking module availability in routes and templates.
|
||||
|
||||
**File:** `app/utils/module_helpers.py`
|
||||
|
||||
```python
|
||||
from functools import wraps
|
||||
from flask import abort, redirect, url_for, current_app
|
||||
from flask_login import current_user
|
||||
from app.models import Settings
|
||||
from app.utils.module_registry import ModuleRegistry
|
||||
|
||||
def module_enabled(module_id: str):
|
||||
"""Decorator to require a module to be enabled"""
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
settings = Settings.get_settings()
|
||||
if not ModuleRegistry.is_enabled(module_id, settings, current_user):
|
||||
if current_user.is_admin:
|
||||
flash(f"Module '{module_id}' is disabled. Enable it in Settings.", "warning")
|
||||
return redirect(url_for('admin.settings'))
|
||||
abort(403)
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
def is_module_enabled(module_id: str) -> bool:
|
||||
"""Check if a module is enabled for current user"""
|
||||
if not current_user.is_authenticated:
|
||||
return False
|
||||
settings = Settings.get_settings()
|
||||
return ModuleRegistry.is_enabled(module_id, settings, current_user)
|
||||
|
||||
# Template helper
|
||||
def init_module_helpers(app):
|
||||
@app.context_processor
|
||||
def inject_module_helpers():
|
||||
return {
|
||||
"is_module_enabled": is_module_enabled,
|
||||
"get_modules_by_category": lambda cat: ModuleRegistry.get_by_category(cat),
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 4: Route Protection
|
||||
|
||||
Add route decorators to protect routes based on module flags.
|
||||
|
||||
**Example:**
|
||||
```python
|
||||
@calendar_bp.route("/calendar")
|
||||
@login_required
|
||||
@module_enabled("calendar")
|
||||
def view_calendar():
|
||||
# Route implementation
|
||||
pass
|
||||
```
|
||||
|
||||
### Phase 5: Navigation Refactoring
|
||||
|
||||
Refactor navigation menu to use module registry instead of hardcoded checks.
|
||||
|
||||
**Benefits:**
|
||||
- Centralized menu structure
|
||||
- Automatic menu item visibility
|
||||
- Easier to add/remove items
|
||||
- Consistent behavior
|
||||
|
||||
**File:** `app/utils/navigation.py`
|
||||
|
||||
```python
|
||||
from app.utils.module_registry import ModuleRegistry, ModuleCategory
|
||||
from app.models import Settings
|
||||
from flask_login import current_user
|
||||
|
||||
def build_navigation_menu(settings, user):
|
||||
"""Build navigation menu structure from module registry"""
|
||||
menu = {
|
||||
"dashboard": {"enabled": True, "items": []},
|
||||
"calendar": {"enabled": False, "items": []},
|
||||
"time_tracking": {"enabled": True, "items": []},
|
||||
"crm": {"enabled": False, "items": []},
|
||||
"finance": {"enabled": False, "items": []},
|
||||
"inventory": {"enabled": False, "items": []},
|
||||
"analytics": {"enabled": False, "items": []},
|
||||
"tools": {"enabled": False, "items": []},
|
||||
"admin": {"enabled": False, "items": []},
|
||||
}
|
||||
|
||||
# Populate menu from module registry
|
||||
for module in ModuleRegistry.get_all().values():
|
||||
if ModuleRegistry.is_enabled(module.id, settings, user):
|
||||
category_key = module.category.value
|
||||
if category_key in menu:
|
||||
menu[category_key]["enabled"] = True
|
||||
menu[category_key]["items"].append({
|
||||
"id": module.id,
|
||||
"name": module.name,
|
||||
"url": url_for(f"{module.blueprint_name}.index") if hasattr(module, "index_route") else None,
|
||||
})
|
||||
|
||||
return menu
|
||||
```
|
||||
|
||||
### Phase 6: Admin UI for Module Management
|
||||
|
||||
Create admin interface to manage module visibility.
|
||||
|
||||
**Route:** `app/routes/admin.py`
|
||||
|
||||
```python
|
||||
@admin_bp.route("/admin/modules", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def manage_modules():
|
||||
"""Manage module visibility settings"""
|
||||
settings = Settings.get_settings()
|
||||
|
||||
if request.method == "POST":
|
||||
# Update module flags
|
||||
for module_id in ModuleRegistry.get_all().keys():
|
||||
flag_name = f"ui_allow_{module_id}"
|
||||
if hasattr(settings, flag_name):
|
||||
setattr(settings, flag_name, request.form.get(flag_name) == "on")
|
||||
|
||||
db.session.commit()
|
||||
flash("Module settings updated successfully", "success")
|
||||
return redirect(url_for("admin.manage_modules"))
|
||||
|
||||
# Group modules by category
|
||||
modules_by_category = {}
|
||||
for category in ModuleCategory:
|
||||
modules_by_category[category] = ModuleRegistry.get_by_category(category)
|
||||
|
||||
return render_template("admin/modules.html",
|
||||
modules_by_category=modules_by_category,
|
||||
settings=settings)
|
||||
```
|
||||
|
||||
**Template:** `app/templates/admin/modules.html`
|
||||
|
||||
- Checkboxes for each module
|
||||
- Category grouping
|
||||
- Dependency warnings
|
||||
- Save button
|
||||
|
||||
### Phase 7: Database Migration
|
||||
|
||||
Create Alembic migration to add missing columns.
|
||||
|
||||
**File:** `migrations/versions/XXXX_add_module_visibility_flags.py`
|
||||
|
||||
```python
|
||||
def upgrade():
|
||||
# Add Settings columns
|
||||
op.add_column('settings', sa.Column('ui_allow_contacts', sa.Boolean(), nullable=False, server_default='true'))
|
||||
op.add_column('settings', sa.Column('ui_allow_deals', sa.Boolean(), nullable=False, server_default='true'))
|
||||
# ... etc
|
||||
|
||||
# Add User columns
|
||||
op.add_column('users', sa.Column('ui_show_contacts', sa.Boolean(), nullable=False, server_default='true'))
|
||||
op.add_column('users', sa.Column('ui_show_deals', sa.Boolean(), nullable=False, server_default='true'))
|
||||
# ... etc
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Foundation (Week 1)
|
||||
- [ ] Create module registry system
|
||||
- [ ] Define all module definitions
|
||||
- [ ] Create module checking utilities
|
||||
- [ ] Add route decorators
|
||||
|
||||
### Phase 2: Database & Models (Week 1-2)
|
||||
- [ ] Create Alembic migration for missing flags
|
||||
- [ ] Update Settings model
|
||||
- [ ] Update User model
|
||||
- [ ] Test migration
|
||||
|
||||
### Phase 3: Route Protection (Week 2)
|
||||
- [ ] Add `@module_enabled` decorator to all optional routes
|
||||
- [ ] Test route protection
|
||||
- [ ] Handle edge cases
|
||||
|
||||
### Phase 4: Navigation Refactoring (Week 2-3)
|
||||
- [ ] Create navigation builder utility
|
||||
- [ ] Refactor base.html to use module registry
|
||||
- [ ] Test navigation visibility
|
||||
|
||||
### Phase 5: Admin UI (Week 3)
|
||||
- [ ] Create admin module management page
|
||||
- [ ] Add dependency validation
|
||||
- [ ] Test admin interface
|
||||
|
||||
### Phase 6: Testing & Documentation (Week 3-4)
|
||||
- [ ] Write unit tests
|
||||
- [ ] Write integration tests
|
||||
- [ ] Update documentation
|
||||
- [ ] Create migration guide
|
||||
|
||||
---
|
||||
|
||||
## Module Dependencies
|
||||
|
||||
### Critical Dependencies
|
||||
- **Invoices** → **Projects** (required)
|
||||
- **Payments** → **Invoices** (required)
|
||||
- **Expenses** → **Projects** (optional, but recommended)
|
||||
- **Tasks** → **Projects** (optional, but recommended)
|
||||
- **Time Entries** → **Projects** (required)
|
||||
|
||||
### Feature Dependencies
|
||||
- **Invoice Approvals** → **Invoices**
|
||||
- **Recurring Invoices** → **Invoices**
|
||||
- **Payment Gateways** → **Payments**
|
||||
- **Budget Alerts** → **Projects**
|
||||
- **Kanban Board** → **Tasks**
|
||||
- **Gantt Chart** → **Tasks**
|
||||
- **Deals** → **Clients**
|
||||
- **Leads** → **Clients**
|
||||
- **Contacts** → **Clients**
|
||||
|
||||
---
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Centralized Management**
|
||||
- Single source of truth for module metadata
|
||||
- Easy to add/remove modules
|
||||
- Clear dependency tracking
|
||||
|
||||
2. **Better User Experience**
|
||||
- Admins can customize application
|
||||
- Users see only relevant features
|
||||
- Cleaner navigation
|
||||
|
||||
3. **Maintainability**
|
||||
- Less code duplication
|
||||
- Easier to test
|
||||
- Clearer architecture
|
||||
|
||||
4. **Flexibility**
|
||||
- Easy to add new modules
|
||||
- Easy to change module behavior
|
||||
- Support for module plugins (future)
|
||||
|
||||
---
|
||||
|
||||
## Risks & Mitigation
|
||||
|
||||
### Risk 1: Breaking Existing Functionality
|
||||
**Mitigation:**
|
||||
- Comprehensive testing
|
||||
- Gradual rollout
|
||||
- Feature flags for new system
|
||||
|
||||
### Risk 2: Performance Impact
|
||||
**Mitigation:**
|
||||
- Cache module registry
|
||||
- Optimize database queries
|
||||
- Lazy loading where possible
|
||||
|
||||
### Risk 3: Migration Complexity
|
||||
**Mitigation:**
|
||||
- Thorough migration testing
|
||||
- Rollback plan
|
||||
- Data validation
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. ✅ All modules have visibility flags
|
||||
2. ✅ Admin can disable any module from settings
|
||||
3. ✅ Routes are protected by module flags
|
||||
4. ✅ Navigation automatically reflects module state
|
||||
5. ✅ Module dependencies are enforced
|
||||
6. ✅ All tests pass
|
||||
7. ✅ Documentation is updated
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Module Plugins**
|
||||
- Support for third-party modules
|
||||
- Module marketplace
|
||||
|
||||
2. **Module Permissions**
|
||||
- Fine-grained permissions per module
|
||||
- Role-based module access
|
||||
|
||||
3. **Module Analytics**
|
||||
- Track module usage
|
||||
- Identify unused modules
|
||||
|
||||
4. **Module Templates**
|
||||
- Pre-configured module sets
|
||||
- Industry-specific configurations
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- Current Settings Model: `app/models/settings.py`
|
||||
- Current User Model: `app/models/user.py`
|
||||
- Navigation Template: `app/templates/base.html`
|
||||
- Blueprint Registration: `app/__init__.py`
|
||||
|
||||
---
|
||||
|
||||
**Next Steps:**
|
||||
1. Review and approve this plan
|
||||
2. Create module registry implementation
|
||||
3. Begin Phase 1 implementation
|
||||
|
||||
@@ -0,0 +1,361 @@
|
||||
# Module Structure Analysis
|
||||
|
||||
**Date:** 2025-01-27
|
||||
**Purpose:** Visual representation of current module structure and integration points
|
||||
|
||||
---
|
||||
|
||||
## Module Categories & Current State
|
||||
|
||||
### 📊 Module Distribution
|
||||
|
||||
```
|
||||
Total Modules: 50+
|
||||
├── Core Modules (6) - Always Enabled
|
||||
├── Optional Modules (44+) - Can Be Disabled
|
||||
│ ├── Time Tracking Features (7)
|
||||
│ ├── CRM Features (4)
|
||||
│ ├── Finance & Expenses (12)
|
||||
│ ├── Inventory (1)
|
||||
│ ├── Analytics (1)
|
||||
│ ├── Tools & Data (3)
|
||||
│ ├── Admin Features (8)
|
||||
│ └── Advanced Features (8)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Current Module Inventory
|
||||
|
||||
### 🔵 Core Modules (Always Enabled)
|
||||
|
||||
| Module ID | Blueprint | Routes | Has Flag | Status |
|
||||
|-----------|-----------|--------|----------|--------|
|
||||
| `auth` | `auth_bp` | `/login`, `/logout`, `/profile` | ❌ | ✅ Active |
|
||||
| `main` | `main_bp` | `/dashboard` | ❌ | ✅ Active |
|
||||
| `projects` | `projects_bp` | `/projects/*` | ❌ | ✅ Active |
|
||||
| `timer` | `timer_bp` | `/timer/*` | ❌ | ✅ Active |
|
||||
| `tasks` | `tasks_bp` | `/tasks/*` | ❌ | ✅ Active |
|
||||
| `clients` | `clients_bp` | `/clients/*` | ❌ | ✅ Active |
|
||||
|
||||
**Note:** Core modules should remain always enabled as they form the foundation of the application.
|
||||
|
||||
---
|
||||
|
||||
### ⏱️ Time Tracking Features
|
||||
|
||||
| Module ID | Blueprint | Settings Flag | User Flag | Status |
|
||||
|-----------|-----------|---------------|-----------|--------|
|
||||
| `calendar` | `calendar_bp` | ✅ `ui_allow_calendar` | ✅ `ui_show_calendar` | ✅ Complete |
|
||||
| `project_templates` | `project_templates_bp` | ✅ `ui_allow_project_templates` | ✅ `ui_show_project_templates` | ✅ Complete |
|
||||
| `gantt` | `gantt_bp` | ✅ `ui_allow_gantt_chart` | ✅ `ui_show_gantt_chart` | ✅ Complete |
|
||||
| `kanban` | `kanban_bp` | ✅ `ui_allow_kanban_board` | ✅ `ui_show_kanban_board` | ✅ Complete |
|
||||
| `weekly_goals` | `weekly_goals_bp` | ✅ `ui_allow_weekly_goals` | ✅ `ui_show_weekly_goals` | ✅ Complete |
|
||||
| `issues` | `issues_bp` | ✅ `ui_allow_issues` | ✅ `ui_show_issues` | ✅ Complete |
|
||||
| `time_entry_templates` | `time_entry_templates_bp` | ❌ | ❌ | ⚠️ Missing Flags |
|
||||
|
||||
**Dependencies:**
|
||||
- `gantt` → `tasks`
|
||||
- `kanban` → `tasks`
|
||||
- `project_templates` → `projects`
|
||||
|
||||
---
|
||||
|
||||
### 💼 CRM Features
|
||||
|
||||
| Module ID | Blueprint | Settings Flag | User Flag | Status |
|
||||
|-----------|-----------|---------------|-----------|--------|
|
||||
| `quotes` | `quotes_bp` | ✅ `ui_allow_quotes` | ✅ `ui_show_quotes` | ✅ Complete |
|
||||
| `contacts` | `contacts_bp` | ❌ | ❌ | ⚠️ Missing Flags |
|
||||
| `deals` | `deals_bp` | ❌ | ❌ | ⚠️ Missing Flags |
|
||||
| `leads` | `leads_bp` | ❌ | ❌ | ⚠️ Missing Flags |
|
||||
|
||||
**Dependencies:**
|
||||
- `quotes` → `clients`
|
||||
- `deals` → `clients`
|
||||
- `leads` → `clients`
|
||||
- `contacts` → `clients`
|
||||
|
||||
---
|
||||
|
||||
### 💰 Finance & Expenses
|
||||
|
||||
| Module ID | Blueprint | Settings Flag | User Flag | Status |
|
||||
|-----------|-----------|---------------|-----------|--------|
|
||||
| `reports` | `reports_bp` | ✅ `ui_allow_reports` | ✅ `ui_show_reports` | ✅ Complete |
|
||||
| `custom_reports` | `custom_reports_bp` | ✅ `ui_allow_report_builder` | ✅ `ui_show_report_builder` | ✅ Complete |
|
||||
| `scheduled_reports` | `scheduled_reports_bp` | ✅ `ui_allow_scheduled_reports` | ✅ `ui_show_scheduled_reports` | ✅ Complete |
|
||||
| `invoices` | `invoices_bp` | ❌ | ❌ | ⚠️ Missing Flags |
|
||||
| `invoice_approvals` | `invoice_approvals_bp` | ✅ `ui_allow_invoice_approvals` | ✅ `ui_show_invoice_approvals` | ✅ Complete |
|
||||
| `recurring_invoices` | `recurring_invoices_bp` | ✅ `ui_allow_recurring_invoices` | ✅ `ui_show_recurring_invoices` | ✅ Complete |
|
||||
| `payments` | `payments_bp` | ✅ `ui_allow_payments` | ✅ `ui_show_payments` | ✅ Complete |
|
||||
| `payment_gateways` | `payment_gateways_bp` | ✅ `ui_allow_payment_gateways` | ✅ `ui_show_payment_gateways` | ✅ Complete |
|
||||
| `expenses` | `expenses_bp` | ❌ | ❌ | ⚠️ Missing Flags |
|
||||
| `mileage` | `mileage_bp` | ✅ `ui_allow_mileage` | ✅ `ui_show_mileage` | ✅ Complete |
|
||||
| `per_diem` | `per_diem_bp` | ✅ `ui_allow_per_diem` | ✅ `ui_show_per_diem` | ✅ Complete |
|
||||
| `budget_alerts` | `budget_alerts_bp` | ✅ `ui_allow_budget_alerts` | ✅ `ui_show_budget_alerts` | ✅ Complete |
|
||||
|
||||
**Dependencies:**
|
||||
- `invoices` → `projects` (required)
|
||||
- `payments` → `invoices` (required)
|
||||
- `invoice_approvals` → `invoices`
|
||||
- `recurring_invoices` → `invoices`
|
||||
- `payment_gateways` → `payments`
|
||||
- `expenses` → `projects` (optional)
|
||||
- `budget_alerts` → `projects`
|
||||
|
||||
---
|
||||
|
||||
### 📦 Inventory
|
||||
|
||||
| Module ID | Blueprint | Settings Flag | User Flag | Status |
|
||||
|-----------|-----------|---------------|-----------|--------|
|
||||
| `inventory` | `inventory_bp` | ✅ `ui_allow_inventory` | ✅ `ui_show_inventory` | ✅ Complete |
|
||||
|
||||
**Dependencies:** None (standalone module)
|
||||
|
||||
---
|
||||
|
||||
### 📈 Analytics
|
||||
|
||||
| Module ID | Blueprint | Settings Flag | User Flag | Status |
|
||||
|-----------|-----------|---------------|-----------|--------|
|
||||
| `analytics` | `analytics_bp` | ✅ `ui_allow_analytics` | ✅ `ui_show_analytics` | ✅ Complete |
|
||||
|
||||
**Dependencies:** None (can work independently)
|
||||
|
||||
---
|
||||
|
||||
### 🛠️ Tools & Data
|
||||
|
||||
| Module ID | Blueprint | Settings Flag | User Flag | Status |
|
||||
|-----------|-----------|---------------|-----------|--------|
|
||||
| `integrations` | `integrations_bp` | ❌ | ❌ | ⚠️ Missing Flags |
|
||||
| `import_export` | `import_export_bp` | ❌ | ❌ | ⚠️ Missing Flags |
|
||||
| `saved_filters` | `saved_filters_bp` | ❌ | ❌ | ⚠️ Missing Flags |
|
||||
|
||||
**Note:** These are grouped under `ui_allow_tools` and `ui_show_tools` but individual flags are missing.
|
||||
|
||||
---
|
||||
|
||||
### ⚙️ Admin Features
|
||||
|
||||
| Module ID | Blueprint | Settings Flag | User Flag | Status |
|
||||
|-----------|-----------|---------------|-----------|--------|
|
||||
| `admin` | `admin_bp` | ❌ | ❌ | ⚠️ Admin Only |
|
||||
| `permissions` | `permissions_bp` | ❌ | ❌ | ⚠️ Admin Only |
|
||||
| `settings` | `settings_bp` | ❌ | ❌ | ⚠️ Admin Only |
|
||||
| `audit_logs` | `audit_logs_bp` | ❌ | ❌ | ⚠️ Admin Only |
|
||||
| `webhooks` | `webhooks_bp` | ❌ | ❌ | ⚠️ Admin Only |
|
||||
| `custom_field_definitions` | `custom_field_definitions_bp` | ❌ | ❌ | ⚠️ Admin Only |
|
||||
| `link_templates` | `link_templates_bp` | ❌ | ❌ | ⚠️ Admin Only |
|
||||
| `expense_categories` | `expense_categories_bp` | ❌ | ❌ | ⚠️ Admin Only |
|
||||
|
||||
**Note:** Admin features are typically always visible to admins, but could benefit from flags for role-based access control.
|
||||
|
||||
---
|
||||
|
||||
### 🚀 Advanced Features
|
||||
|
||||
| Module ID | Blueprint | Settings Flag | User Flag | Status |
|
||||
|-----------|-----------|---------------|-----------|--------|
|
||||
| `workflows` | `workflows_bp` | ❌ | ❌ | ⚠️ Missing Flags |
|
||||
| `time_approvals` | `time_approvals_bp` | ❌ | ❌ | ⚠️ Missing Flags |
|
||||
| `activity_feed` | `activity_feed_bp` | ❌ | ❌ | ⚠️ Missing Flags |
|
||||
| `recurring_tasks` | `recurring_tasks_bp` | ❌ | ❌ | ⚠️ Missing Flags |
|
||||
| `team_chat` | `team_chat_bp` | ❌ | ❌ | ⚠️ Missing Flags |
|
||||
| `client_portal` | `client_portal_bp` | ❌ | ❌ | ⚠️ Missing Flags |
|
||||
| `kiosk` | `kiosk_bp` | ❌ | ❌ | ⚠️ Missing Flags |
|
||||
| `client_portal_customization` | `client_portal_customization_bp` | ❌ | ❌ | ⚠️ Missing Flags |
|
||||
|
||||
**Dependencies:**
|
||||
- `time_approvals` → `timer`
|
||||
- `recurring_tasks` → `tasks`
|
||||
- `client_portal` → `clients`
|
||||
|
||||
---
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Current Integration Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Application Startup │
|
||||
│ (app/__init__.py) │
|
||||
└──────────────────────┬──────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────┐
|
||||
│ Blueprint Registration │
|
||||
│ (50+ blueprints) │
|
||||
└──────────────┬───────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────┐
|
||||
│ Navigation Rendering │
|
||||
│ (base.html) │
|
||||
│ - Hardcoded checks │
|
||||
│ - Endpoint matching │
|
||||
│ - Conditional rendering │
|
||||
└──────────────┬───────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────┐
|
||||
│ Route Execution │
|
||||
│ - No module checks │
|
||||
│ - Direct access │
|
||||
└───────────────────────────────┘
|
||||
```
|
||||
|
||||
### Proposed Integration Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Application Startup │
|
||||
│ (app/__init__.py) │
|
||||
└──────────────────────┬──────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────┐
|
||||
│ Module Registry │
|
||||
│ - Module definitions │
|
||||
│ - Dependencies │
|
||||
│ - Metadata │
|
||||
└──────────────┬───────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────┐
|
||||
│ Blueprint Registration │
|
||||
│ (with module metadata) │
|
||||
└──────────────┬───────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────┐
|
||||
│ Navigation Builder │
|
||||
│ - Module-based menu │
|
||||
│ - Dynamic visibility │
|
||||
└──────────────┬───────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────┐
|
||||
│ Route Protection │
|
||||
│ - @module_enabled decorator │
|
||||
│ - Automatic checks │
|
||||
└───────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Module Dependency Graph
|
||||
|
||||
```
|
||||
Core Modules (Always Enabled)
|
||||
├── projects
|
||||
│ ├── invoices (required)
|
||||
│ ├── expenses (optional)
|
||||
│ ├── tasks (optional)
|
||||
│ └── time_entries (required)
|
||||
│
|
||||
├── clients
|
||||
│ ├── quotes
|
||||
│ ├── deals
|
||||
│ ├── leads
|
||||
│ └── contacts
|
||||
│
|
||||
└── tasks
|
||||
├── kanban
|
||||
├── gantt
|
||||
└── recurring_tasks
|
||||
|
||||
Finance Modules
|
||||
├── invoices
|
||||
│ ├── payments (required)
|
||||
│ ├── invoice_approvals
|
||||
│ └── recurring_invoices
|
||||
│
|
||||
└── payments
|
||||
└── payment_gateways
|
||||
|
||||
Time Tracking
|
||||
├── timer
|
||||
│ └── time_approvals
|
||||
│
|
||||
└── projects
|
||||
└── budget_alerts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Statistics
|
||||
|
||||
### Flag Coverage
|
||||
|
||||
- **Modules with Settings Flags:** 20 (40%)
|
||||
- **Modules with User Flags:** 20 (40%)
|
||||
- **Modules Missing Flags:** 30 (60%)
|
||||
|
||||
### Categories Needing Attention
|
||||
|
||||
1. **CRM Features** - 3 of 4 modules missing flags
|
||||
2. **Advanced Features** - 8 of 8 modules missing flags
|
||||
3. **Tools & Data** - 3 of 3 modules missing individual flags
|
||||
4. **Admin Features** - 8 of 8 modules missing flags (may be intentional)
|
||||
|
||||
### Priority for Flag Addition
|
||||
|
||||
**High Priority:**
|
||||
- `invoices` (core finance feature)
|
||||
- `expenses` (core finance feature)
|
||||
- `contacts`, `deals`, `leads` (CRM features)
|
||||
- `workflows`, `time_approvals` (workflow features)
|
||||
|
||||
**Medium Priority:**
|
||||
- `time_entry_templates`
|
||||
- `integrations`, `import_export`, `saved_filters` (individual flags)
|
||||
- `team_chat`, `activity_feed` (collaboration features)
|
||||
|
||||
**Low Priority:**
|
||||
- Admin features (may remain admin-only)
|
||||
- `client_portal_customization` (admin feature)
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
1. **Immediate Actions:**
|
||||
- Add flags for high-priority modules
|
||||
- Create module registry system
|
||||
- Add route protection for critical modules
|
||||
|
||||
2. **Short-term (1-2 weeks):**
|
||||
- Complete flag coverage for all modules
|
||||
- Implement module registry
|
||||
- Refactor navigation
|
||||
|
||||
3. **Medium-term (1 month):**
|
||||
- Admin UI for module management
|
||||
- Dependency validation
|
||||
- Comprehensive testing
|
||||
|
||||
4. **Long-term (Future):**
|
||||
- Module plugin system
|
||||
- Module marketplace
|
||||
- Advanced permission system
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. ✅ Review module inventory
|
||||
2. ✅ Identify missing flags
|
||||
3. ⏳ Create module registry
|
||||
4. ⏳ Add missing flags
|
||||
5. ⏳ Implement route protection
|
||||
6. ⏳ Refactor navigation
|
||||
7. ⏳ Create admin UI
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2025-01-27
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
"""Add missing module visibility flags to settings and users
|
||||
|
||||
Revision ID: 092_missing_module_flags
|
||||
Revises: 091
|
||||
Create Date: 2025-01-27 12:00:00
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "092_missing_module_flags"
|
||||
down_revision = "090_report_builder_iteration" # Update this to the latest migration
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
"""Add missing module visibility flags to settings and users tables."""
|
||||
bind = op.get_bind()
|
||||
inspector = sa.inspect(bind)
|
||||
|
||||
# Determine database dialect for proper default values
|
||||
dialect_name = bind.dialect.name if bind else "generic"
|
||||
|
||||
# Set appropriate boolean defaults based on database
|
||||
if dialect_name == 'sqlite':
|
||||
bool_true_default = '1'
|
||||
elif dialect_name == 'postgresql':
|
||||
bool_true_default = 'true'
|
||||
else: # MySQL/MariaDB and others
|
||||
bool_true_default = '1'
|
||||
|
||||
# Helper to add a boolean column with server default true
|
||||
def _add_bool_column(table_name: str, column_name: str):
|
||||
try:
|
||||
# Check if table exists
|
||||
table_names = set(inspector.get_table_names())
|
||||
if table_name not in table_names:
|
||||
print(f"⚠ {table_name} table does not exist, skipping {column_name} column")
|
||||
return
|
||||
|
||||
# Check if column already exists
|
||||
current_cols = {c['name'] for c in inspector.get_columns(table_name)}
|
||||
if column_name in current_cols:
|
||||
print(f"✓ Column {column_name} already exists in {table_name} table")
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"⚠ Warning checking for {column_name} column: {e}")
|
||||
|
||||
try:
|
||||
op.add_column(
|
||||
table_name,
|
||||
sa.Column(column_name, sa.Boolean(), nullable=False, server_default=sa.text(bool_true_default)),
|
||||
)
|
||||
print(f"✓ Added {column_name} column to {table_name} table")
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
if 'already exists' in error_msg.lower() or 'duplicate' in error_msg.lower():
|
||||
print(f"✓ Column {column_name} already exists in {table_name} table (detected via error)")
|
||||
else:
|
||||
print(f"✗ Error adding {column_name} column to {table_name} table: {e}")
|
||||
raise
|
||||
|
||||
# Settings table - Tools & Data section
|
||||
_add_bool_column("settings", "ui_allow_integrations")
|
||||
_add_bool_column("settings", "ui_allow_import_export")
|
||||
_add_bool_column("settings", "ui_allow_saved_filters")
|
||||
|
||||
# Settings table - CRM section (additional)
|
||||
_add_bool_column("settings", "ui_allow_contacts")
|
||||
_add_bool_column("settings", "ui_allow_deals")
|
||||
_add_bool_column("settings", "ui_allow_leads")
|
||||
|
||||
# Settings table - Finance section (additional)
|
||||
_add_bool_column("settings", "ui_allow_invoices")
|
||||
_add_bool_column("settings", "ui_allow_expenses")
|
||||
|
||||
# Settings table - Time Tracking section (additional)
|
||||
_add_bool_column("settings", "ui_allow_time_entry_templates")
|
||||
|
||||
# Settings table - Advanced features
|
||||
_add_bool_column("settings", "ui_allow_workflows")
|
||||
_add_bool_column("settings", "ui_allow_time_approvals")
|
||||
_add_bool_column("settings", "ui_allow_activity_feed")
|
||||
_add_bool_column("settings", "ui_allow_recurring_tasks")
|
||||
_add_bool_column("settings", "ui_allow_team_chat")
|
||||
_add_bool_column("settings", "ui_allow_client_portal")
|
||||
_add_bool_column("settings", "ui_allow_kiosk")
|
||||
|
||||
# Users table - Tools & Data section
|
||||
_add_bool_column("users", "ui_show_integrations")
|
||||
_add_bool_column("users", "ui_show_import_export")
|
||||
_add_bool_column("users", "ui_show_saved_filters")
|
||||
|
||||
# Users table - CRM section (additional)
|
||||
_add_bool_column("users", "ui_show_contacts")
|
||||
_add_bool_column("users", "ui_show_deals")
|
||||
_add_bool_column("users", "ui_show_leads")
|
||||
|
||||
# Users table - Finance section (additional)
|
||||
_add_bool_column("users", "ui_show_invoices")
|
||||
_add_bool_column("users", "ui_show_expenses")
|
||||
|
||||
# Users table - Time Tracking section (additional)
|
||||
_add_bool_column("users", "ui_show_time_entry_templates")
|
||||
|
||||
# Users table - Advanced features
|
||||
_add_bool_column("users", "ui_show_workflows")
|
||||
_add_bool_column("users", "ui_show_time_approvals")
|
||||
_add_bool_column("users", "ui_show_activity_feed")
|
||||
_add_bool_column("users", "ui_show_recurring_tasks")
|
||||
_add_bool_column("users", "ui_show_team_chat")
|
||||
_add_bool_column("users", "ui_show_client_portal")
|
||||
_add_bool_column("users", "ui_show_kiosk")
|
||||
|
||||
|
||||
def downgrade():
|
||||
"""Remove missing module visibility flags from settings and users tables."""
|
||||
columns_to_drop_settings = [
|
||||
"ui_allow_kiosk",
|
||||
"ui_allow_client_portal",
|
||||
"ui_allow_team_chat",
|
||||
"ui_allow_recurring_tasks",
|
||||
"ui_allow_activity_feed",
|
||||
"ui_allow_time_approvals",
|
||||
"ui_allow_workflows",
|
||||
"ui_allow_time_entry_templates",
|
||||
"ui_allow_expenses",
|
||||
"ui_allow_invoices",
|
||||
"ui_allow_leads",
|
||||
"ui_allow_deals",
|
||||
"ui_allow_contacts",
|
||||
"ui_allow_saved_filters",
|
||||
"ui_allow_import_export",
|
||||
"ui_allow_integrations",
|
||||
]
|
||||
|
||||
columns_to_drop_users = [
|
||||
"ui_show_kiosk",
|
||||
"ui_show_client_portal",
|
||||
"ui_show_team_chat",
|
||||
"ui_show_recurring_tasks",
|
||||
"ui_show_activity_feed",
|
||||
"ui_show_time_approvals",
|
||||
"ui_show_workflows",
|
||||
"ui_show_time_entry_templates",
|
||||
"ui_show_expenses",
|
||||
"ui_show_invoices",
|
||||
"ui_show_leads",
|
||||
"ui_show_deals",
|
||||
"ui_show_contacts",
|
||||
"ui_show_saved_filters",
|
||||
"ui_show_import_export",
|
||||
"ui_show_integrations",
|
||||
]
|
||||
|
||||
for name in columns_to_drop_settings:
|
||||
try:
|
||||
op.drop_column("settings", name)
|
||||
print(f"✓ Dropped {name} column from settings table")
|
||||
except Exception as e:
|
||||
print(f"⚠ Warning dropping {name} column from settings table: {e}")
|
||||
|
||||
for name in columns_to_drop_users:
|
||||
try:
|
||||
op.drop_column("users", name)
|
||||
print(f"✓ Dropped {name} column from users table")
|
||||
except Exception as e:
|
||||
print(f"⚠ Warning dropping {name} column from users table: {e}")
|
||||
|
||||
Reference in New Issue
Block a user