bmc_hub/app/vendors/backend/router.py
Christian 3a35042788 feat: Implement Vendors API and Frontend
- Added a new API router for managing vendors with endpoints for listing, creating, updating, retrieving, and deleting vendors.
- Implemented frontend views for displaying vendor lists and details using Jinja2 templates.
- Created HTML templates for vendor list and detail pages with responsive design and dynamic content loading.
- Added JavaScript functionality for vendor management, including pagination, filtering, and modal forms for creating new vendors.
- Introduced a settings table in the database for system configuration and extended the users table with additional fields.
- Developed a script to import vendors from an OmniSync database into the PostgreSQL database, handling errors and logging progress.
2025-12-06 11:04:19 +01:00

175 lines
6.1 KiB
Python

"""
Vendors API Router
Endpoints for managing suppliers and vendors
"""
from fastapi import APIRouter, HTTPException, Query
from typing import List, Optional
from app.models.schemas import Vendor, VendorCreate, VendorUpdate
from app.core.database import execute_query
import logging
logger = logging.getLogger(__name__)
router = APIRouter()
@router.get("/vendors", response_model=List[Vendor], tags=["Vendors"])
async def list_vendors(
search: Optional[str] = Query(None, description="Search by name, CVR, or domain"),
category: Optional[str] = Query(None, description="Filter by category"),
is_active: Optional[bool] = Query(None, description="Filter by active status"),
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100)
):
"""Get list of vendors with optional filtering"""
query = "SELECT * FROM vendors WHERE 1=1"
params = []
if search:
query += " AND (name ILIKE %s OR cvr_number ILIKE %s OR domain ILIKE %s)"
search_param = f"%{search}%"
params.extend([search_param, search_param, search_param])
if category:
query += " AND category = %s"
params.append(category)
if is_active is not None:
query += " AND is_active = %s"
params.append(is_active)
query += " ORDER BY name LIMIT %s OFFSET %s"
params.extend([limit, skip])
result = execute_query(query, tuple(params))
return result or []
@router.get("/vendors/{vendor_id}", response_model=Vendor, tags=["Vendors"])
async def get_vendor(vendor_id: int):
"""Get vendor by ID"""
query = "SELECT * FROM vendors WHERE id = %s"
result = execute_query(query, (vendor_id,))
if not result or len(result) == 0:
raise HTTPException(status_code=404, detail="Vendor not found")
return result[0]
@router.post("/vendors", response_model=Vendor, tags=["Vendors"])
async def create_vendor(vendor: VendorCreate):
"""Create a new vendor"""
try:
query = """
INSERT INTO vendors (
name, cvr_number, email, phone, address, postal_code, city,
website, domain, email_pattern, category, priority, notes, is_active
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING *
"""
params = (
vendor.name, vendor.cvr_number, vendor.email, vendor.phone,
vendor.address, vendor.postal_code, vendor.city, vendor.website,
vendor.domain, vendor.email_pattern, vendor.category, vendor.priority,
vendor.notes, vendor.is_active
)
result = execute_query(query, params)
if not result or len(result) == 0:
raise HTTPException(status_code=500, detail="Failed to create vendor")
logger.info(f"✅ Created vendor: {vendor.name}")
return result[0]
except Exception as e:
logger.error(f"❌ Error creating vendor: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.put("/vendors/{vendor_id}", response_model=Vendor, tags=["Vendors"])
async def update_vendor(vendor_id: int, vendor: VendorUpdate):
"""Update an existing vendor"""
# Check if vendor exists
existing = execute_query("SELECT id FROM vendors WHERE id = %s", (vendor_id,))
if not existing:
raise HTTPException(status_code=404, detail="Vendor not found")
# Build update query
update_fields = []
params = []
if vendor.name is not None:
update_fields.append("name = %s")
params.append(vendor.name)
if vendor.cvr_number is not None:
update_fields.append("cvr_number = %s")
params.append(vendor.cvr_number)
if vendor.email is not None:
update_fields.append("email = %s")
params.append(vendor.email)
if vendor.phone is not None:
update_fields.append("phone = %s")
params.append(vendor.phone)
if vendor.address is not None:
update_fields.append("address = %s")
params.append(vendor.address)
if vendor.postal_code is not None:
update_fields.append("postal_code = %s")
params.append(vendor.postal_code)
if vendor.city is not None:
update_fields.append("city = %s")
params.append(vendor.city)
if vendor.website is not None:
update_fields.append("website = %s")
params.append(vendor.website)
if vendor.domain is not None:
update_fields.append("domain = %s")
params.append(vendor.domain)
if vendor.email_pattern is not None:
update_fields.append("email_pattern = %s")
params.append(vendor.email_pattern)
if vendor.category is not None:
update_fields.append("category = %s")
params.append(vendor.category)
if vendor.priority is not None:
update_fields.append("priority = %s")
params.append(vendor.priority)
if vendor.notes is not None:
update_fields.append("notes = %s")
params.append(vendor.notes)
if vendor.is_active is not None:
update_fields.append("is_active = %s")
params.append(vendor.is_active)
if not update_fields:
raise HTTPException(status_code=400, detail="No fields to update")
params.append(vendor_id)
query = f"UPDATE vendors SET {', '.join(update_fields)} WHERE id = %s RETURNING *"
try:
result = execute_query(query, tuple(params))
if not result:
raise HTTPException(status_code=500, detail="Failed to update vendor")
logger.info(f"✅ Updated vendor: {vendor_id}")
return result[0]
except Exception as e:
logger.error(f"❌ Error updating vendor: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/vendors/{vendor_id}", tags=["Vendors"])
async def delete_vendor(vendor_id: int):
"""Soft delete a vendor (set is_active = false)"""
query = "UPDATE vendors SET is_active = false WHERE id = %s RETURNING id"
result = execute_query(query, (vendor_id,))
if not result:
raise HTTPException(status_code=404, detail="Vendor not found")
logger.info(f"✅ Deleted vendor: {vendor_id}")
return {"message": "Vendor deleted successfully"}