feat(reporting): Implement 'Report Mistake' feature with API and UI [2f388f42]

This commit is contained in:
2026-01-27 09:00:20 +00:00
parent 5908c2e403
commit 832b894fbf
6 changed files with 447 additions and 22 deletions

View File

@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
import axios from 'axios'
import { X, Bot, Tag, Target, Users, Plus, Trash2, Save } from 'lucide-react'
import { X, Bot, Tag, Target, Users, Plus, Trash2, Save, Flag, Check, Ban } from 'lucide-react'
import clsx from 'clsx'
interface RoboticsSettingsProps {
@@ -9,27 +9,46 @@ interface RoboticsSettingsProps {
apiBase: string
}
type ReportedMistake = {
id: number;
company_id: number;
company: { name: string }; // Assuming company name is eagerly loaded
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;
updated_at: string;
}
export function RoboticsSettings({ isOpen, onClose, apiBase }: RoboticsSettingsProps) {
const [activeTab, setActiveTab] = useState<'robotics' | 'industries' | 'roles'>(
localStorage.getItem('roboticsSettingsActiveTab') as 'robotics' | 'industries' | 'roles' || 'robotics'
const [activeTab, setActiveTab] = useState<'robotics' | 'industries' | 'roles' | 'mistakes'>(
localStorage.getItem('roboticsSettingsActiveTab') as 'robotics' | 'industries' | 'roles' | 'mistakes' || 'robotics'
)
const [roboticsCategories, setRoboticsCategories] = useState<any[]>([])
const [industries, setIndustries] = useState<any[]>([])
const [jobRoles, setJobRoles] = useState<any[]>([])
const [reportedMistakes, setReportedMistakes] = useState<ReportedMistake[]>([])
const [currentMistakeStatusFilter, setCurrentMistakeStatusFilter] = useState<string>("PENDING");
const [isLoading, setIsLoading] = useState(false);
const fetchAllData = async () => {
setIsLoading(true);
try {
const [resRobotics, resIndustries, resJobRoles] = await Promise.all([
const [resRobotics, resIndustries, resJobRoles, resMistakes] = await Promise.all([
axios.get(`${apiBase}/robotics/categories`),
axios.get(`${apiBase}/industries`),
axios.get(`${apiBase}/job_roles`),
axios.get(`${apiBase}/mistakes?status=${currentMistakeStatusFilter}`),
]);
setRoboticsCategories(resRobotics.data);
setIndustries(resIndustries.data);
setJobRoles(resJobRoles.data);
setReportedMistakes(resMistakes.data.items);
} catch (e) {
console.error("Failed to fetch settings data:", e);
alert("Fehler beim Laden der Settings. Siehe Konsole.");
@@ -62,6 +81,19 @@ export function RoboticsSettings({ isOpen, onClose, apiBase }: RoboticsSettingsP
}
}
const handleUpdateMistakeStatus = async (mistakeId: number, newStatus: 'APPROVED' | 'REJECTED') => {
setIsLoading(true);
try {
await axios.put(`${apiBase}/mistakes/${mistakeId}`, { status: newStatus });
fetchAllData(); // Refresh all data, including mistakes
} catch (e) {
alert("Failed to update mistake status");
console.error(e);
} finally {
setIsLoading(false);
}
};
const handleAddJobRole = async () => {
setIsLoading(true);
try {
@@ -109,6 +141,7 @@ export function RoboticsSettings({ isOpen, onClose, apiBase }: RoboticsSettingsP
{ id: 'robotics', label: 'Robotics Potential', icon: Bot },
{ id: 'industries', label: 'Industry Focus', icon: Target },
{ id: 'roles', label: 'Job Role Mapping', icon: Users },
{ id: 'mistakes', label: 'Reported Mistakes', icon: Flag },
].map(t => (
<button
key={t.id}
@@ -190,6 +223,89 @@ export function RoboticsSettings({ isOpen, onClose, apiBase }: RoboticsSettingsP
</table>
</div>
</div>
<div key="mistakes-content" className={clsx("space-y-4", { 'hidden': isLoading || activeTab !== 'mistakes' })}>
<div className="flex justify-between items-center">
<h3 className="text-sm font-bold text-slate-700 dark:text-slate-300">Reported Data Mistakes</h3>
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500">Filter:</span>
<select
value={currentMistakeStatusFilter}
onChange={e => setCurrentMistakeStatusFilter(e.target.value)}
className="bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 rounded-md px-2 py-1 text-xs text-slate-900 dark:text-white focus:ring-1 focus:ring-blue-500 outline-none"
>
<option value="PENDING">Pending</option>
<option value="APPROVED">Approved</option>
<option value="REJECTED">Rejected</option>
<option value="ALL">All</option>
</select>
</div>
</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">Company</th>
<th className="p-3">Field</th>
<th className="p-3">Wrong Value</th>
<th className="p-3">Corrected Value</th>
<th className="p-3">Source / Quote / Comment</th>
<th className="p-3">Status</th>
<th className="p-3 w-10">Actions</th>
</tr></thead>
<tbody className="divide-y divide-slate-200 dark:divide-slate-800">
{reportedMistakes.length > 0 ? (
reportedMistakes.map(mistake => (
<tr key={mistake.id} className="group">
<td className="p-2 font-medium text-slate-900 dark:text-slate-200">{mistake.company.name}</td>
<td className="p-2 text-slate-700 dark:text-slate-300">{mistake.field_name}</td>
<td className="p-2 text-red-600 dark:text-red-400">{mistake.wrong_value || '-'}</td>
<td className="p-2 text-green-600 dark:text-green-400">{mistake.corrected_value || '-'}</td>
<td className="p-2 text-slate-500">
{mistake.source_url && <a href={mistake.source_url} target="_blank" rel="noopener noreferrer" className="text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-1 mb-1"><ExternalLink className="h-3 w-3" /> Source</a>}
{mistake.quote && <p className="italic text-[10px] my-1">"{mistake.quote}"</p>}
{mistake.user_comment && <p className="text-[10px]">Comment: {mistake.user_comment}</p>}
</td>
<td className="p-2">
<span className={clsx("px-2 py-0.5 rounded-full text-[10px] font-semibold", {
"bg-yellow-100 text-yellow-700": mistake.status === "PENDING",
"bg-green-100 text-green-700": mistake.status === "APPROVED",
"bg-red-100 text-red-700": mistake.status === "REJECTED",
})}>
{mistake.status}
</span>
</td>
<td className="p-2 text-center">
{mistake.status === "PENDING" && (
<div className="flex gap-1 justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={() => handleUpdateMistakeStatus(mistake.id, "APPROVED")}
className="text-green-600 hover:text-green-700"
title="Approve Mistake"
>
<Check className="h-4 w-4" />
</button>
<button
onClick={() => handleUpdateMistakeStatus(mistake.id, "REJECTED")}
className="text-red-600 hover:text-red-700"
title="Reject Mistake"
>
<Ban className="h-4 w-4" />
</button>
</div>
)}
</td>
</tr>
))
) : (
<tr><td colSpan={7} className="p-8 text-center text-slate-500 italic">No reported mistakes found.</td></tr>
)}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>