- Déduplication déterministe des identités, adresses, dates, entreprises, signatures et références - Implémentation dans src/services/ruleNer.ts et src/services/backendApi.ts - Clés de normalisation: prénom+nom, rue+CP+ville, nom+SIRET, type+valeur - Test ciblé tests/deduplication.test.ts pour valider la fonctionnalité - Documentation complète dans docs/deduplication_entites.md - Correction des tests existants (supertest, extractEntitiesFromText) - Compilation validée et services opérationnels
171 lines
6.3 KiB
TypeScript
171 lines
6.3 KiB
TypeScript
import { useEffect, useCallback, useRef } from 'react'
|
|
import './App.css'
|
|
import { AppRouter } from './router'
|
|
import { useAppDispatch, useAppSelector } from './store'
|
|
import { loadFolderResults, setBootstrapped, setCurrentFolderHash, setPollingInterval, stopPolling, setCurrentFolderName, createDefaultFolderThunk } from './store/documentSlice'
|
|
import { usePerformance } from './hooks/usePerformance'
|
|
import { useAccessibility } from './hooks/useAccessibility'
|
|
import './styles/accessibility.css'
|
|
|
|
export default function App() {
|
|
const dispatch = useAppDispatch()
|
|
const { documents, bootstrapped, currentFolderHash, folderResults, hasPending, pollingInterval } =
|
|
useAppSelector((state) => state.document)
|
|
const visibilityRef = useRef<boolean>(typeof document !== 'undefined' ? !document.hidden : true)
|
|
|
|
// Hooks d'optimisation
|
|
const { startRenderTimer, endRenderTimer } = usePerformance()
|
|
const { announceToScreenReader } = useAccessibility()
|
|
|
|
// Bootstrap au démarrage de l'application avec système de dossiers
|
|
useEffect(() => {
|
|
startRenderTimer()
|
|
console.log('🔍 [APP] useEffect déclenché:', {
|
|
documentsLength: documents.length,
|
|
bootstrapped,
|
|
currentFolderHash,
|
|
folderResultsLength: folderResults.length,
|
|
isDev: import.meta.env.DEV,
|
|
})
|
|
|
|
// Récupérer le hash du dossier depuis l'URL
|
|
const urlParams = new URLSearchParams(window.location.search)
|
|
const urlFolderHash = urlParams.get('hash')
|
|
|
|
console.log('🔍 [APP] Hash du dossier depuis URL:', urlFolderHash)
|
|
|
|
const initializeFolder = async () => {
|
|
try {
|
|
let folderHash = urlFolderHash || currentFolderHash
|
|
|
|
// Si un hash est passé dans l'URL, le prioriser et l'enregistrer
|
|
if (urlFolderHash && urlFolderHash !== currentFolderHash) {
|
|
dispatch(setCurrentFolderHash(urlFolderHash))
|
|
}
|
|
|
|
// Si aucun hash n'est disponible, demander le dossier par défaut au backend
|
|
if (!folderHash) {
|
|
const res = await dispatch(createDefaultFolderThunk()).unwrap()
|
|
folderHash = res.folderHash
|
|
dispatch(setCurrentFolderHash(folderHash))
|
|
console.log('📌 [APP] Dossier par défaut créé/récupéré:', folderHash)
|
|
}
|
|
|
|
// Charger les résultats du dossier
|
|
console.log('📁 [APP] Chargement des résultats du dossier:', folderHash)
|
|
await dispatch(loadFolderResults(folderHash)).unwrap()
|
|
|
|
// Marquer le bootstrap comme terminé
|
|
dispatch(setBootstrapped(true))
|
|
endRenderTimer()
|
|
announceToScreenReader('Application chargée avec succès')
|
|
console.log('🎉 [APP] Bootstrap terminé avec le dossier:', folderHash)
|
|
} catch (error) {
|
|
console.error("❌ [APP] Erreur lors de l'initialisation du dossier:", error)
|
|
}
|
|
}
|
|
|
|
// Ne pas refaire le bootstrap si déjà fait
|
|
if (bootstrapped) {
|
|
console.log('⏭️ [APP] Bootstrap déjà effectué, dossier:', currentFolderHash)
|
|
return
|
|
}
|
|
|
|
initializeFolder()
|
|
}, [dispatch, bootstrapped, currentFolderHash, folderResults.length, documents.length, startRenderTimer, endRenderTimer, announceToScreenReader])
|
|
|
|
// Listener pour appliquer le fallback de nom de dossier côté store
|
|
useEffect(() => {
|
|
const handler = (e: Event) => {
|
|
const name = (e as CustomEvent<string>).detail
|
|
if (typeof name === 'string' && name.length > 0) {
|
|
dispatch(setCurrentFolderName(name))
|
|
}
|
|
}
|
|
window.addEventListener('4nk:setFolderName', handler as EventListener)
|
|
return () => window.removeEventListener('4nk:setFolderName', handler as EventListener)
|
|
}, [dispatch])
|
|
|
|
// Fonction pour démarrer le polling
|
|
const startPolling = useCallback(
|
|
(folderHash: string) => {
|
|
console.log('🔄 [APP] Démarrage du polling pour le dossier:', folderHash)
|
|
|
|
let pollCount = 0
|
|
const maxPolls = 30 // Maximum d'itérations
|
|
|
|
const tick = () => {
|
|
if (pollCount >= maxPolls) {
|
|
console.log('⏹️ [APP] Arrêt du polling - limite atteinte')
|
|
dispatch(stopPolling())
|
|
return
|
|
}
|
|
|
|
if (!visibilityRef.current) {
|
|
// Onglet caché: replanifier sans requête
|
|
const hiddenDelay = 20000
|
|
console.log('⏸️ [APP] Onglet caché, report du polling de', hiddenDelay, 'ms')
|
|
const t = setTimeout(tick, hiddenDelay)
|
|
dispatch(setPollingInterval(t as any))
|
|
return
|
|
}
|
|
|
|
pollCount += 1
|
|
console.log(`🔄 [APP] Polling #${pollCount}`)
|
|
dispatch(loadFolderResults(folderHash))
|
|
|
|
// Backoff exponentiel doux basé sur le nombre d'itérations
|
|
const base = 12000
|
|
const factor = Math.min(4, Math.pow(2, Math.floor(pollCount / 5)))
|
|
const delay = base * factor
|
|
const t = setTimeout(tick, delay)
|
|
dispatch(setPollingInterval(t as any))
|
|
}
|
|
|
|
const t0 = setTimeout(tick, 0)
|
|
dispatch(setPollingInterval(t0 as any))
|
|
},
|
|
[dispatch],
|
|
)
|
|
|
|
// Fonction pour arrêter le polling
|
|
const stopPollingCallback = useCallback(() => {
|
|
console.log('⏹️ [APP] Arrêt du polling')
|
|
dispatch(stopPolling())
|
|
}, [dispatch])
|
|
|
|
// Gestion du polling basé sur l'état hasPending
|
|
useEffect(() => {
|
|
// Ne démarrer le polling que si on n'a encore jamais chargé ce dossier
|
|
// et seulement quand le backend indique des pending
|
|
if (hasPending && currentFolderHash && !pollingInterval) {
|
|
console.log('🔄 [APP] Démarrage du polling - fichiers en cours détectés')
|
|
startPolling(currentFolderHash)
|
|
} else if (!hasPending && pollingInterval) {
|
|
console.log('⏹️ [APP] Arrêt du polling - tous les fichiers traités')
|
|
stopPollingCallback()
|
|
}
|
|
}, [hasPending, currentFolderHash, pollingInterval, startPolling, stopPollingCallback])
|
|
|
|
// Pause/reprise du polling selon visibilité de la page
|
|
useEffect(() => {
|
|
const onVis = () => {
|
|
visibilityRef.current = !document.hidden
|
|
console.log('[APP] Visibilité changée, visible =', visibilityRef.current)
|
|
}
|
|
document.addEventListener('visibilitychange', onVis)
|
|
return () => document.removeEventListener('visibilitychange', onVis)
|
|
}, [])
|
|
|
|
// Nettoyage au démontage du composant
|
|
useEffect(() => {
|
|
return () => {
|
|
if (pollingInterval) {
|
|
clearInterval(pollingInterval)
|
|
}
|
|
}
|
|
}, [pollingInterval])
|
|
|
|
return <AppRouter />
|
|
}
|