diff --git a/app/contacts/backend/router.py b/app/contacts/backend/router.py index a6fd305..faab68a 100644 --- a/app/contacts/backend/router.py +++ b/app/contacts/backend/router.py @@ -6,6 +6,15 @@ Handles contact CRUD operations with multi-company support from fastapi import APIRouter, HTTPException, Query from typing import Optional, List from app.core.database import execute_query, execute_insert, execute_update +from app.core.contact_utils import get_contact_customer_ids, get_primary_customer_id +from app.customers.backend.router import ( + get_customer_subscriptions, + lock_customer_subscriptions, + save_subscription_comment, + get_subscription_comment, + get_subscription_billing_matrix, + SubscriptionComment, +) import logging logger = logging.getLogger(__name__) @@ -358,3 +367,88 @@ async def unlink_contact_from_company(contact_id: int, customer_id: int): except Exception as e: logger.error(f"Failed to unlink contact from company: {e}") raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/contacts/{contact_id}/related-contacts", response_model=dict) +async def get_related_contacts(contact_id: int): + """ + Get contacts from the same companies as the contact (excluding itself). + """ + try: + customer_ids = get_contact_customer_ids(contact_id) + if not customer_ids: + return {"contacts": []} + + placeholders = ",".join(["%s"] * len(customer_ids)) + query = f""" + SELECT + c.id, c.first_name, c.last_name, c.email, c.phone, c.mobile, + c.title, c.department, c.is_active, c.vtiger_id, + c.created_at, c.updated_at, + ARRAY_AGG(DISTINCT cu.name ORDER BY cu.name) FILTER (WHERE cu.name IS NOT NULL) as company_names + FROM contacts c + JOIN contact_companies cc ON c.id = cc.contact_id + JOIN customers cu ON cc.customer_id = cu.id + WHERE cc.customer_id IN ({placeholders}) AND c.id <> %s + GROUP BY c.id, c.first_name, c.last_name, c.email, c.phone, c.mobile, + c.title, c.department, c.is_active, c.vtiger_id, c.created_at, c.updated_at + ORDER BY c.last_name, c.first_name + """ + params = tuple(customer_ids + [contact_id]) + results = execute_query(query, params) or [] + return {"contacts": results} + + except Exception as e: + logger.error(f"Failed to get related contacts for {contact_id}: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/contacts/{contact_id}/subscriptions") +async def get_contact_subscriptions(contact_id: int): + customer_id = get_primary_customer_id(contact_id) + if not customer_id: + return { + "status": "no_linked_customer", + "message": "Kontakt er ikke tilknyttet et firma", + "recurring_orders": [], + "sales_orders": [], + "subscriptions": [], + "expired_subscriptions": [], + "bmc_office_subscriptions": [], + } + return await get_customer_subscriptions(customer_id) + + +@router.post("/contacts/{contact_id}/subscriptions/lock") +async def lock_contact_subscriptions(contact_id: int, lock_request: dict): + customer_id = get_primary_customer_id(contact_id) + if not customer_id: + raise HTTPException(status_code=404, detail="Kontakt har ingen tilknyttet kunde") + return await lock_customer_subscriptions(customer_id, lock_request) + + +@router.post("/contacts/{contact_id}/subscription-comment") +async def save_contact_subscription_comment(contact_id: int, data: SubscriptionComment): + customer_id = get_primary_customer_id(contact_id) + if not customer_id: + raise HTTPException(status_code=404, detail="Kontakt har ingen tilknyttet kunde") + return await save_subscription_comment(customer_id, data) + + +@router.get("/contacts/{contact_id}/subscription-comment") +async def get_contact_subscription_comment(contact_id: int): + customer_id = get_primary_customer_id(contact_id) + if not customer_id: + raise HTTPException(status_code=404, detail="Kontakt har ingen tilknyttet kunde") + return await get_subscription_comment(customer_id) + + +@router.get("/contacts/{contact_id}/subscriptions/billing-matrix") +async def get_contact_subscription_billing_matrix( + contact_id: int, + months: int = Query(default=12, ge=1, le=60, description="Number of months to show"), +): + customer_id = get_primary_customer_id(contact_id) + if not customer_id: + raise HTTPException(status_code=404, detail="Kontakt har ingen tilknyttet kunde") + return await get_subscription_billing_matrix(customer_id, months) diff --git a/app/contacts/backend/router_simple.py b/app/contacts/backend/router_simple.py index d862e7b..21e32b7 100644 --- a/app/contacts/backend/router_simple.py +++ b/app/contacts/backend/router_simple.py @@ -7,6 +7,15 @@ from fastapi import APIRouter, HTTPException, Query, Body, status from typing import Optional from pydantic import BaseModel, Field from app.core.database import execute_query, execute_insert +from app.core.contact_utils import get_contact_customer_ids, get_primary_customer_id +from app.customers.backend.router import ( + get_customer_subscriptions, + lock_customer_subscriptions, + save_subscription_comment, + get_subscription_comment, + get_subscription_billing_matrix, + SubscriptionComment, +) import logging logger = logging.getLogger(__name__) @@ -314,3 +323,86 @@ async def link_contact_to_company(contact_id: int, link: ContactCompanyLink): except Exception as e: logger.error(f"Failed to link contact to company: {e}") raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/contacts/{contact_id}/related-contacts") +async def get_related_contacts(contact_id: int): + """Get contacts from the same companies as the contact (excluding itself).""" + try: + customer_ids = get_contact_customer_ids(contact_id) + if not customer_ids: + return {"contacts": []} + + placeholders = ",".join(["%s"] * len(customer_ids)) + query = f""" + SELECT + c.id, c.first_name, c.last_name, c.email, c.phone, c.mobile, + c.title, c.department, c.is_active, c.vtiger_id, + c.created_at, c.updated_at, + ARRAY_AGG(DISTINCT cu.name ORDER BY cu.name) FILTER (WHERE cu.name IS NOT NULL) as company_names + FROM contacts c + JOIN contact_companies cc ON c.id = cc.contact_id + JOIN customers cu ON cc.customer_id = cu.id + WHERE cc.customer_id IN ({placeholders}) AND c.id <> %s + GROUP BY c.id, c.first_name, c.last_name, c.email, c.phone, c.mobile, + c.title, c.department, c.is_active, c.vtiger_id, c.created_at, c.updated_at + ORDER BY c.last_name, c.first_name + """ + params = tuple(customer_ids + [contact_id]) + results = execute_query(query, params) or [] + return {"contacts": results} + + except Exception as e: + logger.error(f"Failed to get related contacts for {contact_id}: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/contacts/{contact_id}/subscriptions") +async def get_contact_subscriptions(contact_id: int): + customer_id = get_primary_customer_id(contact_id) + if not customer_id: + return { + "status": "no_linked_customer", + "message": "Kontakt er ikke tilknyttet et firma", + "recurring_orders": [], + "sales_orders": [], + "subscriptions": [], + "expired_subscriptions": [], + "bmc_office_subscriptions": [], + } + return await get_customer_subscriptions(customer_id) + + +@router.post("/contacts/{contact_id}/subscriptions/lock") +async def lock_contact_subscriptions(contact_id: int, lock_request: dict): + customer_id = get_primary_customer_id(contact_id) + if not customer_id: + raise HTTPException(status_code=404, detail="Kontakt har ingen tilknyttet kunde") + return await lock_customer_subscriptions(customer_id, lock_request) + + +@router.post("/contacts/{contact_id}/subscription-comment") +async def save_contact_subscription_comment(contact_id: int, data: SubscriptionComment): + customer_id = get_primary_customer_id(contact_id) + if not customer_id: + raise HTTPException(status_code=404, detail="Kontakt har ingen tilknyttet kunde") + return await save_subscription_comment(customer_id, data) + + +@router.get("/contacts/{contact_id}/subscription-comment") +async def get_contact_subscription_comment(contact_id: int): + customer_id = get_primary_customer_id(contact_id) + if not customer_id: + raise HTTPException(status_code=404, detail="Kontakt har ingen tilknyttet kunde") + return await get_subscription_comment(customer_id) + + +@router.get("/contacts/{contact_id}/subscriptions/billing-matrix") +async def get_contact_subscription_billing_matrix( + contact_id: int, + months: int = Query(default=12, ge=1, le=60, description="Number of months to show"), +): + customer_id = get_primary_customer_id(contact_id) + if not customer_id: + raise HTTPException(status_code=404, detail="Kontakt har ingen tilknyttet kunde") + return await get_subscription_billing_matrix(customer_id, months) diff --git a/app/contacts/frontend/contact_detail.html b/app/contacts/frontend/contact_detail.html index 4613b32..1348764 100644 --- a/app/contacts/frontend/contact_detail.html +++ b/app/contacts/frontend/contact_detail.html @@ -159,11 +159,61 @@ Firmaer + + + + + + + + + + @@ -254,6 +304,262 @@ + +
+
+
Kontakter i samme firmaer
+ + Se alle + +
+
+ + + + + + + + + + + + + + + +
NavnTitelEmailTelefonFirmaer
+
+
+
+
+ + +
+
Fakturaer
+
+ Fakturamodul kommer snart... +
+
+ + +
+
+
Abonnnents tjek
+ + Åbn kundedetalje + +
+ +
+
+
+ Intern Kommentar + (kun synlig for medarbejdere) +
+ +
+ +
+ +
+
+
+
+ +
+
+
+

Henter data...

+
+
+
+ + +
+
+
+
Kunde pipeline
+ Muligheder knyttet til kontakten +
+ + Åbn kundedetalje + +
+
+
+ + + + + + + + + + + + + + + +
TitelBeløbStageSandsynlighedHandling
+
+
+
+
+
+ + +
+
+
+ Abonnements-matrix + (fra e-conomic) +
+ +
+ + + + +
+
+

Henter fakturamatrix fra e-conomic...

+
+ +
+ + +
+
+
+
Lokationer
+ Lokationer knyttet til kontaktens firmaer +
+ + Åbn kundedetalje + +
+
+
+ Ingen lokationer fundet for denne kontakt +
+
+ + +
+
+
+
Hardware
+ Hardware knyttet til kontaktens firmaer +
+ + Åbn kundedetalje + +
+
+ + + + + + + + + + + + + + + + +
HardwareTypeSerienr.LokationStatusHandling
+
+
+
+
+ Ingen hardware fundet for denne kontakt +
+
+ + +
+
+
+
+
+
Systemstatus
+ + + +
+
Ukendt
+
-
+
+
CPU load-
+
Free disk-
+
RAM usage-
+
OPCache hit rate-
+
+
+
+
+
+
+
Nøgletal
+
File count growth-
+
Public shares uden password-
+
Active users-
+
+
+
+
+
Historik
+
Ingen events endnu.
+
+
+
+
+
@@ -276,6 +582,37 @@
+ + +
+
Aktivitet
+
+
+
+
+
+
+ + +
+
+
Samtaler
+ + Åbn kundedetalje + +
+ +
+ + +
+ +
+
+ Henter samtaler... +
+
+
@@ -416,6 +753,7 @@