Spaces:
Running
Running
Fabio Antonini Claude Sonnet 4.6 commited on
Commit ·
a617713
1
Parent(s): 3c9c028
feat: delete single analysis artefact in Perizie page
Browse files- backend/routers/analysis.py: add DELETE /analysis/{id} endpoint with
project access check and audit log
- frontend/src/lib/api.ts: add analysisApi.deleteOne()
- frontend/src/pages/ProjectDetailPage.tsx: add Trash2 button to each
AnalysisCard (red on hover), handleDeleteAnalysis() removes from local
state without full reload
- i18n: add delete_analysis key (it/en)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- backend/routers/analysis.py +17 -0
- frontend/src/i18n/en.json +1 -0
- frontend/src/i18n/it.json +1 -0
- frontend/src/lib/api.ts +1 -0
- frontend/src/pages/ProjectDetailPage.tsx +14 -2
backend/routers/analysis.py
CHANGED
|
@@ -387,6 +387,23 @@ async def clear_analyses(
|
|
| 387 |
await db.commit()
|
| 388 |
|
| 389 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
@router.get("/{analysis_id}/image")
|
| 391 |
async def get_analysis_image(
|
| 392 |
analysis_id: int,
|
|
|
|
| 387 |
await db.commit()
|
| 388 |
|
| 389 |
|
| 390 |
+
@router.delete("/{analysis_id}", status_code=status.HTTP_204_NO_CONTENT)
|
| 391 |
+
async def delete_analysis(
|
| 392 |
+
analysis_id: int,
|
| 393 |
+
db: AsyncSession = Depends(get_db),
|
| 394 |
+
current_user: User = Depends(get_current_user),
|
| 395 |
+
) -> None:
|
| 396 |
+
result = await db.execute(select(Analysis).where(Analysis.id == analysis_id))
|
| 397 |
+
analysis = result.scalar_one_or_none()
|
| 398 |
+
if analysis is None:
|
| 399 |
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Analisi non trovata.")
|
| 400 |
+
await _check_project_access(analysis.project_id, db, current_user)
|
| 401 |
+
await db.delete(analysis)
|
| 402 |
+
await log_event(db, current_user, AuditAction.analysis_clear,
|
| 403 |
+
resource_type="analysis", resource_id=analysis_id)
|
| 404 |
+
await db.commit()
|
| 405 |
+
|
| 406 |
+
|
| 407 |
@router.get("/{analysis_id}/image")
|
| 408 |
async def get_analysis_image(
|
| 409 |
analysis_id: int,
|
frontend/src/i18n/en.json
CHANGED
|
@@ -62,6 +62,7 @@
|
|
| 62 |
},
|
| 63 |
"clear_analyses": "Clear analyses",
|
| 64 |
"clear_analyses_confirm": "Delete all analyses for this case? This cannot be undone.",
|
|
|
|
| 65 |
"running": "Running analysis…",
|
| 66 |
"done": "Analysis complete",
|
| 67 |
"select_reference_doc": "Select reference document",
|
|
|
|
| 62 |
},
|
| 63 |
"clear_analyses": "Clear analyses",
|
| 64 |
"clear_analyses_confirm": "Delete all analyses for this case? This cannot be undone.",
|
| 65 |
+
"delete_analysis": "Delete this artefact",
|
| 66 |
"running": "Running analysis…",
|
| 67 |
"done": "Analysis complete",
|
| 68 |
"select_reference_doc": "Select reference document",
|
frontend/src/i18n/it.json
CHANGED
|
@@ -62,6 +62,7 @@
|
|
| 62 |
},
|
| 63 |
"clear_analyses": "Cancella analisi",
|
| 64 |
"clear_analyses_confirm": "Eliminare tutte le analisi di questa perizia? L'operazione non è reversibile.",
|
|
|
|
| 65 |
"running": "Analisi in corso…",
|
| 66 |
"done": "Analisi completata",
|
| 67 |
"select_reference_doc": "Seleziona documento di riferimento",
|
|
|
|
| 62 |
},
|
| 63 |
"clear_analyses": "Cancella analisi",
|
| 64 |
"clear_analyses_confirm": "Eliminare tutte le analisi di questa perizia? L'operazione non è reversibile.",
|
| 65 |
+
"delete_analysis": "Elimina questo artefatto",
|
| 66 |
"running": "Analisi in corso…",
|
| 67 |
"done": "Analisi completata",
|
| 68 |
"select_reference_doc": "Seleziona documento di riferimento",
|
frontend/src/lib/api.ts
CHANGED
|
@@ -162,6 +162,7 @@ export const analysisApi = {
|
|
| 162 |
api.post<Analysis>("/analysis/pipeline", { project_id: projectId, document_id: documentId, reference_document_id: referenceDocumentId ?? null }),
|
| 163 |
list: (projectId: number) => api.get<Analysis[]>(`/analysis/project/${projectId}`),
|
| 164 |
clearAll: (projectId: number) => api.delete(`/analysis/project/${projectId}`),
|
|
|
|
| 165 |
imageUrl: (analysisId: number) => `/api/analysis/${analysisId}/image`,
|
| 166 |
}
|
| 167 |
|
|
|
|
| 162 |
api.post<Analysis>("/analysis/pipeline", { project_id: projectId, document_id: documentId, reference_document_id: referenceDocumentId ?? null }),
|
| 163 |
list: (projectId: number) => api.get<Analysis[]>(`/analysis/project/${projectId}`),
|
| 164 |
clearAll: (projectId: number) => api.delete(`/analysis/project/${projectId}`),
|
| 165 |
+
deleteOne: (analysisId: number) => api.delete(`/analysis/${analysisId}`),
|
| 166 |
imageUrl: (analysisId: number) => `/api/analysis/${analysisId}/image`,
|
| 167 |
}
|
| 168 |
|
frontend/src/pages/ProjectDetailPage.tsx
CHANGED
|
@@ -34,7 +34,7 @@ function AuthImage({ src, alt, className }: { src: string; alt: string; classNam
|
|
| 34 |
return <img src={blobSrc} alt={alt} className={className} />
|
| 35 |
}
|
| 36 |
|
| 37 |
-
function AnalysisCard({ analysis }: { analysis: Analysis }) {
|
| 38 |
const { t } = useTranslation()
|
| 39 |
const [open, setOpen] = useState(false)
|
| 40 |
const [imgSrc, setImgSrc] = useState<string | null>(null)
|
|
@@ -72,6 +72,13 @@ function AnalysisCard({ analysis }: { analysis: Analysis }) {
|
|
| 72 |
PDF
|
| 73 |
</Button>
|
| 74 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => setOpen((o) => !o)}>
|
| 76 |
{open ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
| 77 |
</Button>
|
|
@@ -203,6 +210,11 @@ export default function ProjectDetailPage() {
|
|
| 203 |
setAnalyses([])
|
| 204 |
}
|
| 205 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
if (loading) return <div className="p-6 text-muted-foreground">{t("common.loading")}</div>
|
| 207 |
if (!project) return <div className="p-6">{t("common.error")}</div>
|
| 208 |
|
|
@@ -355,7 +367,7 @@ export default function ProjectDetailPage() {
|
|
| 355 |
{analyses.length === 0 ? (
|
| 356 |
<p className="text-sm text-muted-foreground">{t("project.no_analyses")}</p>
|
| 357 |
) : (
|
| 358 |
-
analyses.map((a) => <AnalysisCard key={a.id} analysis={a} />)
|
| 359 |
)}
|
| 360 |
</div>
|
| 361 |
</div>
|
|
|
|
| 34 |
return <img src={blobSrc} alt={alt} className={className} />
|
| 35 |
}
|
| 36 |
|
| 37 |
+
function AnalysisCard({ analysis, onDelete }: { analysis: Analysis; onDelete: (id: number) => void }) {
|
| 38 |
const { t } = useTranslation()
|
| 39 |
const [open, setOpen] = useState(false)
|
| 40 |
const [imgSrc, setImgSrc] = useState<string | null>(null)
|
|
|
|
| 72 |
PDF
|
| 73 |
</Button>
|
| 74 |
)}
|
| 75 |
+
<Button
|
| 76 |
+
variant="ghost" size="icon" className="h-7 w-7 text-muted-foreground hover:text-destructive"
|
| 77 |
+
title={t("project.delete_analysis")}
|
| 78 |
+
onClick={() => onDelete(analysis.id)}
|
| 79 |
+
>
|
| 80 |
+
<Trash2 className="h-3.5 w-3.5" />
|
| 81 |
+
</Button>
|
| 82 |
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => setOpen((o) => !o)}>
|
| 83 |
{open ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
| 84 |
</Button>
|
|
|
|
| 210 |
setAnalyses([])
|
| 211 |
}
|
| 212 |
|
| 213 |
+
async function handleDeleteAnalysis(analysisId: number) {
|
| 214 |
+
await analysisApi.deleteOne(analysisId)
|
| 215 |
+
setAnalyses((a) => a.filter((x) => x.id !== analysisId))
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
if (loading) return <div className="p-6 text-muted-foreground">{t("common.loading")}</div>
|
| 219 |
if (!project) return <div className="p-6">{t("common.error")}</div>
|
| 220 |
|
|
|
|
| 367 |
{analyses.length === 0 ? (
|
| 368 |
<p className="text-sm text-muted-foreground">{t("project.no_analyses")}</p>
|
| 369 |
) : (
|
| 370 |
+
analyses.map((a) => <AnalysisCard key={a.id} analysis={a} onDelete={handleDeleteAnalysis} />)
|
| 371 |
)}
|
| 372 |
</div>
|
| 373 |
</div>
|