feat(ce): upgrade to v0.5.0 with contacts management, advanced settings and ui modernization
This commit is contained in:
@@ -17,7 +17,7 @@ setup_logging()
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from .database import init_db, get_db, Company, Signal, EnrichmentData, RoboticsCategory
|
||||
from .database import init_db, get_db, Company, Signal, EnrichmentData, RoboticsCategory, Contact, Industry, JobRoleMapping
|
||||
from .services.deduplication import Deduplicator
|
||||
from .services.discovery import DiscoveryService
|
||||
from .services.scraping import ScraperService
|
||||
@@ -58,6 +58,33 @@ class AnalysisRequest(BaseModel):
|
||||
company_id: int
|
||||
force_scrape: bool = False
|
||||
|
||||
class ContactBase(BaseModel):
|
||||
gender: str
|
||||
title: str = ""
|
||||
first_name: str
|
||||
last_name: str
|
||||
email: str
|
||||
job_title: str
|
||||
language: str = "De"
|
||||
role: str
|
||||
status: str = ""
|
||||
is_primary: bool = False
|
||||
|
||||
class ContactCreate(ContactBase):
|
||||
company_id: int
|
||||
|
||||
class ContactUpdate(BaseModel):
|
||||
gender: Optional[str] = None
|
||||
title: Optional[str] = None
|
||||
first_name: Optional[str] = None
|
||||
last_name: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
job_title: Optional[str] = None
|
||||
language: Optional[str] = None
|
||||
role: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
is_primary: Optional[bool] = None
|
||||
|
||||
# --- Events ---
|
||||
@app.on_event("startup")
|
||||
def on_startup():
|
||||
@@ -99,7 +126,8 @@ def list_companies(
|
||||
def get_company(company_id: int, db: Session = Depends(get_db)):
|
||||
company = db.query(Company).options(
|
||||
joinedload(Company.signals),
|
||||
joinedload(Company.enrichment_data)
|
||||
joinedload(Company.enrichment_data),
|
||||
joinedload(Company.contacts)
|
||||
).filter(Company.id == company_id).first()
|
||||
if not company:
|
||||
raise HTTPException(status_code=404, detail="Company not found")
|
||||
@@ -300,6 +328,230 @@ def override_impressum_url(company_id: int, url: str = Query(...), db: Session =
|
||||
db.commit()
|
||||
return {"status": "updated", "data": impressum_data}
|
||||
|
||||
# --- Contact Routes ---
|
||||
|
||||
@app.post("/api/contacts")
|
||||
def create_contact(contact: ContactCreate, db: Session = Depends(get_db)):
|
||||
"""Creates a new contact and handles primary contact logic."""
|
||||
if contact.is_primary:
|
||||
db.query(Contact).filter(Contact.company_id == contact.company_id).update({"is_primary": False})
|
||||
|
||||
db_contact = Contact(**contact.dict())
|
||||
db.add(db_contact)
|
||||
db.commit()
|
||||
db.refresh(db_contact)
|
||||
return db_contact
|
||||
|
||||
# --- Industry Routes ---
|
||||
|
||||
class IndustryCreate(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
is_focus: bool = False
|
||||
primary_category_id: Optional[int] = None
|
||||
|
||||
class IndustryUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
is_focus: Optional[bool] = None
|
||||
primary_category_id: Optional[int] = None
|
||||
|
||||
@app.get("/api/industries")
|
||||
def list_industries(db: Session = Depends(get_db)):
|
||||
return db.query(Industry).all()
|
||||
|
||||
@app.post("/api/industries")
|
||||
def create_industry(ind: IndustryCreate, db: Session = Depends(get_db)):
|
||||
# 1. Prepare data
|
||||
ind_data = ind.dict()
|
||||
base_name = ind_data['name']
|
||||
|
||||
# 2. Check for duplicate name
|
||||
existing = db.query(Industry).filter(Industry.name == base_name).first()
|
||||
if existing:
|
||||
# Auto-increment name if duplicated
|
||||
counter = 1
|
||||
while db.query(Industry).filter(Industry.name == f"{base_name} ({counter})").first():
|
||||
counter += 1
|
||||
ind_data['name'] = f"{base_name} ({counter})"
|
||||
|
||||
# 3. Create
|
||||
db_ind = Industry(**ind_data)
|
||||
db.add(db_ind)
|
||||
db.commit()
|
||||
db.refresh(db_ind)
|
||||
return db_ind
|
||||
|
||||
@app.put("/api/industries/{id}")
|
||||
def update_industry(id: int, ind: IndustryUpdate, db: Session = Depends(get_db)):
|
||||
db_ind = db.query(Industry).filter(Industry.id == id).first()
|
||||
if not db_ind:
|
||||
raise HTTPException(404, "Industry not found")
|
||||
|
||||
for key, value in ind.dict(exclude_unset=True).items():
|
||||
setattr(db_ind, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_ind)
|
||||
return db_ind
|
||||
|
||||
@app.delete("/api/industries/{id}")
|
||||
def delete_industry(id: int, db: Session = Depends(get_db)):
|
||||
db_ind = db.query(Industry).filter(Industry.id == id).first()
|
||||
if not db_ind:
|
||||
raise HTTPException(404, "Industry not found")
|
||||
db.delete(db_ind)
|
||||
db.commit()
|
||||
return {"status": "deleted"}
|
||||
|
||||
# --- Job Role Mapping Routes ---
|
||||
|
||||
class JobRoleMappingCreate(BaseModel):
|
||||
pattern: str
|
||||
role: str
|
||||
|
||||
@app.get("/api/job_roles")
|
||||
def list_job_roles(db: Session = Depends(get_db)):
|
||||
return db.query(JobRoleMapping).all()
|
||||
|
||||
@app.post("/api/job_roles")
|
||||
def create_job_role(mapping: JobRoleMappingCreate, db: Session = Depends(get_db)):
|
||||
db_mapping = JobRoleMapping(**mapping.dict())
|
||||
db.add(db_mapping)
|
||||
db.commit()
|
||||
db.refresh(db_mapping)
|
||||
return db_mapping
|
||||
|
||||
@app.delete("/api/job_roles/{id}")
|
||||
def delete_job_role(id: int, db: Session = Depends(get_db)):
|
||||
db_mapping = db.query(JobRoleMapping).filter(JobRoleMapping.id == id).first()
|
||||
if not db_mapping:
|
||||
raise HTTPException(404, "Mapping not found")
|
||||
db.delete(db_mapping)
|
||||
db.commit()
|
||||
return {"status": "deleted"}
|
||||
|
||||
@app.put("/api/contacts/{contact_id}")
|
||||
def update_contact(contact_id: int, contact: ContactUpdate, db: Session = Depends(get_db)):
|
||||
"""Updates an existing contact."""
|
||||
db_contact = db.query(Contact).filter(Contact.id == contact_id).first()
|
||||
if not db_contact:
|
||||
raise HTTPException(404, "Contact not found")
|
||||
|
||||
update_data = contact.dict(exclude_unset=True)
|
||||
|
||||
if update_data.get("is_primary"):
|
||||
db.query(Contact).filter(Contact.company_id == db_contact.company_id).update({"is_primary": False})
|
||||
|
||||
for key, value in update_data.items():
|
||||
setattr(db_contact, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_contact)
|
||||
return db_contact
|
||||
|
||||
@app.delete("/api/contacts/{contact_id}")
|
||||
def delete_contact(contact_id: int, db: Session = Depends(get_db)):
|
||||
"""Deletes a contact."""
|
||||
db_contact = db.query(Contact).filter(Contact.id == contact_id).first()
|
||||
if not db_contact:
|
||||
raise HTTPException(404, "Contact not found")
|
||||
db.delete(db_contact)
|
||||
db.commit()
|
||||
return {"status": "deleted"}
|
||||
|
||||
@app.get("/api/contacts/all")
|
||||
def list_all_contacts(
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
search: Optional[str] = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Lists all contacts across all companies with pagination and search.
|
||||
"""
|
||||
query = db.query(Contact).join(Company)
|
||||
|
||||
if search:
|
||||
search_term = f"%{search}%"
|
||||
query = query.filter(
|
||||
(Contact.first_name.ilike(search_term)) |
|
||||
(Contact.last_name.ilike(search_term)) |
|
||||
(Contact.email.ilike(search_term)) |
|
||||
(Company.name.ilike(search_term))
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
# Sort by ID desc
|
||||
contacts = query.order_by(Contact.id.desc()).offset(skip).limit(limit).all()
|
||||
|
||||
# Enrich with Company Name for the frontend list
|
||||
result = []
|
||||
for c in contacts:
|
||||
c_dict = {k: v for k, v in c.__dict__.items() if not k.startswith('_')}
|
||||
c_dict['company_name'] = c.company.name if c.company else "Unknown"
|
||||
result.append(c_dict)
|
||||
|
||||
return {"total": total, "items": result}
|
||||
|
||||
class BulkContactImportItem(BaseModel):
|
||||
company_name: str
|
||||
first_name: str
|
||||
last_name: str
|
||||
email: Optional[str] = None
|
||||
job_title: Optional[str] = None
|
||||
role: Optional[str] = "Operativer Entscheider"
|
||||
gender: Optional[str] = "männlich"
|
||||
|
||||
class BulkContactImportRequest(BaseModel):
|
||||
contacts: List[BulkContactImportItem]
|
||||
|
||||
@app.post("/api/contacts/bulk")
|
||||
def bulk_import_contacts(req: BulkContactImportRequest, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Bulk imports contacts.
|
||||
Matches Company by Name (creates if missing).
|
||||
Dedupes Contact by Email.
|
||||
"""
|
||||
logger.info(f"Starting bulk contact import: {len(req.contacts)} items")
|
||||
stats = {"added": 0, "skipped": 0, "companies_created": 0}
|
||||
|
||||
for item in req.contacts:
|
||||
if not item.company_name: continue
|
||||
|
||||
# 1. Find or Create Company
|
||||
company = db.query(Company).filter(Company.name.ilike(item.company_name.strip())).first()
|
||||
if not company:
|
||||
company = Company(name=item.company_name.strip(), status="NEW")
|
||||
db.add(company)
|
||||
db.commit() # Commit to get ID
|
||||
db.refresh(company)
|
||||
stats["companies_created"] += 1
|
||||
|
||||
# 2. Check for Duplicate Contact (by Email)
|
||||
if item.email:
|
||||
exists = db.query(Contact).filter(Contact.email == item.email.strip()).first()
|
||||
if exists:
|
||||
stats["skipped"] += 1
|
||||
continue
|
||||
|
||||
# 3. Create Contact
|
||||
new_contact = Contact(
|
||||
company_id=company.id,
|
||||
first_name=item.first_name,
|
||||
last_name=item.last_name,
|
||||
email=item.email,
|
||||
job_title=item.job_title,
|
||||
role=item.role,
|
||||
gender=item.gender,
|
||||
status="Init" # Default status
|
||||
)
|
||||
db.add(new_contact)
|
||||
stats["added"] += 1
|
||||
|
||||
db.commit()
|
||||
return stats
|
||||
|
||||
def run_discovery_task(company_id: int):
|
||||
# New Session for Background Task
|
||||
from .database import SessionLocal
|
||||
|
||||
Reference in New Issue
Block a user