Consolidate Reports and Data Entry apps into a single Vue.js SPA with: Architecture: - Module-based structure with lazy-loaded routes (@reports, @data-entry) - Error boundaries per module to prevent cascade failures - Dual API proxy in Vite for microservices (reports:8001, data-entry:8003) - Pinia store factories for shared auth, company, and period stores - Vite path aliases for clear module boundaries (@shared, @reports, @data-entry) Service Management: - Granular service control scripts (backend-reports.sh, backend-data-entry.sh, bot.sh, frontend.sh) - 87% faster frontend restart: 7s vs 53s full restart - 38% faster full startup: 33s vs 53s via parallel backend initialization - Enhanced start-dev.sh with proper service timeouts (OCR: 30s, Vite: 15s, Bot: 10s) - status.sh for comprehensive health checks Features: - Auto-select first company on login with period auto-load - Hamburger menu with feature toggle support - JWT token auto-injection via axios interceptors - Unified header with company/period selectors - IIS web.config for production deployment with multi-API routing UX Improvements: - Vue watchers for reactive company/period loading - Lazy store initialization with graceful error handling - Period persistence per user+company in localStorage - Feature flags for optional modules Deployment: - Single IIS site serves unified frontend with API proxy rules - Maintains separate backend processes for microservices - Windows line ending fixes (.env CRLF → LF conversion) Stats: 112 files changed, 38,342 insertions(+), 2,342 deletions(-) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
282 lines
6.1 KiB
Vue
282 lines
6.1 KiB
Vue
<template>
|
|
<div class="ocr-upload-zone">
|
|
<div
|
|
class="upload-dropzone"
|
|
:class="{ 'dragging': isDragging, 'processing': processing }"
|
|
@dragover.prevent="onDragOver"
|
|
@dragleave.prevent="onDragLeave"
|
|
@drop.prevent="onDrop"
|
|
@click="triggerFileInput"
|
|
>
|
|
<input
|
|
ref="fileInput"
|
|
type="file"
|
|
accept="image/*,application/pdf"
|
|
class="hidden-input"
|
|
@change="onFileSelected"
|
|
/>
|
|
|
|
<div v-if="processing" class="processing-state">
|
|
<ProgressSpinner
|
|
style="width: 50px; height: 50px"
|
|
strokeWidth="4"
|
|
/>
|
|
<p class="processing-text">Se proceseaza imaginea...</p>
|
|
<p class="processing-subtext">Acest proces poate dura cateva secunde</p>
|
|
</div>
|
|
|
|
<div v-else-if="selectedFile" class="file-selected-state">
|
|
<i class="pi pi-check-circle" style="font-size: 1.75rem; color: #22c55e;"></i>
|
|
<p class="file-name">{{ selectedFile.name }}</p>
|
|
<p class="file-size">{{ formatFileSize(selectedFile.size) }}</p>
|
|
<div class="file-actions">
|
|
<Button
|
|
label="Schimba"
|
|
icon="pi pi-refresh"
|
|
severity="secondary"
|
|
size="small"
|
|
@click.stop="triggerFileInput"
|
|
/>
|
|
<Button
|
|
label="Proceseaza OCR"
|
|
icon="pi pi-cog"
|
|
size="small"
|
|
@click.stop="processOCR"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="empty-state">
|
|
<i class="pi pi-camera" style="font-size: 2rem; color: #667eea;"></i>
|
|
<p class="main-text">
|
|
<span v-if="isDragging">Elibereaza pentru a incarca</span>
|
|
<span v-else>Trage poza bonului aici sau click pentru a selecta</span>
|
|
</p>
|
|
<p class="sub-text">
|
|
JPG, PNG, PDF (max 10MB) • OCR extrage automat datele
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- OCR Error Message -->
|
|
<Message v-if="error" severity="error" :closable="true" @close="error = null">
|
|
{{ error }}
|
|
</Message>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref } from 'vue'
|
|
import api from '@data-entry/services/api'
|
|
|
|
const emit = defineEmits(['ocr-result', 'file-selected', 'error'])
|
|
|
|
const fileInput = ref(null)
|
|
const selectedFile = ref(null)
|
|
const isDragging = ref(false)
|
|
const processing = ref(false)
|
|
const error = ref(null)
|
|
|
|
const onDragOver = () => {
|
|
isDragging.value = true
|
|
}
|
|
|
|
const onDragLeave = () => {
|
|
isDragging.value = false
|
|
}
|
|
|
|
const onDrop = (event) => {
|
|
isDragging.value = false
|
|
const files = event.dataTransfer?.files
|
|
if (files?.length > 0) {
|
|
handleFile(files[0])
|
|
}
|
|
}
|
|
|
|
const triggerFileInput = () => {
|
|
fileInput.value?.click()
|
|
}
|
|
|
|
const onFileSelected = (event) => {
|
|
const files = event.target?.files
|
|
if (files?.length > 0) {
|
|
handleFile(files[0])
|
|
}
|
|
}
|
|
|
|
const handleFile = (file) => {
|
|
// Validate file type
|
|
const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf']
|
|
if (!allowedTypes.includes(file.type)) {
|
|
error.value = 'Tip de fisier invalid. Sunt acceptate doar: JPG, PNG, PDF'
|
|
return
|
|
}
|
|
|
|
// Validate file size (10MB)
|
|
if (file.size > 10 * 1024 * 1024) {
|
|
error.value = 'Fisierul este prea mare. Dimensiunea maxima este 10MB.'
|
|
return
|
|
}
|
|
|
|
error.value = null
|
|
selectedFile.value = file
|
|
emit('file-selected', file)
|
|
}
|
|
|
|
const processOCR = async () => {
|
|
if (!selectedFile.value) return
|
|
|
|
processing.value = true
|
|
error.value = null
|
|
|
|
try {
|
|
const formData = new FormData()
|
|
formData.append('file', selectedFile.value)
|
|
|
|
const response = await api.post('/ocr/extract', formData, {
|
|
headers: { 'Content-Type': 'multipart/form-data' },
|
|
timeout: 60000, // 60 second timeout for OCR
|
|
})
|
|
|
|
if (response.data.success) {
|
|
// Include the OCR message in the data for debugging
|
|
const resultData = {
|
|
...response.data.data,
|
|
_ocr_message: response.data.message
|
|
}
|
|
emit('ocr-result', resultData)
|
|
} else {
|
|
error.value = response.data.message || 'OCR processing failed'
|
|
emit('error', error.value)
|
|
}
|
|
} catch (err) {
|
|
const message = err.response?.data?.detail || err.message || 'Eroare la procesarea OCR'
|
|
error.value = message
|
|
emit('error', message)
|
|
} finally {
|
|
processing.value = false
|
|
}
|
|
}
|
|
|
|
const formatFileSize = (bytes) => {
|
|
if (bytes < 1024) return bytes + ' B'
|
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
|
|
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
|
|
}
|
|
|
|
const reset = () => {
|
|
selectedFile.value = null
|
|
error.value = null
|
|
if (fileInput.value) {
|
|
fileInput.value.value = ''
|
|
}
|
|
}
|
|
|
|
// Expose methods for parent components
|
|
defineExpose({ reset, processOCR })
|
|
</script>
|
|
|
|
<style scoped>
|
|
.ocr-upload-zone {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.upload-dropzone {
|
|
border: 2px dashed #cbd5e1;
|
|
border-radius: 10px;
|
|
padding: 1rem 1.25rem;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
background: #f8fafc;
|
|
}
|
|
|
|
.upload-dropzone:hover {
|
|
border-color: #667eea;
|
|
background: #f1f5f9;
|
|
}
|
|
|
|
.upload-dropzone.dragging {
|
|
border-color: #667eea;
|
|
background: #eef2ff;
|
|
transform: scale(1.02);
|
|
}
|
|
|
|
.upload-dropzone.processing {
|
|
cursor: default;
|
|
background: #fefefe;
|
|
}
|
|
|
|
.hidden-input {
|
|
display: none;
|
|
}
|
|
|
|
/* Empty state - compact */
|
|
.empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.main-text {
|
|
font-size: 0.9rem;
|
|
color: #475569;
|
|
margin: 0.25rem 0;
|
|
}
|
|
|
|
.sub-text {
|
|
font-size: 0.8rem;
|
|
color: #94a3b8;
|
|
margin: 0;
|
|
}
|
|
|
|
/* File selected state - compact */
|
|
.file-selected-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 0.15rem;
|
|
}
|
|
|
|
.file-name {
|
|
font-weight: 600;
|
|
font-size: 0.9rem;
|
|
color: #1e293b;
|
|
margin: 0.25rem 0 0 0;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.file-size {
|
|
font-size: 0.8rem;
|
|
color: #64748b;
|
|
margin: 0;
|
|
}
|
|
|
|
.file-actions {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
/* Processing state */
|
|
.processing-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.processing-text {
|
|
font-size: 1rem;
|
|
color: #475569;
|
|
margin: 0.5rem 0 0 0;
|
|
}
|
|
|
|
.processing-subtext {
|
|
font-size: 0.85rem;
|
|
color: #94a3b8;
|
|
margin: 0;
|
|
}
|
|
</style>
|