Files
pdf_oralia_vibe/frontend/src/pages/ExtractPage.vue
Bertrand Benjamin aa365dbb63 refactor(ui): passe les pages en pleine largeur
Chaque page plafonnait sa largeur differemment (max-w-4xl pour Config,
5xl pour IA, 6xl pour l'accueil et les documents, 7xl pour les revenus
et les depenses) : sur un ecran large, les donnees se tassaient au
centre. Les pages n'ont plus de plafond, le contenu occupe l'ecran avec
une marge laterale fixe.

Pour tirer parti de la place gagnee : Config affiche ses deux sections
cote a cote, et le split apercu PDF / donnees passe de 50-50 a 40-60 en
faveur des donnees.

Pendant le tagging, le bouton « Annuler » de l'en-tete d'edition est
masque : il faisait doublon avec celui du bandeau de tagging, qui a un
sens different (retour a l'edition, et non abandon). La sortie vers la
liste reste le fil d'Ariane.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 19:49:43 +02:00

295 lines
9.4 KiB
Vue

<template>
<div class="flex-1 flex overflow-hidden">
<!-- Left panel: Upload or PDF Preview -->
<div class="w-2/5 flex flex-col border-r border-gray-700">
<!-- Upload zone when no file -->
<div v-if="!pdfFile" class="flex-1 flex items-center justify-center p-8 bg-gray-950">
<div class="max-w-md w-full">
<div
class="border-2 border-dashed border-gray-700 rounded-lg p-8 text-center hover:border-blue-500 transition-colors cursor-pointer bg-gray-900/50"
@click="triggerFileInput"
@dragover.prevent="isDragging = true"
@dragleave.prevent="isDragging = false"
@drop.prevent="onDrop"
:class="{ 'border-blue-500 bg-blue-500/10': isDragging }"
>
<input
ref="fileInput"
type="file"
accept="application/pdf,.pdf"
class="hidden"
@change="onFileChange"
/>
<svg class="mx-auto h-12 w-12 text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<p class="text-gray-400 mb-2">Glisser un PDF ici</p>
<p class="text-sm text-gray-500">ou <span class="text-blue-400">cliquer pour parcourir</span></p>
</div>
</div>
</div>
<!-- PDF Preview when file loaded -->
<PdfPreview
v-else
:file="pdfFile"
:file-name="pdfFile.name"
class="flex-1"
/>
</div>
<!-- Right panel: JSON Viewer or Tagging Step -->
<div class="w-3/5 flex flex-col bg-gray-950">
<!-- Extract button bar -->
<div v-if="pdfFile && !extractedData && !isExtracting && !showTagging" class="flex-shrink-0 p-4 bg-gray-900 border-b border-gray-700">
<button
@click="extractCurrentFile"
class="btn btn-primary w-full py-2.5"
>
Extraire les donnees du PDF
</button>
</div>
<!-- Continue to tagging button (when data is extracted) -->
<div v-if="extractedData && !isExtracting && !showTagging" class="flex-shrink-0 p-4 bg-gray-900 border-b border-gray-700">
<!-- Duplicate warning -->
<div v-if="saveMessage && saveMessage.type === 'duplicate'" class="mb-3">
<div class="px-4 py-3 rounded-lg text-sm bg-amber-500/10 text-amber-400 border border-amber-500/30">
<div class="font-medium flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
{{ saveMessage.title }}
</div>
<div v-if="saveMessage.details" class="mt-1 text-xs opacity-90">{{ saveMessage.details }}</div>
</div>
</div>
<button
@click="goToTagging"
class="btn btn-primary w-full py-2.5"
>
Continuer vers le tagging
</button>
</div>
<!-- Tagging step -->
<TaggingStep
v-if="showTagging && extractedData"
:depenses="extractedData.data.recapitulatif_operations"
:is-duplicate="isDuplicate"
@save="handleTaggingSave"
@cancel="showTagging = false"
class="flex-1"
/>
<!-- Save feedback message -->
<div v-if="saveMessage && showTagging" class="flex-shrink-0 p-4 bg-gray-900 border-t border-gray-700">
<div
:class="[
'px-4 py-3 rounded-lg text-sm border',
saveMessage.type === 'success' ? 'bg-green-500/10 text-green-400 border-green-500/30' : '',
saveMessage.type === 'duplicate' ? 'bg-amber-500/10 text-amber-400 border-amber-500/30' : '',
saveMessage.type === 'error' ? 'bg-red-500/10 text-red-400 border-red-500/30' : ''
]"
>
<div class="font-medium">{{ saveMessage.title }}</div>
<div v-if="saveMessage.details" class="mt-1 text-xs opacity-80">{{ saveMessage.details }}</div>
</div>
</div>
<!-- Placeholder when no file -->
<div v-if="!pdfFile && !showTagging" class="flex-1 flex items-center justify-center text-gray-500">
<p>Selectionnez un PDF pour commencer</p>
</div>
<!-- JSON Viewer (only shown when not tagging) -->
<JsonViewer
v-if="pdfFile && !showTagging"
:data="extractedData"
@update:data="extractedData = $event"
:is-loading="isExtracting"
class="flex-1 overflow-hidden"
/>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { pendingFile } from '../store'
import PdfPreview from '../components/PdfPreview.vue'
import JsonViewer from '../components/JsonViewer.vue'
import TaggingStep from '../components/TaggingStep.vue'
const router = useRouter()
const fileInput = ref(null)
const isDragging = ref(false)
const pdfFile = ref(null)
const extractedData = ref(null)
const isExtracting = ref(false)
const isSaving = ref(false)
const saveMessage = ref(null)
const showTagging = ref(false)
const isDuplicate = ref(false)
// Check for pending file from store on mount
onMounted(() => {
if (pendingFile.value) {
pdfFile.value = pendingFile.value
pendingFile.value = null // Clear the store
extractCurrentFile()
}
})
function triggerFileInput() {
fileInput.value?.click()
}
function resetState() {
extractedData.value = null
saveMessage.value = null
showTagging.value = false
isDuplicate.value = false
}
function onFileChange(e) {
const file = e.target.files[0]
if (file && file.name.toLowerCase().endsWith('.pdf')) {
pdfFile.value = file
resetState()
extractCurrentFile()
}
e.target.value = ''
}
function onDrop(e) {
isDragging.value = false
const file = e.dataTransfer.files[0]
if (file && file.name.toLowerCase().endsWith('.pdf')) {
pdfFile.value = file
resetState()
extractCurrentFile()
}
}
async function extractCurrentFile() {
if (!pdfFile.value) return
isExtracting.value = true
saveMessage.value = null
try {
const formData = new FormData()
formData.append('file', pdfFile.value)
const response = await fetch('/api/extract', {
method: 'POST',
body: formData
})
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.detail || 'Erreur lors de l\'extraction')
}
const data = await response.json()
extractedData.value = data
// Check for duplicate immediately after extraction
await checkDuplicate()
} catch (err) {
console.error('Extraction error:', err)
alert('Erreur: ' + err.message)
} finally {
isExtracting.value = false
}
}
async function checkDuplicate() {
if (!extractedData.value) return
try {
const metadata = extractedData.value.data.metadata
const reference = metadata.document?.reference
const date = metadata.document?.date
if (!reference || !date) return
const response = await fetch(`/api/check-duplicate?reference=${encodeURIComponent(reference)}&date=${encodeURIComponent(date)}`)
const result = await response.json()
if (result.exists) {
isDuplicate.value = true
saveMessage.value = {
type: 'duplicate',
title: '⚠️ Document déjà existant en base',
details: `Ce document (Référence: ${reference}, Date: ${date}) a déjà été importé. Vous pouvez continuer pour le réimporter (les anciennes données seront écrasées).`
}
} else {
isDuplicate.value = false
}
} catch (err) {
console.error('Duplicate check error:', err)
isDuplicate.value = false
// Don't block the flow if check fails
}
}
function goToTagging() {
showTagging.value = true
saveMessage.value = null
}
async function handleTaggingSave(depensesTags, shouldOverwrite) {
if (!extractedData.value) return
isSaving.value = true
saveMessage.value = null
try {
const formData = new FormData()
formData.append('pdf_file', pdfFile.value)
formData.append('data', JSON.stringify(extractedData.value.data))
if (depensesTags && depensesTags.length > 0) {
formData.append('depenses_tags', JSON.stringify(depensesTags))
}
formData.append('overwrite', shouldOverwrite ? 'true' : 'false')
const response = await fetch('/api/save-with-pdf', {
method: 'POST',
body: formData
})
const result = await response.json()
if (!response.ok) {
throw new Error(result.detail || 'Erreur lors de la sauvegarde')
}
if (result.success) {
// Success - redirect to home
router.push('/')
} else {
// Duplicate detected - stay on current view
saveMessage.value = {
type: 'duplicate',
title: 'Document deja existant',
details: `Reference: ${result.reference} | Date: ${result.date}`
}
}
} catch (err) {
console.error('Save error:', err)
saveMessage.value = {
type: 'error',
title: 'Erreur lors de la sauvegarde',
details: err.message
}
} finally {
isSaving.value = false
}
}
</script>