From 489f81a1e39f2878aa284bcd39fdb5d10aa54ee9 Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 11 Feb 2026 23:51:21 +0100 Subject: [PATCH] feat: Enhance hardware detail view with ESET data synchronization and specifications - Added a button to sync ESET data in the hardware detail view. - Introduced a new tab for ESET specifications, displaying relevant device information. - Included ESET UUID and group details in the hardware information section. - Implemented a JavaScript function to handle ESET data synchronization via API. - Updated the ESET import template to improve device listing and inline contact selection. - Enhanced the Nextcloud and locations routers to support customer ID resolution from contacts. - Added utility functions for retrieving customer IDs linked to contacts. - Removed debug information from the service contract wizard for cleaner output. --- app/contacts/backend/router.py | 94 ++ app/contacts/backend/router_simple.py | 92 ++ app/contacts/frontend/contact_detail.html | 1190 +++++++++++++++++ app/conversations/backend/router.py | 17 +- app/core/contact_utils.py | 32 + app/modules/hardware/backend/router.py | 15 +- app/modules/hardware/frontend/views.py | 98 +- app/modules/hardware/templates/detail.html | 125 ++ .../hardware/templates/eset_import.html | 337 ++++- .../hardware/templates/eset_overview.html | 5 + app/modules/locations/backend/router.py | 34 + app/modules/nextcloud/backend/router.py | 143 +- app/opportunities/backend/router.py | 9 +- app/services/eset_service.py | 9 +- .../frontend/service_contract_wizard.html | 15 - 15 files changed, 2141 insertions(+), 74 deletions(-) create mode 100644 app/core/contact_utils.py 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
+
+
+
+
+
+
+ + +
+ + +
+ + +
+ +
+
+ Henter samtaler... +
+
+
@@ -416,6 +753,7 @@