Files
TimeTracker/app/routes/contacts.py
T
Dries Peeters b4486a627f fix: CI tests, code quality, and duplicate DB indexes
- Webhook models: remove duplicate index definitions so db.create_all()
  no longer raises 'index already exists' (columns already have index=True)
- ImportService: fix circular import by late-importing ClientService,
  ProjectService, TimeTrackingService in __init__
- reports: fix F823 by renaming unpack variable _ to _entry_count to avoid
  shadowing gettext _ in export_task_excel()
- Code quality: add .flake8 with extend-ignore so flake8 CI passes;
  simplify pyproject.toml isort config (drop unsupported options)
- Format: run black and isort on app/
- tests: restore minimal app fixture in test_import_export_models
2026-03-15 10:51:52 +01:00

199 lines
8.4 KiB
Python

"""Routes for contact management"""
from datetime import datetime
from flask import Blueprint, flash, jsonify, redirect, render_template, request, url_for
from flask_babel import gettext as _
from flask_login import current_user, login_required
from app import db
from app.models import Client, Contact, ContactCommunication
from app.utils.db import safe_commit
from app.utils.module_helpers import module_enabled
from app.utils.timezone import parse_local_datetime
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)
contacts = Contact.get_active_contacts(client_id)
return render_template("contacts/list.html", client=client, contacts=contacts)
@contacts_bp.route("/clients/<int:client_id>/contacts/create", methods=["GET", "POST"])
@login_required
def create_contact(client_id):
"""Create a new contact for a client"""
client = Client.query.get_or_404(client_id)
if request.method == "POST":
try:
contact = Contact(
client_id=client_id,
first_name=request.form.get("first_name", "").strip(),
last_name=request.form.get("last_name", "").strip(),
created_by=current_user.id,
email=request.form.get("email", "").strip() or None,
phone=request.form.get("phone", "").strip() or None,
mobile=request.form.get("mobile", "").strip() or None,
title=request.form.get("title", "").strip() or None,
department=request.form.get("department", "").strip() or None,
role=request.form.get("role", "contact").strip() or "contact",
is_primary=request.form.get("is_primary") == "on",
address=request.form.get("address", "").strip() or None,
notes=request.form.get("notes", "").strip() or None,
tags=request.form.get("tags", "").strip() or None,
)
db.session.add(contact)
# If this is set as primary, unset others
if contact.is_primary:
Contact.query.filter(
Contact.client_id == client_id, Contact.id != contact.id, Contact.is_primary == True
).update({"is_primary": False})
if safe_commit():
flash(_("Contact created successfully"), "success")
return redirect(url_for("contacts.list_contacts", client_id=client_id))
except Exception as e:
db.session.rollback()
flash(_("Error creating contact: %(error)s", error=str(e)), "error")
return render_template("contacts/form.html", client=client, contact=None)
@contacts_bp.route("/contacts/<int:contact_id>")
@login_required
def view_contact(contact_id):
"""View a contact"""
contact = Contact.query.get_or_404(contact_id)
communications = ContactCommunication.get_recent_communications(contact_id, limit=20)
return render_template("contacts/view.html", contact=contact, communications=communications)
@contacts_bp.route("/contacts/<int:contact_id>/edit", methods=["GET", "POST"])
@login_required
def edit_contact(contact_id):
"""Edit a contact"""
contact = Contact.query.get_or_404(contact_id)
if request.method == "POST":
try:
contact.first_name = request.form.get("first_name", "").strip()
contact.last_name = request.form.get("last_name", "").strip()
contact.email = request.form.get("email", "").strip() or None
contact.phone = request.form.get("phone", "").strip() or None
contact.mobile = request.form.get("mobile", "").strip() or None
contact.title = request.form.get("title", "").strip() or None
contact.department = request.form.get("department", "").strip() or None
contact.role = request.form.get("role", "contact").strip() or "contact"
contact.is_primary = request.form.get("is_primary") == "on"
contact.address = request.form.get("address", "").strip() or None
contact.notes = request.form.get("notes", "").strip() or None
contact.tags = request.form.get("tags", "").strip() or None
contact.updated_at = datetime.utcnow()
# If this is set as primary, unset others
if contact.is_primary:
Contact.query.filter(
Contact.client_id == contact.client_id, Contact.id != contact.id, Contact.is_primary == True
).update({"is_primary": False})
if safe_commit():
flash(_("Contact updated successfully"), "success")
return redirect(url_for("contacts.view_contact", contact_id=contact_id))
except Exception as e:
db.session.rollback()
flash(_("Error updating contact: %(error)s", error=str(e)), "error")
return render_template("contacts/form.html", client=contact.client, contact=contact)
@contacts_bp.route("/contacts/<int:contact_id>/delete", methods=["POST"])
@login_required
def delete_contact(contact_id):
"""Delete a contact (soft delete by setting is_active=False)"""
contact = Contact.query.get_or_404(contact_id)
try:
contact.is_active = False
contact.updated_at = datetime.utcnow()
if safe_commit():
flash(_("Contact deleted successfully"), "success")
except Exception as e:
db.session.rollback()
flash(_("Error deleting contact: %(error)s", error=str(e)), "error")
return redirect(url_for("contacts.list_contacts", client_id=contact.client_id))
@contacts_bp.route("/contacts/<int:contact_id>/set-primary", methods=["POST"])
@login_required
def set_primary_contact(contact_id):
"""Set a contact as primary"""
contact = Contact.query.get_or_404(contact_id)
try:
contact.set_as_primary()
if safe_commit():
flash(_("Contact set as primary"), "success")
except Exception as e:
db.session.rollback()
flash(_("Error setting primary contact: %(error)s", error=str(e)), "error")
return redirect(url_for("contacts.list_contacts", client_id=contact.client_id))
@contacts_bp.route("/contacts/<int:contact_id>/communications/create", methods=["GET", "POST"])
@login_required
def create_communication(contact_id):
"""Create a communication record for a contact"""
contact = Contact.query.get_or_404(contact_id)
if request.method == "POST":
try:
comm_date_str = request.form.get("communication_date", "")
comm_date = parse_local_datetime(comm_date_str) if comm_date_str else datetime.utcnow()
follow_up_str = request.form.get("follow_up_date", "")
follow_up_date = parse_local_datetime(follow_up_str) if follow_up_str else None
communication = ContactCommunication(
contact_id=contact_id,
type=request.form.get("type", "note").strip(),
created_by=current_user.id,
subject=request.form.get("subject", "").strip() or None,
content=request.form.get("content", "").strip() or None,
direction=request.form.get("direction", "outbound").strip(),
status=request.form.get("status", "completed").strip() or None,
communication_date=comm_date,
follow_up_date=follow_up_date,
related_project_id=(
int(request.form.get("related_project_id")) if request.form.get("related_project_id") else None
),
related_quote_id=(
int(request.form.get("related_quote_id")) if request.form.get("related_quote_id") else None
),
related_deal_id=(
int(request.form.get("related_deal_id")) if request.form.get("related_deal_id") else None
),
)
db.session.add(communication)
if safe_commit():
flash(_("Communication recorded successfully"), "success")
return redirect(url_for("contacts.view_contact", contact_id=contact_id))
except Exception as e:
db.session.rollback()
flash(_("Error recording communication: %(error)s", error=str(e)), "error")
return render_template("contacts/communication_form.html", contact=contact, communication=None)