[2f388f42] Implementierung der UI-Anpassungen zur Anzeige von ausstehenden Fehlerberichten (rote Flagge in der Unternehmensliste, Anzeige im Inspector) und zur Ermöglichung weiterer Fehlerberichte. Backend-APIs wurden entsprechend erweitert.

Implementierung der UI-Anpassungen zur Anzeige von ausstehenden Fehlerberichten (rote Flagge in der Unternehmensliste, Anzeige im Inspector) und zur Ermöglichung weiterer Fehlerberichte. Backend-APIs wurden entsprechend erweitert.
This commit is contained in:
2026-01-27 11:18:36 +00:00
parent 2e75fba71f
commit b184cf1d0f
4 changed files with 96 additions and 10 deletions

View File

@@ -1 +1 @@
{"task_id": "2f488f42-8544-802f-8311-ee72ef1aac2f", "token": "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", "session_start_time": "2026-01-27T10:44:04.509525"} {"task_id": "2f388f42-8544-80b7-9c71-e7c8f319990a", "token": "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", "session_start_time": "2026-01-27T11:18:14.201030"}

View File

@@ -107,6 +107,22 @@ def list_companies(
query = query.order_by(Company.name.asc()) query = query.order_by(Company.name.asc())
items = query.offset(skip).limit(limit).all() items = query.offset(skip).limit(limit).all()
# Efficiently check for pending mistakes
company_ids = [c.id for c in items]
if company_ids:
pending_mistakes = db.query(ReportedMistake.company_id).filter(
ReportedMistake.company_id.in_(company_ids),
ReportedMistake.status == 'PENDING'
).distinct().all()
companies_with_pending_mistakes = {row[0] for row in pending_mistakes}
else:
companies_with_pending_mistakes = set()
# Add the flag to each company object
for company in items:
company.has_pending_mistakes = company.id in companies_with_pending_mistakes
return {"total": total, "items": items} return {"total": total, "items": items}
except Exception as e: except Exception as e:
logger.error(f"List Companies Error: {e}", exc_info=True) logger.error(f"List Companies Error: {e}", exc_info=True)
@@ -251,6 +267,7 @@ def list_job_roles(db: Session = Depends(get_db)):
@app.get("/api/mistakes") @app.get("/api/mistakes")
def list_reported_mistakes( def list_reported_mistakes(
status: Optional[str] = Query(None), status: Optional[str] = Query(None),
company_id: Optional[int] = Query(None),
skip: int = 0, skip: int = 0,
limit: int = 50, limit: int = 50,
db: Session = Depends(get_db) db: Session = Depends(get_db)
@@ -260,6 +277,9 @@ def list_reported_mistakes(
if status: if status:
query = query.filter(ReportedMistake.status == status.upper()) query = query.filter(ReportedMistake.status == status.upper())
if company_id:
query = query.filter(ReportedMistake.company_id == company_id)
total = query.count() total = query.count()
items = query.order_by(ReportedMistake.created_at.desc()).offset(skip).limit(limit).all() items = query.order_by(ReportedMistake.created_at.desc()).offset(skip).limit(limit).all()

View File

@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'
import axios from 'axios' import axios from 'axios'
import { import {
Building, Search, Upload, Globe, MapPin, Play, Search as SearchIcon, Loader2, Building, Search, Upload, Globe, MapPin, Play, Search as SearchIcon, Loader2,
LayoutGrid, List, ChevronLeft, ChevronRight, ArrowDownUp LayoutGrid, List, ChevronLeft, ChevronRight, ArrowDownUp, Flag
} from 'lucide-react' } from 'lucide-react'
import clsx from 'clsx' import clsx from 'clsx'
@@ -16,6 +16,7 @@ interface Company {
industry_ai: string | null industry_ai: string | null
created_at: string created_at: string
updated_at: string updated_at: string
has_pending_mistakes: boolean
} }
interface CompanyTableProps { interface CompanyTableProps {
@@ -124,7 +125,10 @@ export function CompanyTable({ apiBase, onRowClick, refreshKey, onImportClick }:
style={{ borderLeftColor: c.status === 'ENRICHED' ? '#22c55e' : c.status === 'DISCOVERED' ? '#3b82f6' : '#94a3b8' }}> style={{ borderLeftColor: c.status === 'ENRICHED' ? '#22c55e' : c.status === 'DISCOVERED' ? '#3b82f6' : '#94a3b8' }}>
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="font-bold text-slate-900 dark:text-white text-sm truncate" title={c.name}>{c.name}</div> <div className="flex items-center gap-2">
<Flag className={clsx("h-3 w-3 text-slate-300 dark:text-slate-600", c.has_pending_mistakes && "text-red-500 fill-red-500")} />
<div className="font-bold text-slate-900 dark:text-white text-sm truncate" title={c.name}>{c.name}</div>
</div>
<div className="flex items-center gap-1 text-[10px] text-slate-500 dark:text-slate-400 font-medium"> <div className="flex items-center gap-1 text-[10px] text-slate-500 dark:text-slate-400 font-medium">
{c.city && c.country ? (<><MapPin className="h-3 w-3" /> {c.city} <span className="text-slate-400">({c.country})</span></>) : (<span className="italic opacity-50">-</span>)} {c.city && c.country ? (<><MapPin className="h-3 w-3" /> {c.city} <span className="text-slate-400">({c.country})</span></>) : (<span className="italic opacity-50">-</span>)}
</div> </div>
@@ -163,7 +167,12 @@ export function CompanyTable({ apiBase, onRowClick, refreshKey, onImportClick }:
<tbody className="divide-y divide-slate-200 dark:divide-slate-800 bg-white dark:bg-slate-900"> <tbody className="divide-y divide-slate-200 dark:divide-slate-800 bg-white dark:bg-slate-900">
{data.map((c) => ( {data.map((c) => (
<tr key={c.id} onClick={() => onRowClick(c.id)} className="hover:bg-slate-50 dark:hover:bg-slate-800/50 cursor-pointer"> <tr key={c.id} onClick={() => onRowClick(c.id)} className="hover:bg-slate-50 dark:hover:bg-slate-800/50 cursor-pointer">
<td className="whitespace-nowrap px-3 py-4 text-sm font-medium text-slate-900 dark:text-white">{c.name}</td> <td className="whitespace-nowrap px-3 py-4 text-sm font-medium text-slate-900 dark:text-white">
<div className="flex items-center gap-2">
<Flag className={clsx("h-3 w-3 text-slate-300 dark:text-slate-600", c.has_pending_mistakes && "text-red-500 fill-red-500")} />
<span>{c.name}</span>
</div>
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-slate-500 dark:text-slate-400"> <td className="whitespace-nowrap px-3 py-4 text-sm text-slate-500 dark:text-slate-400">
{c.city && c.country ? `${c.city}, (${c.country})` : '-'} {c.city && c.country ? `${c.city}, (${c.country})` : '-'}
</td> </td>

View File

@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import axios from 'axios' import axios from 'axios'
import { X, ExternalLink, Bot, Briefcase, Calendar, Globe, Users, DollarSign, MapPin, Tag, RefreshCw as RefreshCwIcon, Search as SearchIcon, Pencil, Check, Download, Clock, Lock, Unlock, Calculator, Ruler, Database, Trash2, Flag } from 'lucide-react' import { X, ExternalLink, Bot, Briefcase, Calendar, Globe, Users, DollarSign, MapPin, Tag, RefreshCw as RefreshCwIcon, Search as SearchIcon, Pencil, Check, Download, Clock, Lock, Unlock, Calculator, Ruler, Database, Trash2, Flag, AlertTriangle } from 'lucide-react'
import clsx from 'clsx' import clsx from 'clsx'
import { ContactsManager, Contact } from './ContactsManager' import { ContactsManager, Contact } from './ContactsManager'
@@ -48,6 +48,20 @@ type CompanyDetail = {
metric_confidence_reason: string | null metric_confidence_reason: string | null
} }
// NEW
type ReportedMistake = {
id: number;
field_name: string;
wrong_value: string | null;
corrected_value: string | null;
source_url: string | null;
quote: string | null;
user_comment: string | null;
status: 'PENDING' | 'APPROVED' | 'REJECTED';
created_at: string;
};
export function Inspector({ companyId, initialContactId, onClose, apiBase }: InspectorProps) { export function Inspector({ companyId, initialContactId, onClose, apiBase }: InspectorProps) {
const [data, setData] = useState<CompanyDetail | null>(null) const [data, setData] = useState<CompanyDetail | null>(null)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@@ -56,6 +70,7 @@ export function Inspector({ companyId, initialContactId, onClose, apiBase }: Ins
// NEW: Report Mistake State // NEW: Report Mistake State
const [isReportingMistake, setIsReportingMistake] = useState(false) const [isReportingMistake, setIsReportingMistake] = useState(false)
const [existingMistakes, setExistingMistakes] = useState<ReportedMistake[]>([])
const [reportedFieldName, setReportedFieldName] = useState("") const [reportedFieldName, setReportedFieldName] = useState("")
const [reportedWrongValue, setReportedWrongValue] = useState("") const [reportedWrongValue, setReportedWrongValue] = useState("")
const [reportedCorrectedValue, setReportedCorrectedValue] = useState("") const [reportedCorrectedValue, setReportedCorrectedValue] = useState("")
@@ -100,11 +115,14 @@ export function Inspector({ companyId, initialContactId, onClose, apiBase }: Ins
if (!companyId) return if (!companyId) return
if (!silent) setLoading(true) if (!silent) setLoading(true)
axios.get(`${apiBase}/companies/${companyId}`) const companyRequest = axios.get(`${apiBase}/companies/${companyId}`)
.then(res => { const mistakesRequest = axios.get(`${apiBase}/mistakes?company_id=${companyId}`)
const newData = res.data
console.log("FETCHED COMPANY DATA:", newData) // DEBUG: Log raw data from API Promise.all([companyRequest, mistakesRequest])
.then(([companyRes, mistakesRes]) => {
const newData = companyRes.data
setData(newData) setData(newData)
setExistingMistakes(mistakesRes.data.items)
// Auto-stop processing if status changes to ENRICHED or we see data // Auto-stop processing if status changes to ENRICHED or we see data
if (isProcessing) { if (isProcessing) {
@@ -296,7 +314,15 @@ export function Inspector({ companyId, initialContactId, onClose, apiBase }: Ins
} }
} }
const handleLockToggle = async (sourceType: string, currentLockStatus: boolean) => {\n if (!companyId) return\n try {\n await axios.post(`${apiBase}/enrichment/${companyId}/${sourceType}/lock?locked=${!currentLockStatus}`)\n fetchData(true) // Silent refresh\n } catch (e) {\n console.error(\"Lock toggle failed\", e)\n }\n }\n\n // NEW: Interface for reporting mistakes\n interface ReportedMistakeRequest {\n field_name: string;\n wrong_value?: string | null;\n corrected_value?: string | null;\n source_url?: string | null;\n quote?: string | null;\n user_comment?: string | null;\n }\n\n const handleReportMistake = async () => {\n if (!companyId) return;\n if (!reportedFieldName) {\n alert(\"Field Name is required.\");\n return;\n }\n\n setIsProcessing(true);\n try {\n const payload: ReportedMistakeRequest = {\n field_name: reportedFieldName,\n wrong_value: reportedWrongValue || null,\n corrected_value: reportedCorrectedValue || null,\n source_url: reportedSourceUrl || null,\n quote: reportedQuote || null,\n user_comment: reportedComment || null,\n };\n\n await axios.post(`${apiBase}/companies/${companyId}/report-mistake`, payload);\n alert(\"Mistake reported successfully!\");\n setIsReportingMistake(false);\n // Reset form fields\n setReportedFieldName(\"\");\n setReportedWrongValue(\"\");\ setReportedCorrectedValue(\"\");\n setReportedSourceUrl(\"\");\n setReportedQuote(\"\");\n setReportedComment(\"\");\n } catch (e) {\n alert(\"Failed to report mistake.\");\n console.error(e);\n } finally {\n setIsProcessing(false);\n }\n }; const handleLockToggle = async (sourceType: string, currentLockStatus: boolean) => {
if (!companyId) return
try {
await axios.post(`${apiBase}/enrichment/${companyId}/${sourceType}/lock?locked=${!currentLockStatus}`)
fetchData(true) // Silent refresh
} catch (e) {
console.error("Lock toggle failed", e)
}
}
// NEW: Interface for reporting mistakes // NEW: Interface for reporting mistakes
interface ReportedMistakeRequest { interface ReportedMistakeRequest {
@@ -336,6 +362,7 @@ export function Inspector({ companyId, initialContactId, onClose, apiBase }: Ins
setReportedSourceUrl(""); setReportedSourceUrl("");
setReportedQuote(""); setReportedQuote("");
setReportedComment(""); setReportedComment("");
fetchData(true); // Re-fetch to show the new mistake
} catch (e) { } catch (e) {
alert("Failed to report mistake."); alert("Failed to report mistake.");
console.error(e); console.error(e);
@@ -478,6 +505,36 @@ export function Inspector({ companyId, initialContactId, onClose, apiBase }: Ins
)} )}
</div> </div>
{/* Reported Mistakes Section */}
{existingMistakes.length > 0 && (
<div className="mt-4 p-4 bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800/50 rounded-lg">
<h4 className="flex items-center gap-2 text-sm font-bold text-orange-800 dark:text-orange-300 mb-3">
<AlertTriangle className="h-4 w-4" />
Existing Correction Proposals
</h4>
<div className="space-y-3 max-h-40 overflow-y-auto pr-2">
{existingMistakes.map(mistake => (
<div key={mistake.id} className="text-xs p-3 bg-white dark:bg-slate-800/50 rounded border border-slate-200 dark:border-slate-700/50">
<div className="flex justify-between items-start">
<span className="font-bold text-slate-800 dark:text-slate-200">{mistake.field_name}</span>
<span className={clsx("px-2 py-0.5 rounded-full text-[9px] font-medium", {
'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/50 dark:text-yellow-300': mistake.status === 'PENDING',
'bg-green-100 text-green-800 dark:bg-green-900/50 dark:text-green-300': mistake.status === 'APPROVED',
'bg-red-100 text-red-800 dark:bg-red-900/50 dark:text-red-300': mistake.status === 'REJECTED'
})}>
{mistake.status}
</span>
</div>
<p className="text-slate-600 dark:text-slate-400 mt-1">
<span className="line-through text-red-500/80">{mistake.wrong_value || 'N/A'}</span> <strong className="text-green-600 dark:text-green-400">{mistake.corrected_value || 'N/A'}</strong>
</p>
{mistake.user_comment && <p className="mt-2 text-slate-500 italic">"{mistake.user_comment}"</p>}
</div>
))}
</div>
</div>
)}
{/* Tab Navigation */} {/* Tab Navigation */}
<div className="mt-6 flex border-b border-slate-200 dark:border-slate-800"> <div className="mt-6 flex border-b border-slate-200 dark:border-slate-800">
<button <button