feat(entities): suppression unitaire + édition inline (personnes/adresses/entreprises) avec persistance cache

This commit is contained in:
4NK IA 2025-09-18 13:53:56 +00:00
parent 984c3838ae
commit 43ebc94b5b
4 changed files with 139 additions and 7 deletions

View File

@ -2267,6 +2267,48 @@ app.post('/api/folders/:folderHash/files/:fileHash/entities/delete', express.jso
}
})
// Mettre à jour une entité (personne/adresse/entreprise) dans le cache JSON
// Body: { kind: 'person'|'address'|'company', id?: string, index?: number, patch: object }
app.post('/api/folders/:folderHash/files/:fileHash/entities/update', express.json(), (req, res) => {
try {
const { folderHash, fileHash } = req.params
const { kind, id, index, patch } = req.body || {}
if (!['person', 'address', 'company'].includes(kind)) {
return res.status(400).json({ success: false, error: 'Paramètre kind invalide' })
}
if (!patch || typeof patch !== 'object') {
return res.status(400).json({ success: false, error: 'Patch invalide' })
}
const jsonPath = path.join('cache', folderHash, `${fileHash}.json`)
if (!fs.existsSync(jsonPath)) {
return res.status(404).json({ success: false, error: 'Résultat non trouvé' })
}
const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8'))
const ents = (((data || {}).extraction || {}).entities || {})
const map = { person: 'persons', address: 'addresses', company: 'companies' }
const key = map[kind]
if (!Array.isArray(ents[key])) ents[key] = []
let targetIndex = -1
if (typeof index === 'number' && index >= 0 && index < ents[key].length) {
targetIndex = index
} else if (typeof id === 'string' && id.length > 0) {
targetIndex = ents[key].findIndex((e) => e && e.id === id)
}
if (targetIndex < 0) {
return res.status(404).json({ success: false, error: 'Entité non trouvée' })
}
const before = ents[key][targetIndex] || {}
const updated = { ...before, ...patch }
ents[key][targetIndex] = updated
// Sauvegarder
fs.writeFileSync(jsonPath, JSON.stringify(data, null, 2))
return res.json({ success: true, kind, index: targetIndex, entity: updated })
} catch (e) {
return res.status(500).json({ success: false, error: e?.message || String(e) })
}
})
// Suppression d'un fichier d'un dossier (uploads + cache)
app.delete('/api/folders/:folderHash/files/:fileHash', (req, res) => {
try {

View File

@ -7,6 +7,7 @@ module.exports = {
env: {
NODE_ENV: 'production',
PORT: 3001,
OLLAMA_MIN_REVIEW_MS: process.env.OLLAMA_MIN_REVIEW_MS || 0,
},
instances: 1,
exec_mode: 'fork',

View File

@ -23,7 +23,7 @@ import {
import type { Document } from '../types'
import { useAppDispatch, useAppSelector } from '../store'
import { loadFolderResults } from '../store/documentSlice'
import { deleteEntity } from '../services/folderApi'
import { deleteEntity, updateEntity } from '../services/folderApi'
interface FilePreviewProps {
document: Document
@ -39,6 +39,7 @@ export const FilePreview: React.FC<FilePreviewProps> = ({ document, onClose }) =
const [page, setPage] = useState(1)
const [scale, setScale] = useState(1.0)
const [numPages, setNumPages] = useState(0)
const [savingKey, setSavingKey] = useState<string | null>(null)
useEffect(() => {
setLoading(true)
@ -323,8 +324,29 @@ export const FilePreview: React.FC<FilePreviewProps> = ({ document, onClose }) =
<Box>
{Array.isArray((fullResult?.extraction?.entities?.persons)) && fullResult.extraction.entities.persons.length > 0 ? (
fullResult.extraction.entities.persons.map((p: any, i: number) => (
<Box key={`p-${i}`} display="flex" alignItems="center" justifyContent="space-between" sx={{ py: 0.5 }}>
<Typography variant="body2">{p.firstName} {p.lastName}</Typography>
<Box key={`p-${i}`} display="flex" alignItems="center" justifyContent="space-between" sx={{ py: 0.5, gap: 1 }}>
<Box display="flex" alignItems="center" gap={1}>
<input style={{ padding: 4 }} defaultValue={p.firstName} onChange={(e) => (p.firstName = e.target.value)} />
<input style={{ padding: 4 }} defaultValue={p.lastName} onChange={(e) => (p.lastName = e.target.value)} />
</Box>
<Box display="flex" gap={1}>
<Button
size="small"
variant="outlined"
disabled={!currentFolderHash || savingKey === `p-${i}`}
onClick={async () => {
if (!currentFolderHash) return
try {
setSavingKey(`p-${i}`)
await updateEntity(currentFolderHash, document.id, 'person', { index: i, id: p.id, patch: { firstName: p.firstName, lastName: p.lastName } })
await dispatch(loadFolderResults(currentFolderHash)).unwrap()
} finally {
setSavingKey(null)
}
}}
>
Enregistrer
</Button>
<Button
size="small"
color="error"
@ -342,6 +364,7 @@ export const FilePreview: React.FC<FilePreviewProps> = ({ document, onClose }) =
>
Supprimer
</Button>
</Box>
</Box>
))
) : (
@ -354,8 +377,31 @@ export const FilePreview: React.FC<FilePreviewProps> = ({ document, onClose }) =
<Box>
{Array.isArray((fullResult?.extraction?.entities?.addresses)) && fullResult.extraction.entities.addresses.length > 0 ? (
fullResult.extraction.entities.addresses.map((a: any, i: number) => (
<Box key={`a-${i}`} display="flex" alignItems="center" justifyContent="space-between" sx={{ py: 0.5 }}>
<Typography variant="body2">{a.street}, {a.postalCode} {a.city} {a.country || ''}</Typography>
<Box key={`a-${i}`} display="flex" alignItems="center" justifyContent="space-between" sx={{ py: 0.5, gap: 1 }}>
<Box display="flex" alignItems="center" gap={1}>
<input style={{ padding: 4, width: 220 }} defaultValue={a.street} onChange={(e) => (a.street = e.target.value)} />
<input style={{ padding: 4, width: 100 }} defaultValue={a.postalCode} onChange={(e) => (a.postalCode = e.target.value)} />
<input style={{ padding: 4, width: 160 }} defaultValue={a.city} onChange={(e) => (a.city = e.target.value)} />
<input style={{ padding: 4, width: 120 }} defaultValue={a.country || ''} onChange={(e) => (a.country = e.target.value)} />
</Box>
<Box display="flex" gap={1}>
<Button
size="small"
variant="outlined"
disabled={!currentFolderHash || savingKey === `a-${i}`}
onClick={async () => {
if (!currentFolderHash) return
try {
setSavingKey(`a-${i}`)
await updateEntity(currentFolderHash, document.id, 'address', { index: i, id: a.id, patch: { street: a.street, city: a.city, postalCode: a.postalCode, country: a.country } })
await dispatch(loadFolderResults(currentFolderHash)).unwrap()
} finally {
setSavingKey(null)
}
}}
>
Enregistrer
</Button>
<Button
size="small"
color="error"
@ -372,6 +418,7 @@ export const FilePreview: React.FC<FilePreviewProps> = ({ document, onClose }) =
>
Supprimer
</Button>
</Box>
</Box>
))
) : (
@ -384,8 +431,28 @@ export const FilePreview: React.FC<FilePreviewProps> = ({ document, onClose }) =
<Box>
{Array.isArray((fullResult?.extraction?.entities?.companies)) && fullResult.extraction.entities.companies.length > 0 ? (
fullResult.extraction.entities.companies.map((c: any, i: number) => (
<Box key={`c-${i}`} display="flex" alignItems="center" justifyContent="space-between" sx={{ py: 0.5 }}>
<Typography variant="body2">{c.name}</Typography>
<Box key={`c-${i}`} display="flex" alignItems="center" justifyContent="space-between" sx={{ py: 0.5, gap: 1 }}>
<Box display="flex" alignItems="center" gap={1}>
<input style={{ padding: 4, width: 260 }} defaultValue={c.name} onChange={(e) => (c.name = e.target.value)} />
</Box>
<Box display="flex" gap={1}>
<Button
size="small"
variant="outlined"
disabled={!currentFolderHash || savingKey === `c-${i}`}
onClick={async () => {
if (!currentFolderHash) return
try {
setSavingKey(`c-${i}`)
await updateEntity(currentFolderHash, document.id, 'company', { index: i, id: c.id, patch: { name: c.name } })
await dispatch(loadFolderResults(currentFolderHash)).unwrap()
} finally {
setSavingKey(null)
}
}}
>
Enregistrer
</Button>
<Button
size="small"
color="error"
@ -402,6 +469,7 @@ export const FilePreview: React.FC<FilePreviewProps> = ({ document, onClose }) =
>
Supprimer
</Button>
</Box>
</Box>
))
) : (

View File

@ -364,3 +364,24 @@ export async function deleteEntity(
}
return response.json()
}
// Mettre à jour une entité dans le cache (merge partiel)
export async function updateEntity(
folderHash: string,
fileHash: string,
kind: 'person' | 'address' | 'company',
payload: { id?: string; index?: number; patch: Record<string, any> },
): Promise<{ success: boolean }>{
const response = await fetch(
`${API_BASE_URL}/folders/${folderHash}/files/${fileHash}/entities/update`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ kind, ...payload }),
},
)
if (!response.ok) {
throw new Error(`Erreur mise à jour entité: ${response.statusText}`)
}
return response.json()
}