feat(ui): chips remplacement image & confirmation adresse + API calls; docs qualité

This commit is contained in:
4NK IA 2025-09-18 10:18:45 +00:00
parent 5135b9aceb
commit 1118bbbf5d
4 changed files with 171 additions and 1 deletions

View File

@ -0,0 +1,20 @@
## Gestion de la qualité: remplacement d'image et confirmation d'adresse
### Backend
- Suggestions ajoutées dans `status.suggestions` des résultats:
- `needsReupload` (bool), `reasons` (array)
- `needsAddressConfirmation` (bool), `detectedAddress` (objet)
- Endpoint confirmation d'adresse:
- POST `/api/folders/:folderHash/files/:fileHash/confirm-address`
- Body `{ confirmed: true, address: { street, city, postalCode, country } }`
### Frontend (UploadView)
- Si `needsReupload`: chip “Qualité faible: remplacer” → ouvre un file picker, supprime loriginal et réuploade.
- Si `needsAddressConfirmation`: chip “Adresse à confirmer” → dialogue pré-rempli; POST de confirmation; rafraîchissement.
### Tests manuels
1) Télécharger une image de faible qualité → vérifier l'apparition du chip “Qualité faible: remplacer”.
2) Confirmer l'adresse détectée → vérifier que le chip disparaît après POST.
### Notes
- Lusage dun annuaire de noms (FR) pourra être ajouté pour rehausser la confiance sur NOM/PRÉNOM.

View File

@ -292,3 +292,35 @@ export async function uploadFileToFolder(file: File, folderHash: string): Promis
return response.json()
}
// Supprimer un fichier (uploads + cache)
export async function deleteFolderFile(folderHash: string, fileHash: string): Promise<{ success: boolean }>{
const response = await fetch(`${API_BASE_URL}/folders/${folderHash}/files/${fileHash}`, {
method: 'DELETE',
headers: { Accept: 'application/json' },
})
if (!response.ok) {
throw new Error(`Erreur lors de la suppression du fichier: ${response.statusText}`)
}
return response.json()
}
// Confirmer/corriger l'adresse détectée
export async function confirmDetectedAddress(
folderHash: string,
fileHash: string,
address: { street: string; city: string; postalCode: string; country?: string },
): Promise<{ success: boolean }>{
const response = await fetch(
`${API_BASE_URL}/folders/${folderHash}/files/${fileHash}/confirm-address`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ confirmed: true, address }),
},
)
if (!response.ok) {
throw new Error(`Erreur lors de la confirmation d'adresse: ${response.statusText}`)
}
return response.json()
}

View File

@ -408,6 +408,8 @@ const documentSlice = createSlice({
uploadDate: new Date(result.document.uploadTimestamp),
status: 'completed' as const,
previewUrl: `/api/folders/${action.payload.folderHash}/files/${result.fileHash}`,
// Transporter les suggestions backend jusqu'au front
suggestions: (result as any)?.status?.suggestions || undefined,
}
})
newDocuments.push(...completedDocs)

View File

@ -47,13 +47,16 @@ import {
import { Layout } from '../components/Layout'
import { FilePreview } from '../components/FilePreview'
import type { Document } from '../types'
import { confirmDetectedAddress, deleteFolderFile } from '../services/folderApi'
// Composant mémorisé pour les items de la liste
const DocumentListItem = memo(({ doc, index, onPreview, onDelete, totalCount }: {
const DocumentListItem = memo(({ doc, index, onPreview, onDelete, onReplace, onConfirmAddress, totalCount }: {
doc: Document,
index: number,
onPreview: (doc: Document) => void,
onDelete: (id: string) => void,
onReplace: (doc: Document) => void,
onConfirmAddress: (doc: Document) => void,
totalCount: number
}) => {
const getFileIcon = (mimeType: string) => {
@ -146,6 +149,22 @@ const DocumentListItem = memo(({ doc, index, onPreview, onDelete, totalCount }:
variant="outlined"
/>
)}
{doc.status === 'completed' && (doc as any)?.suggestions?.needsReupload && (
<Chip
color="warning"
label="Qualité faible: remplacer"
size="small"
onClick={() => onReplace(doc)}
/>
)}
{doc.status === 'completed' && (doc as any)?.suggestions?.needsAddressConfirmation && (
<Chip
color="info"
label="Adresse à confirmer"
size="small"
onClick={() => onConfirmAddress(doc)}
/>
)}
</Box>
</Box>
}
@ -197,6 +216,58 @@ export default function UploadView() {
const [newFolderDesc, setNewFolderDesc] = useState('')
const [newFolderHash, setNewFolderHash] = useState('')
// Dialogue de confirmation d'adresse
const [addressDialogOpen, setAddressDialogOpen] = useState(false)
const [addressDraft, setAddressDraft] = useState({ street: '', city: '', postalCode: '', country: 'France' })
const [addressDoc, setAddressDoc] = useState<Document | null>(null)
const handleConfirmAddress = useCallback((doc: Document) => {
try {
const suggestions: any = (doc as any).suggestions || {}
const addr = suggestions.detectedAddress || { street: '', city: '', postalCode: '', country: 'France' }
setAddressDraft({
street: addr.street || '',
city: addr.city || '',
postalCode: addr.postalCode || '',
country: addr.country || 'France',
})
setAddressDoc(doc)
setAddressDialogOpen(true)
} catch (e) {
console.error('❌ Préparation confirmation adresse:', e)
}
}, [])
const submitAddressConfirmation = useCallback(async () => {
if (!currentFolderHash || !addressDoc) return
try {
await confirmDetectedAddress(currentFolderHash, addressDoc.id, addressDraft)
setAddressDialogOpen(false)
await dispatch(loadFolderResults(currentFolderHash)).unwrap()
} catch (e) {
console.error('❌ Confirmation adresse:', e)
}
}, [currentFolderHash, addressDoc, addressDraft, dispatch])
// Remplacement d'un fichier (qualité faible)
const handleReplace = useCallback((doc: Document) => {
const input = document.createElement('input')
input.type = 'file'
input.accept = 'image/*,application/pdf'
input.onchange = async () => {
const file = (input.files && input.files[0]) || null
if (!file || !currentFolderHash) return
try {
await deleteFolderFile(currentFolderHash, doc.id)
await dispatch(uploadFileToFolderThunk({ file, folderHash: currentFolderHash })).unwrap()
await dispatch(loadFolderResults(currentFolderHash)).unwrap()
} catch (e) {
console.error('❌ Remplacement de document:', e)
}
}
input.click()
}, [currentFolderHash, dispatch])
// Créer un nouveau dossier
const handleCreateNewFolder = useCallback(async () => {
try {
@ -440,6 +511,8 @@ export default function UploadView() {
index={index}
onPreview={setPreviewDocument}
onDelete={handleDelete}
onReplace={handleReplace}
onConfirmAddress={handleConfirmAddress}
totalCount={memoizedDocuments.length}
/>
))}
@ -558,6 +631,49 @@ export default function UploadView() {
</Button>
</DialogActions>
</Dialog>
{/* Dialogue de confirmation d'adresse */}
<Dialog open={addressDialogOpen} onClose={() => setAddressDialogOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle>Confirmer l'adresse détectée</DialogTitle>
<DialogContent>
<TextField
margin="dense"
label="Rue"
fullWidth
variant="outlined"
value={addressDraft.street}
onChange={(e) => setAddressDraft({ ...addressDraft, street: e.target.value })}
/>
<TextField
margin="dense"
label="Code postal"
fullWidth
variant="outlined"
value={addressDraft.postalCode}
onChange={(e) => setAddressDraft({ ...addressDraft, postalCode: e.target.value })}
/>
<TextField
margin="dense"
label="Ville"
fullWidth
variant="outlined"
value={addressDraft.city}
onChange={(e) => setAddressDraft({ ...addressDraft, city: e.target.value })}
/>
<TextField
margin="dense"
label="Pays"
fullWidth
variant="outlined"
value={addressDraft.country}
onChange={(e) => setAddressDraft({ ...addressDraft, country: e.target.value })}
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setAddressDialogOpen(false)}>Annuler</Button>
<Button variant="contained" onClick={submitAddressConfirmation}>Confirmer</Button>
</DialogActions>
</Dialog>
</Layout>
)
}