feat(customers): add dashboard and customer management
This commit is contained in:
parent
694b7bd09a
commit
92cb8d1286
28 changed files with 2521 additions and 13 deletions
208
backend/hermes/app/api/customers.py
Normal file
208
backend/hermes/app/api/customers.py
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.rbac import require_permission
|
||||
from app.db.database import get_db
|
||||
from app.models.customer import Customer, CustomerContact
|
||||
from app.models.user import User
|
||||
from app.repositories.customer_repository import CustomerRepository
|
||||
from app.schemas.customer import (
|
||||
CustomerContactCreate,
|
||||
CustomerContactResponse,
|
||||
CustomerContactUpdate,
|
||||
CustomerCreate,
|
||||
CustomerResponse,
|
||||
CustomerUpdate,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/customers",
|
||||
tags=["Customers"],
|
||||
)
|
||||
|
||||
|
||||
def get_customer_or_404(db: Session, customer_id: int) -> Customer:
|
||||
customer = CustomerRepository.get_by_id(db, customer_id)
|
||||
if customer is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Kunde nicht gefunden",
|
||||
)
|
||||
return customer
|
||||
|
||||
|
||||
def get_contact_or_404(db: Session, customer_id: int, contact_id: int) -> CustomerContact:
|
||||
contact = CustomerRepository.get_contact_by_id(db, customer_id, contact_id)
|
||||
if contact is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Ansprechpartner nicht gefunden",
|
||||
)
|
||||
return contact
|
||||
|
||||
|
||||
def ensure_unique_customer_number(
|
||||
db: Session,
|
||||
customer_number: str,
|
||||
customer_id: int | None = None,
|
||||
) -> None:
|
||||
conflict = CustomerRepository.find_number_conflict(
|
||||
db,
|
||||
customer_number=customer_number,
|
||||
exclude_customer_id=customer_id,
|
||||
)
|
||||
if conflict is not None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Kundennummer ist bereits vergeben",
|
||||
)
|
||||
|
||||
|
||||
@router.get("", response_model=list[CustomerResponse])
|
||||
@router.get("/", response_model=list[CustomerResponse], include_in_schema=False)
|
||||
def get_customers(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_permission("customers.read")),
|
||||
):
|
||||
logger.info("customers.list", extra={"actor_user_id": current_user.id})
|
||||
return CustomerRepository.get_all(db)
|
||||
|
||||
|
||||
@router.get("/{customer_id}", response_model=CustomerResponse)
|
||||
def get_customer(
|
||||
customer_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_permission("customers.read")),
|
||||
):
|
||||
logger.info(
|
||||
"customers.detail",
|
||||
extra={"actor_user_id": current_user.id, "target_customer_id": customer_id},
|
||||
)
|
||||
return get_customer_or_404(db, customer_id)
|
||||
|
||||
|
||||
@router.post("", response_model=CustomerResponse, status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"/",
|
||||
response_model=CustomerResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
include_in_schema=False,
|
||||
)
|
||||
def create_customer(
|
||||
customer: CustomerCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_permission("customers.create")),
|
||||
):
|
||||
ensure_unique_customer_number(db, customer.customer_number)
|
||||
logger.info("customers.create", extra={"actor_user_id": current_user.id})
|
||||
return CustomerRepository.create(db, customer)
|
||||
|
||||
|
||||
@router.put("/{customer_id}", response_model=CustomerResponse)
|
||||
def update_customer(
|
||||
customer_id: int,
|
||||
customer: CustomerUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_permission("customers.update")),
|
||||
):
|
||||
db_customer = get_customer_or_404(db, customer_id)
|
||||
ensure_unique_customer_number(db, customer.customer_number, customer_id)
|
||||
logger.info(
|
||||
"customers.update",
|
||||
extra={"actor_user_id": current_user.id, "target_customer_id": customer_id},
|
||||
)
|
||||
return CustomerRepository.update(db, db_customer, customer)
|
||||
|
||||
|
||||
@router.delete("/{customer_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_customer(
|
||||
customer_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_permission("customers.delete")),
|
||||
):
|
||||
db_customer = get_customer_or_404(db, customer_id)
|
||||
logger.info(
|
||||
"customers.delete",
|
||||
extra={"actor_user_id": current_user.id, "target_customer_id": customer_id},
|
||||
)
|
||||
CustomerRepository.delete(db, db_customer)
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@router.get("/{customer_id}/contacts", response_model=list[CustomerContactResponse])
|
||||
def get_customer_contacts(
|
||||
customer_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_permission("customers.read")),
|
||||
):
|
||||
get_customer_or_404(db, customer_id)
|
||||
logger.info(
|
||||
"customers.contacts.list",
|
||||
extra={"actor_user_id": current_user.id, "target_customer_id": customer_id},
|
||||
)
|
||||
return CustomerRepository.get_contacts(db, customer_id)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{customer_id}/contacts",
|
||||
response_model=CustomerContactResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def create_customer_contact(
|
||||
customer_id: int,
|
||||
contact: CustomerContactCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_permission("customers.update")),
|
||||
):
|
||||
get_customer_or_404(db, customer_id)
|
||||
logger.info(
|
||||
"customers.contacts.create",
|
||||
extra={"actor_user_id": current_user.id, "target_customer_id": customer_id},
|
||||
)
|
||||
return CustomerRepository.create_contact(db, customer_id, contact)
|
||||
|
||||
|
||||
@router.put("/{customer_id}/contacts/{contact_id}", response_model=CustomerContactResponse)
|
||||
def update_customer_contact(
|
||||
customer_id: int,
|
||||
contact_id: int,
|
||||
contact: CustomerContactUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_permission("customers.update")),
|
||||
):
|
||||
get_customer_or_404(db, customer_id)
|
||||
db_contact = get_contact_or_404(db, customer_id, contact_id)
|
||||
logger.info(
|
||||
"customers.contacts.update",
|
||||
extra={
|
||||
"actor_user_id": current_user.id,
|
||||
"target_customer_id": customer_id,
|
||||
"target_contact_id": contact_id,
|
||||
},
|
||||
)
|
||||
return CustomerRepository.update_contact(db, db_contact, contact)
|
||||
|
||||
|
||||
@router.delete("/{customer_id}/contacts/{contact_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_customer_contact(
|
||||
customer_id: int,
|
||||
contact_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_permission("customers.update")),
|
||||
):
|
||||
get_customer_or_404(db, customer_id)
|
||||
db_contact = get_contact_or_404(db, customer_id, contact_id)
|
||||
logger.info(
|
||||
"customers.contacts.delete",
|
||||
extra={
|
||||
"actor_user_id": current_user.id,
|
||||
"target_customer_id": customer_id,
|
||||
"target_contact_id": contact_id,
|
||||
},
|
||||
)
|
||||
CustomerRepository.delete_contact(db, db_contact)
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
80
backend/hermes/app/api/dashboard.py
Normal file
80
backend/hermes/app/api/dashboard.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.rbac import get_user_permission_names, require_permission
|
||||
from app.db.database import get_db
|
||||
from app.models.rbac import Role
|
||||
from app.models.user import User
|
||||
from app.repositories.customer_repository import CustomerRepository
|
||||
from app.repositories.user_repository import UserRepository
|
||||
from app.schemas.dashboard import DashboardSummary, EmptyWidget, MetricCard
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/dashboard",
|
||||
tags=["Dashboard"],
|
||||
)
|
||||
|
||||
|
||||
@router.get("/summary", response_model=DashboardSummary)
|
||||
def get_dashboard_summary(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_permission("dashboard.read")),
|
||||
):
|
||||
permissions = get_user_permission_names(current_user)
|
||||
customers: list[MetricCard] = []
|
||||
latest_customers = []
|
||||
users: list[MetricCard] = []
|
||||
roles: list[MetricCard] = []
|
||||
|
||||
if "customers.read" in permissions:
|
||||
customers = [
|
||||
MetricCard(label="Kunden", value=CustomerRepository.count_all(db)),
|
||||
MetricCard(label="Aktive Kunden", value=CustomerRepository.count_active(db)),
|
||||
MetricCard(label="Neue Kunden", value=CustomerRepository.count_recent(db)),
|
||||
]
|
||||
latest_customers = CustomerRepository.get_latest(db)
|
||||
|
||||
if "users.read" in permissions:
|
||||
all_users = UserRepository.get_all(db)
|
||||
users = [
|
||||
MetricCard(label="Benutzer", value=len(all_users)),
|
||||
MetricCard(label="Aktive Benutzer", value=len([user for user in all_users if user.is_active])),
|
||||
]
|
||||
|
||||
if "roles.read" in permissions:
|
||||
roles = [
|
||||
MetricCard(
|
||||
label="Rollen",
|
||||
value=db.scalar(select(func.count(Role.id))) or 0,
|
||||
)
|
||||
]
|
||||
|
||||
logger.info("dashboard.summary", extra={"actor_user_id": current_user.id})
|
||||
|
||||
return DashboardSummary(
|
||||
customers=customers,
|
||||
latest_customers=latest_customers,
|
||||
users=users,
|
||||
roles=roles,
|
||||
activities=EmptyWidget(
|
||||
title="Letzte Aktivitäten",
|
||||
message="Noch keine Aktivitäten vorhanden.",
|
||||
),
|
||||
tasks=EmptyWidget(
|
||||
title="Offene Aufgaben",
|
||||
message="Noch keine Aufgaben vorhanden.",
|
||||
),
|
||||
tickets=EmptyWidget(
|
||||
title="Offene Tickets",
|
||||
message="Noch keine Tickets vorhanden.",
|
||||
),
|
||||
projects=EmptyWidget(
|
||||
title="Projekte",
|
||||
message="Noch keine Projektdaten vorhanden.",
|
||||
),
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue