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()
|
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),
|
uploadDate: new Date(result.document.uploadTimestamp),
|
||||||
status: 'completed' as const,
|
status: 'completed' as const,
|
||||||
previewUrl: `/api/folders/${action.payload.folderHash}/files/${result.fileHash}`,
|
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)
|
newDocuments.push(...completedDocs)
|
||||||
|
|||||||
@ -47,13 +47,16 @@ import {
|
|||||||
import { Layout } from '../components/Layout'
|
import { Layout } from '../components/Layout'
|
||||||
import { FilePreview } from '../components/FilePreview'
|
import { FilePreview } from '../components/FilePreview'
|
||||||
import type { Document } from '../types'
|
import type { Document } from '../types'
|
||||||
|
import { confirmDetectedAddress, deleteFolderFile } from '../services/folderApi'
|
||||||
|
|
||||||
// Composant mémorisé pour les items de la liste
|
// 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,
|
doc: Document,
|
||||||
index: number,
|
index: number,
|
||||||
onPreview: (doc: Document) => void,
|
onPreview: (doc: Document) => void,
|
||||||
onDelete: (id: string) => void,
|
onDelete: (id: string) => void,
|
||||||
|
onReplace: (doc: Document) => void,
|
||||||
|
onConfirmAddress: (doc: Document) => void,
|
||||||
totalCount: number
|
totalCount: number
|
||||||
}) => {
|
}) => {
|
||||||
const getFileIcon = (mimeType: string) => {
|
const getFileIcon = (mimeType: string) => {
|
||||||
@ -146,6 +149,22 @@ const DocumentListItem = memo(({ doc, index, onPreview, onDelete, totalCount }:
|
|||||||
variant="outlined"
|
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>
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
@ -197,6 +216,58 @@ export default function UploadView() {
|
|||||||
const [newFolderDesc, setNewFolderDesc] = useState('')
|
const [newFolderDesc, setNewFolderDesc] = useState('')
|
||||||
const [newFolderHash, setNewFolderHash] = 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
|
// Créer un nouveau dossier
|
||||||
const handleCreateNewFolder = useCallback(async () => {
|
const handleCreateNewFolder = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@ -440,6 +511,8 @@ export default function UploadView() {
|
|||||||
index={index}
|
index={index}
|
||||||
onPreview={setPreviewDocument}
|
onPreview={setPreviewDocument}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
|
onReplace={handleReplace}
|
||||||
|
onConfirmAddress={handleConfirmAddress}
|
||||||
totalCount={memoizedDocuments.length}
|
totalCount={memoizedDocuments.length}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@ -558,6 +631,49 @@ export default function UploadView() {
|
|||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</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>
|
</Layout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user