OCR Template mapping done Now lets work on user and permission modules
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
import re
|
import re
|
||||||
from typing import Optional, Union
|
from typing import Optional
|
||||||
|
from datetime import datetime
|
||||||
|
from dateutil import parser
|
||||||
|
|
||||||
def sanitize_amount(text: str) -> Optional[float]:
|
def sanitize_amount(text: str) -> Optional[float]:
|
||||||
"""
|
"""
|
||||||
@@ -31,3 +33,30 @@ def sanitize_amount(text: str) -> Optional[float]:
|
|||||||
return float(cleaned_text)
|
return float(cleaned_text)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def sanitize_date(text: str, field_type: str = 'DATE') -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Sanitizes a string representing a date or datetime and converts it to ISO 8601 format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text (str): The raw extracted text from the document.
|
||||||
|
field_type (str): 'DATE' or 'DATETIME'. Determines the output format.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[str]: The sanitized date string in ISO format, or None if it could not be parsed.
|
||||||
|
"""
|
||||||
|
if not text:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# dateutil.parser is very robust at handling formats like "Dec 11, 2020", "11/12/2020", etc.
|
||||||
|
# fuzzy=True allows it to ignore extra words/characters around the date
|
||||||
|
parsed_date = parser.parse(text, fuzzy=True)
|
||||||
|
|
||||||
|
if field_type == 'DATETIME':
|
||||||
|
return parsed_date.strftime('%Y-%m-%dT%H:%M:%S')
|
||||||
|
else:
|
||||||
|
return parsed_date.strftime('%Y-%m-%d')
|
||||||
|
except (ValueError, TypeError, OverflowError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="preview-area p-4" style="overflow-y: auto;">
|
<div class="preview-area p-4" style="overflow-y: auto; position: relative;">
|
||||||
<div *ngIf="layoutNodes.length === 0" class="empty-state">
|
<div *ngIf="layoutNodes.length === 0 && !isScanning" class="empty-state">
|
||||||
<i class="pi pi-file-pdf text-4xl mb-3"></i>
|
<i class="pi pi-file text-4xl mb-3"></i>
|
||||||
<p>Upload a document to view its extracted layout structure.</p>
|
<p>Upload a document to view its extracted layout structure.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Scanning Overlay -->
|
||||||
|
<div *ngIf="isScanning" class="scanning-overlay">
|
||||||
|
<div class="scanner-container">
|
||||||
|
<i class="pi pi-file text-6xl text-gray-400 mb-4 scanner-doc"></i>
|
||||||
|
<div class="laser-beam"></div>
|
||||||
|
<div class="scanner-grid"></div>
|
||||||
|
</div>
|
||||||
|
<div class="scanning-text mt-4">
|
||||||
|
<i class="pi pi-spin pi-cog mr-2"></i>
|
||||||
|
<span class="font-semibold text-lg text-primary">Analyzing Document Layout...</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-500 mt-2">Extracting text, tables, and regions</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div *ngFor="let page of pages; let i = index" class="document-page-container mb-5">
|
<div *ngFor="let page of pages; let i = index" class="document-page-container mb-5">
|
||||||
<div class="page-header text-sm font-semibold text-gray-500 mb-2">Page {{ page.page_number }}</div>
|
<div class="page-header text-sm font-semibold text-gray-500 mb-2">Page {{ page.page_number }}</div>
|
||||||
|
|
||||||
|
|||||||
@@ -225,3 +225,74 @@
|
|||||||
.cdk-drag-animating {
|
.cdk-drag-animating {
|
||||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Scanning Overlay Animation */
|
||||||
|
.scanning-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scanner-container {
|
||||||
|
position: relative;
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f8f9fa;
|
||||||
|
box-shadow: inset 0 0 10px rgba(0,0,0,0.05);
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scanner-doc {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.laser-beam {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 3px;
|
||||||
|
background: var(--primary-color, #3b82f6);
|
||||||
|
box-shadow: 0 0 10px 2px var(--primary-color, #3b82f6);
|
||||||
|
z-index: 2;
|
||||||
|
animation: scan-laser 2s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scanner-grid {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-image: linear-gradient(var(--primary-100, #dbeafe) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, var(--primary-100, #dbeafe) 1px, transparent 1px);
|
||||||
|
background-size: 10px 10px;
|
||||||
|
z-index: 0;
|
||||||
|
opacity: 0.5;
|
||||||
|
animation: scan-grid 4s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scan-laser {
|
||||||
|
0% { top: -5%; opacity: 0; }
|
||||||
|
10% { opacity: 1; }
|
||||||
|
90% { opacity: 1; }
|
||||||
|
100% { top: 105%; opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scan-grid {
|
||||||
|
0% { background-position: 0 0; }
|
||||||
|
100% { background-position: 20px 20px; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export class TemplatesComponent implements OnInit {
|
|||||||
{ label: 'TEXT', value: 'TEXT' },
|
{ label: 'TEXT', value: 'TEXT' },
|
||||||
{ label: 'NUMBER', value: 'NUMBER' },
|
{ label: 'NUMBER', value: 'NUMBER' },
|
||||||
{ label: 'DATE', value: 'DATE' },
|
{ label: 'DATE', value: 'DATE' },
|
||||||
|
{ label: 'DATETIME', value: 'DATETIME' },
|
||||||
{ label: 'AMOUNT', value: 'AMOUNT' },
|
{ label: 'AMOUNT', value: 'AMOUNT' },
|
||||||
{ label: 'ADDRESS', value: 'ADDRESS' },
|
{ label: 'ADDRESS', value: 'ADDRESS' },
|
||||||
{ label: 'TABLE_COLUMN', value: 'TABLE_COLUMN' }
|
{ label: 'TABLE_COLUMN', value: 'TABLE_COLUMN' }
|
||||||
@@ -79,6 +80,7 @@ export class TemplatesComponent implements OnInit {
|
|||||||
next: (res) => {
|
next: (res) => {
|
||||||
this.documentId = res.id || res.pk_document_id;
|
this.documentId = res.id || res.pk_document_id;
|
||||||
this.messageService.add({ severity: 'success', summary: 'Uploaded', detail: 'Document uploaded successfully.' });
|
this.messageService.add({ severity: 'success', summary: 'Uploaded', detail: 'Document uploaded successfully.' });
|
||||||
|
this.isScanning = true;
|
||||||
this.fetchLayout();
|
this.fetchLayout();
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
@@ -98,6 +100,8 @@ export class TemplatesComponent implements OnInit {
|
|||||||
setTimeout(() => this.fetchLayout(), 2000);
|
setTimeout(() => this.fetchLayout(), 2000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.isScanning = false;
|
||||||
this.layoutNodes = layouts;
|
this.layoutNodes = layouts;
|
||||||
|
|
||||||
const pageMap = new Map<number, { page_number: number, nodes: DocumentLayout[], width: number, height: number }>();
|
const pageMap = new Map<number, { page_number: number, nodes: DocumentLayout[], width: number, height: number }>();
|
||||||
@@ -300,6 +304,27 @@ export class TemplatesComponent implements OnInit {
|
|||||||
} else {
|
} else {
|
||||||
clonedNode.text_value = '';
|
clonedNode.text_value = '';
|
||||||
}
|
}
|
||||||
|
} else if (field && (field.field_type === 'DATE' || field.field_type === 'DATETIME') && clonedNode.text_value) {
|
||||||
|
// Attempt to parse the date and output standard format
|
||||||
|
const timestamp = Date.parse(clonedNode.text_value);
|
||||||
|
if (!isNaN(timestamp)) {
|
||||||
|
const parsedDate = new Date(timestamp);
|
||||||
|
// Extract YYYY-MM-DD
|
||||||
|
const yyyy = parsedDate.getFullYear();
|
||||||
|
const mm = String(parsedDate.getMonth() + 1).padStart(2, '0');
|
||||||
|
const dd = String(parsedDate.getDate()).padStart(2, '0');
|
||||||
|
if (field.field_type === 'DATE') {
|
||||||
|
clonedNode.text_value = `${yyyy}-${mm}-${dd}`;
|
||||||
|
} else {
|
||||||
|
const hh = String(parsedDate.getHours()).padStart(2, '0');
|
||||||
|
const min = String(parsedDate.getMinutes()).padStart(2, '0');
|
||||||
|
const ss = String(parsedDate.getSeconds()).padStart(2, '0');
|
||||||
|
clonedNode.text_value = `${yyyy}-${mm}-${dd}T${hh}:${min}:${ss}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If we can't parse it reliably on frontend, we leave it or let backend handle it
|
||||||
|
// We'll leave it as is to give user visual feedback, backend parser is more robust (dateutil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user