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>
295 lines
9.4 KiB
Vue
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>
|