mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-24 07:10:21 -05:00
b4486a627f
- 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
137 lines
5.1 KiB
Python
137 lines
5.1 KiB
Python
"""
|
|
API v1 - Contacts (CRM) sub-blueprint.
|
|
Routes under /api/v1/clients/<id>/contacts and /api/v1/contacts.
|
|
"""
|
|
|
|
from flask import Blueprint, g, jsonify, request
|
|
|
|
from app import db
|
|
from app.models import Client, Contact
|
|
from app.routes.api_v1_common import _require_module_enabled_for_api
|
|
from app.utils.api_auth import require_api_token
|
|
from app.utils.api_responses import error_response, forbidden_response
|
|
|
|
api_v1_contacts_bp = Blueprint("api_v1_contacts", __name__, url_prefix="/api/v1")
|
|
|
|
|
|
@api_v1_contacts_bp.route("/clients/<int:client_id>/contacts", methods=["GET"])
|
|
@require_api_token("read:contacts")
|
|
def list_contacts(client_id):
|
|
"""List contacts for a client."""
|
|
blocked = _require_module_enabled_for_api("contacts")
|
|
if blocked:
|
|
return blocked
|
|
from app.utils.scope_filter import user_can_access_client
|
|
|
|
client = Client.query.filter_by(id=client_id).first_or_404()
|
|
if not user_can_access_client(g.api_user, client_id):
|
|
return jsonify({"error": "Access denied", "message": "You do not have access to this client"}), 403
|
|
contacts = Contact.get_active_contacts(client_id)
|
|
return jsonify({"contacts": [c.to_dict() for c in contacts]})
|
|
|
|
|
|
@api_v1_contacts_bp.route("/clients/<int:client_id>/contacts", methods=["POST"])
|
|
@require_api_token("write:contacts")
|
|
def create_contact(client_id):
|
|
"""Create a contact for a client."""
|
|
blocked = _require_module_enabled_for_api("contacts")
|
|
if blocked:
|
|
return blocked
|
|
from app.utils.scope_filter import user_can_access_client
|
|
|
|
client = Client.query.filter_by(id=client_id).first_or_404()
|
|
if not user_can_access_client(g.api_user, client_id):
|
|
return forbidden_response("You do not have access to this client")
|
|
data = request.get_json() or {}
|
|
first_name = (data.get("first_name") or "").strip()
|
|
last_name = (data.get("last_name") or "").strip()
|
|
if not first_name or not last_name:
|
|
return error_response("first_name and last_name are required", status_code=400)
|
|
contact = Contact(
|
|
client_id=client_id,
|
|
first_name=first_name,
|
|
last_name=last_name,
|
|
created_by=g.api_user.id,
|
|
email=(data.get("email") or "").strip() or None,
|
|
phone=(data.get("phone") or "").strip() or None,
|
|
mobile=(data.get("mobile") or "").strip() or None,
|
|
title=(data.get("title") or "").strip() or None,
|
|
department=(data.get("department") or "").strip() or None,
|
|
role=(data.get("role") or "contact").strip(),
|
|
is_primary=bool(data.get("is_primary", False)),
|
|
address=(data.get("address") or "").strip() or None,
|
|
notes=(data.get("notes") or "").strip() or None,
|
|
tags=(data.get("tags") or "").strip() or None,
|
|
)
|
|
db.session.add(contact)
|
|
if contact.is_primary:
|
|
Contact.query.filter(
|
|
Contact.client_id == client_id, Contact.id != contact.id, Contact.is_primary == True
|
|
).update({"is_primary": False})
|
|
db.session.commit()
|
|
return jsonify({"message": "Contact created successfully", "contact": contact.to_dict()}), 201
|
|
|
|
|
|
@api_v1_contacts_bp.route("/contacts/<int:contact_id>", methods=["GET"])
|
|
@require_api_token("read:contacts")
|
|
def get_contact(contact_id):
|
|
"""Get a contact by id."""
|
|
blocked = _require_module_enabled_for_api("contacts")
|
|
if blocked:
|
|
return blocked
|
|
contact = Contact.query.filter_by(id=contact_id).first_or_404()
|
|
return jsonify({"contact": contact.to_dict()})
|
|
|
|
|
|
@api_v1_contacts_bp.route("/contacts/<int:contact_id>", methods=["PUT", "PATCH"])
|
|
@require_api_token("write:contacts")
|
|
def update_contact(contact_id):
|
|
"""Update a contact."""
|
|
blocked = _require_module_enabled_for_api("contacts")
|
|
if blocked:
|
|
return blocked
|
|
contact = Contact.query.filter_by(id=contact_id).first_or_404()
|
|
data = request.get_json() or {}
|
|
for field in (
|
|
"first_name",
|
|
"last_name",
|
|
"email",
|
|
"phone",
|
|
"mobile",
|
|
"title",
|
|
"department",
|
|
"role",
|
|
"address",
|
|
"notes",
|
|
"tags",
|
|
):
|
|
if field in data and data[field] is not None:
|
|
setattr(
|
|
contact,
|
|
field,
|
|
str(data[field]).strip() if isinstance(data[field], str) else data[field],
|
|
)
|
|
if "is_primary" in data:
|
|
contact.is_primary = bool(data["is_primary"])
|
|
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})
|
|
db.session.commit()
|
|
return jsonify({"message": "Contact updated successfully", "contact": contact.to_dict()})
|
|
|
|
|
|
@api_v1_contacts_bp.route("/contacts/<int:contact_id>", methods=["DELETE"])
|
|
@require_api_token("write:contacts")
|
|
def delete_contact(contact_id):
|
|
"""Soft-delete a contact (set is_active=False)."""
|
|
blocked = _require_module_enabled_for_api("contacts")
|
|
if blocked:
|
|
return blocked
|
|
contact = Contact.query.filter_by(id=contact_id).first_or_404()
|
|
contact.is_active = False
|
|
db.session.commit()
|
|
return jsonify({"message": "Contact deleted successfully"})
|