[2ff88f42] einfügen
einfügen
This commit is contained in:
@@ -1 +1 @@
|
||||
{"task_id": "2ff88f42-8544-8000-8314-c9013414d1d0", "token": "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", "session_start_time": "2026-02-20T10:56:03.179196"}
|
||||
{"task_id": "2ff88f42-8544-8018-883f-e8837c0421af", "token": "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", "session_start_time": "2026-02-20T13:24:58.251700"}
|
||||
@@ -454,6 +454,22 @@ def list_industries(db: Session = Depends(get_db), username: str = Depends(authe
|
||||
def list_job_roles(db: Session = Depends(get_db), username: str = Depends(authenticate_user)):
|
||||
return db.query(JobRoleMapping).order_by(JobRoleMapping.pattern.asc()).all()
|
||||
|
||||
@app.get("/api/job_roles/raw")
|
||||
def list_raw_job_titles(
|
||||
limit: int = 100,
|
||||
unmapped_only: bool = True,
|
||||
db: Session = Depends(get_db),
|
||||
username: str = Depends(authenticate_user)
|
||||
):
|
||||
"""
|
||||
Returns unique raw job titles from CRM imports, prioritized by frequency.
|
||||
"""
|
||||
query = db.query(RawJobTitle)
|
||||
if unmapped_only:
|
||||
query = query.filter(RawJobTitle.is_mapped == False)
|
||||
|
||||
return query.order_by(RawJobTitle.count.desc()).limit(limit).all()
|
||||
|
||||
@app.get("/api/mistakes")
|
||||
def list_reported_mistakes(
|
||||
status: Optional[str] = Query(None),
|
||||
|
||||
@@ -150,7 +150,7 @@ class Industry(Base):
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
|
||||
class JobRoleMapping(Base):
|
||||
class JobRoleMapping(BaseModel):
|
||||
"""
|
||||
Maps job title patterns (regex or simple string) to Roles.
|
||||
"""
|
||||
@@ -162,7 +162,25 @@ class JobRoleMapping(Base):
|
||||
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
class Persona(Base):
|
||||
class RawJobTitle(BaseModel):
|
||||
"""
|
||||
Stores raw unique job titles imported from CRM to assist in pattern mining.
|
||||
Tracks frequency to prioritize high-impact patterns.
|
||||
"""
|
||||
__tablename__ = "raw_job_titles"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
title = Column(String, unique=True, index=True) # The raw string, e.g. "Senior Sales Mgr."
|
||||
count = Column(Integer, default=1) # How often this title appears in the CRM
|
||||
source = Column(String, default="import")
|
||||
|
||||
# Status Flags
|
||||
is_mapped = Column(Boolean, default=False) # True if a pattern currently covers this title
|
||||
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
class Persona(BaseModel):
|
||||
"""
|
||||
Represents a generalized persona/role (e.g. 'Geschäftsführer', 'IT-Leiter')
|
||||
independent of the specific job title pattern.
|
||||
|
||||
95
company-explorer/backend/scripts/import_job_titles.py
Normal file
95
company-explorer/backend/scripts/import_job_titles.py
Normal file
@@ -0,0 +1,95 @@
|
||||
import sys
|
||||
import os
|
||||
import csv
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
|
||||
# Setup Environment
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), "../../"))
|
||||
|
||||
from backend.database import SessionLocal, RawJobTitle, init_db, engine, Base
|
||||
|
||||
def import_titles(file_path: str, delimiter: str = ';'):
|
||||
print(f"🚀 Starting Import from {file_path}...")
|
||||
|
||||
# Ensure Table Exists
|
||||
RawJobTitle.__table__.create(bind=engine, checkfirst=True)
|
||||
|
||||
db = SessionLocal()
|
||||
total_rows = 0
|
||||
new_titles = 0
|
||||
updated_titles = 0
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8-sig') as f: # utf-8-sig handles BOM from Excel
|
||||
# Try to detect header
|
||||
sample = f.read(1024)
|
||||
has_header = csv.Sniffer().has_header(sample)
|
||||
f.seek(0)
|
||||
|
||||
reader = csv.reader(f, delimiter=delimiter)
|
||||
|
||||
if has_header:
|
||||
headers = next(reader)
|
||||
print(f"ℹ️ Header detected: {headers}")
|
||||
# Try to find the right column index
|
||||
col_idx = 0
|
||||
for i, h in enumerate(headers):
|
||||
if h.lower() in ['funktion', 'jobtitle', 'title', 'position', 'rolle']:
|
||||
col_idx = i
|
||||
print(f" -> Using column '{h}' (Index {i})")
|
||||
break
|
||||
else:
|
||||
col_idx = 0
|
||||
print("ℹ️ No header detected, using first column.")
|
||||
|
||||
# Process Rows
|
||||
for row in reader:
|
||||
if not row: continue
|
||||
if len(row) <= col_idx: continue
|
||||
|
||||
raw_title = row[col_idx].strip()
|
||||
if not raw_title: continue # Skip empty
|
||||
|
||||
total_rows += 1
|
||||
|
||||
# Check existance
|
||||
existing = db.query(RawJobTitle).filter(RawJobTitle.title == raw_title).first()
|
||||
|
||||
if existing:
|
||||
existing.count += 1
|
||||
existing.updated_at = datetime.utcnow()
|
||||
updated_titles += 1
|
||||
else:
|
||||
db.add(RawJobTitle(title=raw_title, count=1))
|
||||
new_titles += 1
|
||||
|
||||
if total_rows % 100 == 0:
|
||||
db.commit()
|
||||
print(f" Processed {total_rows} rows...", end='\r')
|
||||
|
||||
db.commit()
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error: {e}")
|
||||
db.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
print(f"\n✅ Import Complete.")
|
||||
print(f" Total Processed: {total_rows}")
|
||||
print(f" New Unique Titles: {new_titles}")
|
||||
print(f" Updated Frequencies: {updated_titles}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Import Job Titles from CSV")
|
||||
parser.add_argument("file", help="Path to CSV file")
|
||||
parser.add_argument("--delimiter", default=";", help="CSV Delimiter (default: ';')")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not os.path.exists(args.file):
|
||||
print(f"❌ File not found: {args.file}")
|
||||
sys.exit(1)
|
||||
|
||||
import_titles(args.file, args.delimiter)
|
||||
@@ -32,6 +32,7 @@ export function RoboticsSettings({ isOpen, onClose, apiBase }: RoboticsSettingsP
|
||||
const [roboticsCategories, setRoboticsCategories] = useState<any[]>([])
|
||||
const [industries, setIndustries] = useState<any[]>([])
|
||||
const [jobRoles, setJobRoles] = useState<any[]>([])
|
||||
const [rawJobTitles, setRawJobTitles] = useState<any[]>([])
|
||||
const [reportedMistakes, setReportedMistakes] = useState<ReportedMistake[]>([])
|
||||
const [currentMistakeStatusFilter, setCurrentMistakeStatusFilter] = useState<string>("PENDING");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -39,15 +40,17 @@ export function RoboticsSettings({ isOpen, onClose, apiBase }: RoboticsSettingsP
|
||||
const fetchAllData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const [resRobotics, resIndustries, resJobRoles, resMistakes] = await Promise.all([
|
||||
const [resRobotics, resIndustries, resJobRoles, resRawTitles, resMistakes] = await Promise.all([
|
||||
axios.get(`${apiBase}/robotics/categories`),
|
||||
axios.get(`${apiBase}/industries`),
|
||||
axios.get(`${apiBase}/job_roles`),
|
||||
axios.get(`${apiBase}/job_roles/raw`),
|
||||
axios.get(`${apiBase}/mistakes?status=${currentMistakeStatusFilter}`),
|
||||
]);
|
||||
setRoboticsCategories(resRobotics.data);
|
||||
setIndustries(resIndustries.data);
|
||||
setJobRoles(resJobRoles.data);
|
||||
setRawJobTitles(resRawTitles.data);
|
||||
setReportedMistakes(resMistakes.data.items);
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch settings data:", e);
|
||||
@@ -251,22 +254,58 @@ export function RoboticsSettings({ isOpen, onClose, apiBase }: RoboticsSettingsP
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div key="roles-content" className={clsx("space-y-4", { 'hidden': isLoading || activeTab !== 'roles' })}>
|
||||
<div className="flex justify-between items-center"><h3 className="text-sm font-bold text-slate-700 dark:text-slate-300">Job Title Mapping Patterns</h3><button onClick={handleAddJobRole} className="flex items-center gap-1 px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-white text-xs font-bold rounded"><Plus className="h-3 w-3" /> ADD PATTERN</button></div>
|
||||
<div className="bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-lg overflow-hidden">
|
||||
<table className="w-full text-left text-xs">
|
||||
<thead className="bg-slate-100 dark:bg-slate-900 border-b border-slate-200 dark:border-slate-800 text-slate-500 font-bold uppercase"><tr><th className="p-3">Job Title Pattern (Regex/Text)</th><th className="p-3">Mapped Role</th><th className="p-3 w-10"></th></tr></thead>
|
||||
<tbody className="divide-y divide-slate-200 dark:divide-slate-800">
|
||||
{jobRoles.map(role => (
|
||||
<tr key={role.id} className="group">
|
||||
<td className="p-2"><input className="w-full bg-transparent border border-transparent hover:border-slate-300 dark:hover:border-slate-700 rounded px-2 py-1 text-slate-900 dark:text-slate-200 outline-none focus:border-blue-500" defaultValue={role.pattern} /></td>
|
||||
<td className="p-2"><select className="w-full bg-transparent border border-transparent hover:border-slate-300 dark:hover:border-slate-700 rounded px-2 py-1 text-slate-900 dark:text-slate-200 outline-none focus:border-blue-500" defaultValue={role.role}><option>Operativer Entscheider</option><option>Infrastruktur-Verantwortlicher</option><option>Wirtschaftlicher Entscheider</option><option>Innovations-Treiber</option><option>Influencer</option></select></td>
|
||||
<td className="p-2 text-center"><button onClick={() => handleDeleteJobRole(role.id)} className="text-slate-400 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity"><Trash2 className="h-4 w-4" /></button></td>
|
||||
</tr>
|
||||
))}
|
||||
{jobRoles.length === 0 && (<tr><td colSpan={3} className="p-8 text-center text-slate-500 italic">No patterns defined yet.</td></tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
<div key="roles-content" className={clsx("space-y-8", { 'hidden': isLoading || activeTab !== 'roles' })}>
|
||||
{/* Existing Patterns */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h3 className="text-sm font-bold text-slate-700 dark:text-slate-300">Active Mapping Patterns</h3>
|
||||
<p className="text-[10px] text-slate-500 uppercase font-semibold">Deterministic Regex/Text rules</p>
|
||||
</div>
|
||||
<button onClick={handleAddJobRole} className="flex items-center gap-1 px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-white text-xs font-bold rounded shadow-lg shadow-blue-500/20"><Plus className="h-3 w-3" /> ADD PATTERN</button>
|
||||
</div>
|
||||
<div className="bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl overflow-hidden shadow-sm">
|
||||
<table className="w-full text-left text-xs">
|
||||
<thead className="bg-slate-50 dark:bg-slate-900/50 border-b border-slate-200 dark:border-slate-800 text-slate-500 font-bold uppercase tracking-wider"><tr><th className="p-3">Pattern (% for wildcard)</th><th className="p-3">Target Persona Role</th><th className="p-3 w-10"></th></tr></thead>
|
||||
<tbody className="divide-y divide-slate-100 dark:divide-slate-800/50">
|
||||
{jobRoles.map(role => (
|
||||
<tr key={role.id} className="group hover:bg-slate-50/50 dark:hover:bg-slate-800/30 transition-colors">
|
||||
<td className="p-2"><input className="w-full bg-transparent border border-transparent hover:border-slate-300 dark:hover:border-slate-700 rounded px-2 py-1 text-slate-900 dark:text-slate-200 outline-none focus:border-blue-500 font-mono" defaultValue={role.pattern} /></td>
|
||||
<td className="p-2"><select className="w-full bg-transparent border border-transparent hover:border-slate-300 dark:hover:border-slate-700 rounded px-2 py-1 text-slate-900 dark:text-slate-200 outline-none focus:border-blue-500" defaultValue={role.role}><option>Operativer Entscheider</option><option>Infrastruktur-Verantwortlicher</option><option>Wirtschaftlicher Entscheider</option><option>Innovations-Treiber</option><option>Influencer</option></select></td>
|
||||
<td className="p-2 text-center"><button onClick={() => handleDeleteJobRole(role.id)} className="text-slate-400 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-all transform hover:scale-110"><Trash2 className="h-4 w-4" /></button></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Discovery Inbox */}
|
||||
<div className="space-y-4 pt-4 border-t border-slate-200 dark:border-slate-800">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h3 className="text-sm font-bold text-slate-700 dark:text-slate-300">Discovery Inbox</h3>
|
||||
<p className="text-[10px] text-slate-500 uppercase font-semibold">Unmapped job titles from CRM, prioritized by frequency</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-50/50 dark:bg-slate-900/20 border border-dashed border-slate-300 dark:border-slate-700 rounded-xl overflow-hidden">
|
||||
<table className="w-full text-left text-xs">
|
||||
<thead className="bg-slate-100/50 dark:bg-slate-900/80 border-b border-slate-200 dark:border-slate-800 text-slate-400 font-bold uppercase tracking-wider"><tr><th className="p-3">Job Title from CRM</th><th className="p-3 w-20 text-center">Frequency</th><th className="p-3 w-10"></th></tr></thead>
|
||||
<tbody className="divide-y divide-slate-100 dark:divide-slate-800/50">
|
||||
{rawJobTitles.map(raw => (
|
||||
<tr key={raw.id} className="group hover:bg-white dark:hover:bg-slate-800 transition-colors">
|
||||
<td className="p-3 font-medium text-slate-600 dark:text-slate-400 italic">{raw.title}</td>
|
||||
<td className="p-3 text-center"><span className="px-2 py-1 bg-slate-200 dark:bg-slate-800 rounded-full font-bold text-[10px] text-slate-500">{raw.count}x</span></td>
|
||||
<td className="p-3 text-center"><button onClick={async () => {
|
||||
await axios.post(`${apiBase}/job_roles`, { pattern: `%${raw.title.toLowerCase()}%`, role: "Influencer" });
|
||||
fetchAllData();
|
||||
}} className="p-1 text-blue-500 hover:bg-blue-100 dark:hover:bg-blue-900/30 rounded transition-all"><Plus className="h-4 w-4" /></button></td>
|
||||
</tr>
|
||||
))}
|
||||
{rawJobTitles.length === 0 && (<tr><td colSpan={3} className="p-12 text-center text-slate-400 italic">Discovery inbox is empty. Import raw job titles to see data here.</td></tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user