Files
TimeTracker/app/routes/user.py
T
Dries Peeters 7791e6ada0 feat: Add comprehensive issue/bug tracking system
Implement a complete issue management system with client portal integration
and internal admin interface for tracking and resolving client-reported issues.

Features:
- New Issue model with full lifecycle management (open, in_progress, resolved, closed, cancelled)
- Priority levels (low, medium, high, urgent) with visual indicators
- Issue linking to projects and tasks
- Create tasks directly from issues
- Client portal integration for issue reporting and viewing
- Internal admin routes for issue management, filtering, and assignment
- Comprehensive templates for both client and admin views
- Status filtering and search functionality
- Issue assignment to internal users
- Automatic timestamp tracking (created, updated, resolved, closed)

Client Portal:
- Clients can report new issues with project association
- View all issues with status filtering
- View individual issue details
- Submit issues with optional submitter name/email

Admin Interface:
- List all issues with advanced filtering (status, priority, client, project, assignee, search)
- View, edit, and delete issues
- Link issues to existing tasks
- Create tasks from issues
- Update issue status, priority, and assignment
- Issue statistics dashboard

Technical:
- Added Issue model with relationships to Client, Project, Task, and User
- New issues blueprint for internal management
- Extended client_portal routes with issue endpoints
- Updated model imports and relationships
- Added navigation links in base templates
- Version bump to 4.6.0
- Code cleanup in docker scripts and schema verification
2025-12-14 07:25:42 +01:00

319 lines
12 KiB
Python

"""User profile and settings routes"""
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
from flask_login import login_required, current_user
from app import db
from app.models import User, Activity
from app.utils.db import safe_commit
from flask_babel import gettext as _
import pytz
from app.utils.timezone import get_available_timezones
user_bp = Blueprint("user", __name__)
@user_bp.route("/profile")
@login_required
def profile():
"""User profile page"""
# Get user statistics
total_hours = current_user.total_hours
active_timer = current_user.active_timer
recent_entries = current_user.get_recent_entries(limit=10)
# Get recent activities
recent_activities = Activity.get_recent(user_id=current_user.id, limit=20)
return render_template(
"user/profile.html",
user=current_user,
total_hours=total_hours,
active_timer=active_timer,
recent_entries=recent_entries,
recent_activities=recent_activities,
)
@user_bp.route("/settings", methods=["GET", "POST"])
@login_required
def settings():
"""User settings and preferences page"""
if request.method == "POST":
try:
# Notification preferences
current_user.email_notifications = "email_notifications" in request.form
current_user.notification_overdue_invoices = "notification_overdue_invoices" in request.form
current_user.notification_task_assigned = "notification_task_assigned" in request.form
current_user.notification_task_comments = "notification_task_comments" in request.form
current_user.notification_weekly_summary = "notification_weekly_summary" in request.form
# Profile information
full_name = request.form.get("full_name", "").strip()
if full_name:
current_user.full_name = full_name
email = request.form.get("email", "").strip()
if email:
current_user.email = email
# Display preferences
theme_preference = request.form.get("theme_preference")
if theme_preference in ["light", "dark", None, ""]:
current_user.theme_preference = theme_preference if theme_preference else None
# Regional settings
timezone = request.form.get("timezone")
if timezone is not None:
timezone = timezone.strip()
if timezone == "":
current_user.timezone = None
else:
try:
# Validate timezone
pytz.timezone(timezone)
current_user.timezone = timezone
except pytz.exceptions.UnknownTimeZoneError:
flash(_("Invalid timezone selected"), "error")
return redirect(url_for("user.settings"))
date_format = request.form.get("date_format")
if date_format:
current_user.date_format = date_format
time_format = request.form.get("time_format")
if time_format in ["12h", "24h"]:
current_user.time_format = time_format
week_start_day = request.form.get("week_start_day", type=int)
if week_start_day is not None and 0 <= week_start_day <= 6:
current_user.week_start_day = week_start_day
# Language preference
preferred_language = request.form.get("preferred_language")
if preferred_language is not None: # Allow empty string to clear preference
current_user.preferred_language = preferred_language if preferred_language else None
# Also update session for immediate effect
from flask import session
if preferred_language:
session["preferred_language"] = preferred_language
session.permanent = True
session.modified = True
else:
session.pop("preferred_language", None)
session.modified = True
# Time rounding preferences
current_user.time_rounding_enabled = "time_rounding_enabled" in request.form
time_rounding_minutes = request.form.get("time_rounding_minutes", type=int)
if time_rounding_minutes and time_rounding_minutes in [1, 5, 10, 15, 30, 60]:
current_user.time_rounding_minutes = time_rounding_minutes
time_rounding_method = request.form.get("time_rounding_method")
if time_rounding_method in ["nearest", "up", "down"]:
current_user.time_rounding_method = time_rounding_method
# Overtime settings
standard_hours_per_day = request.form.get("standard_hours_per_day", type=float)
if standard_hours_per_day is not None:
# Validate range (0.5 to 24 hours)
if 0.5 <= standard_hours_per_day <= 24:
current_user.standard_hours_per_day = standard_hours_per_day
else:
flash(_("Standard hours per day must be between 0.5 and 24"), "error")
return redirect(url_for("user.settings"))
# UI feature flags - Calendar
current_user.ui_show_calendar = "ui_show_calendar" in request.form
# UI feature flags - Time Tracking
current_user.ui_show_project_templates = "ui_show_project_templates" in request.form
current_user.ui_show_gantt_chart = "ui_show_gantt_chart" in request.form
current_user.ui_show_kanban_board = "ui_show_kanban_board" in request.form
current_user.ui_show_weekly_goals = "ui_show_weekly_goals" in request.form
current_user.ui_show_issues = "ui_show_issues" in request.form
# UI feature flags - CRM
current_user.ui_show_quotes = "ui_show_quotes" in request.form
# UI feature flags - Finance & Expenses
current_user.ui_show_reports = "ui_show_reports" in request.form
current_user.ui_show_report_builder = "ui_show_report_builder" in request.form
current_user.ui_show_scheduled_reports = "ui_show_scheduled_reports" in request.form
current_user.ui_show_invoice_approvals = "ui_show_invoice_approvals" in request.form
current_user.ui_show_payment_gateways = "ui_show_payment_gateways" in request.form
current_user.ui_show_recurring_invoices = "ui_show_recurring_invoices" in request.form
current_user.ui_show_payments = "ui_show_payments" in request.form
current_user.ui_show_mileage = "ui_show_mileage" in request.form
current_user.ui_show_per_diem = "ui_show_per_diem" in request.form
current_user.ui_show_budget_alerts = "ui_show_budget_alerts" in request.form
# UI feature flags - Inventory
current_user.ui_show_inventory = "ui_show_inventory" in request.form
# UI feature flags - Analytics
current_user.ui_show_analytics = "ui_show_analytics" in request.form
# UI feature flags - Tools
current_user.ui_show_tools = "ui_show_tools" in request.form
# Save changes
if safe_commit(db.session):
# Log activity
Activity.log(
user_id=current_user.id,
action="updated",
entity_type="user",
entity_id=current_user.id,
entity_name=current_user.username,
description="Updated user settings",
)
flash(_("Settings saved successfully"), "success")
else:
flash(_("Error saving settings"), "error")
except Exception as e:
flash(_("Error saving settings: %(error)s", error=str(e)), "error")
db.session.rollback()
return redirect(url_for("user.settings"))
# Get all available timezones
timezones = get_available_timezones()
# Get available languages from config
from flask import current_app
languages = current_app.config.get(
"LANGUAGES",
{"en": "English", "nl": "Nederlands", "de": "Deutsch", "fr": "Français", "it": "Italiano", "fi": "Suomi"},
)
# Get time rounding options
from app.utils.time_rounding import get_available_rounding_intervals, get_available_rounding_methods
rounding_intervals = get_available_rounding_intervals()
rounding_methods = get_available_rounding_methods()
return render_template(
"user/settings.html",
user=current_user,
timezones=timezones,
languages=languages,
rounding_intervals=rounding_intervals,
rounding_methods=rounding_methods,
)
@user_bp.route("/api/preferences", methods=["PATCH"])
@login_required
def update_preferences():
"""API endpoint to update user preferences (for AJAX calls)"""
try:
data = request.get_json()
if "theme_preference" in data:
theme = data["theme_preference"]
if theme in ["light", "dark", "system", None, ""]:
current_user.theme_preference = theme if theme and theme != "system" else None
if "email_notifications" in data:
current_user.email_notifications = bool(data["email_notifications"])
if "timezone" in data:
tz_value = data["timezone"]
if tz_value in [None, "", "system"]:
current_user.timezone = None
else:
try:
pytz.timezone(tz_value)
current_user.timezone = tz_value
except pytz.exceptions.UnknownTimeZoneError:
return jsonify({"error": "Invalid timezone"}), 400
db.session.commit()
return jsonify({"success": True, "message": _("Preferences updated")})
except Exception as e:
db.session.rollback()
return jsonify({"error": str(e)}), 500
@user_bp.route("/api/theme", methods=["POST"])
@login_required
def set_theme():
"""Quick API endpoint to set theme (for theme switcher)"""
try:
data = request.get_json()
theme = data.get("theme")
if theme in ["light", "dark", None, ""]:
current_user.theme_preference = theme if theme else None
db.session.commit()
return jsonify({"success": True, "theme": current_user.theme_preference or "system"})
return jsonify({"error": "Invalid theme"}), 400
except Exception as e:
db.session.rollback()
return jsonify({"error": str(e)}), 500
@user_bp.route("/api/language", methods=["POST"])
@login_required
def set_language():
"""Quick API endpoint to set language (for language switcher)"""
from flask import current_app, session
try:
data = request.get_json()
language = data.get("language")
# Get available languages from config
available_languages = current_app.config.get("LANGUAGES", {})
if language in available_languages:
# Update user preference
current_user.preferred_language = language
db.session.commit()
# Also set in session for immediate effect
session["preferred_language"] = language
return jsonify({"success": True, "language": language, "message": _("Language updated successfully")})
return jsonify({"error": _("Invalid language")}), 400
except Exception as e:
db.session.rollback()
return jsonify({"error": str(e)}), 500
@user_bp.route("/set-language/<language>")
def set_language_direct(language):
"""Direct route to set language (for non-JS fallback)"""
from flask import current_app, session
# Get available languages from config
available_languages = current_app.config.get("LANGUAGES", {})
if language in available_languages:
# Set in session for immediate effect
session["preferred_language"] = language
# If user is logged in, update their preference
if current_user.is_authenticated:
current_user.preferred_language = language
db.session.commit()
flash(_("Language updated to %(language)s", language=available_languages[language]), "success")
# Redirect back to referring page or dashboard
next_page = request.referrer or url_for("main.dashboard")
return redirect(next_page)
flash(_("Invalid language"), "error")
return redirect(url_for("main.dashboard"))