feat(ui): chips remplacement image & confirmation adresse + API calls; docs qualité
This commit is contained in:
parent
5135b9aceb
commit
1118bbbf5d
20
docs/qualite_reupload_conf_adresse.md
Normal file
20
docs/qualite_reupload_conf_adresse.md
Normal 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 l’original 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
|
||||
- L’usage d’un annuaire de noms (FR) pourra être ajouté pour rehausser la confiance sur NOM/PRÉNOM.
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user