mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-24 07:10:21 -05:00
7791e6ada0
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
319 lines
12 KiB
Python
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"))
|