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 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>