[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:
@@ -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"}
|
||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user