diff --git a/docengine/app/utils/sanitizers.py b/docengine/app/utils/sanitizers.py index bb6f50c..964eb82 100644 --- a/docengine/app/utils/sanitizers.py +++ b/docengine/app/utils/sanitizers.py @@ -1,5 +1,7 @@ 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]: """ @@ -31,3 +33,30 @@ def sanitize_amount(text: str) -> Optional[float]: return float(cleaned_text) except ValueError: 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 + diff --git a/frontend/src/app/templates/templates.component.html b/frontend/src/app/templates/templates.component.html index e62edb4..fc6a23e 100644 --- a/frontend/src/app/templates/templates.component.html +++ b/frontend/src/app/templates/templates.component.html @@ -13,12 +13,26 @@ -
-
- +
+
+

Upload a document to view its extracted layout structure.

+ +
+
+ +
+
+
+
+ + Analyzing Document Layout... +
+

Extracting text, tables, and regions

+
+
diff --git a/frontend/src/app/templates/templates.component.scss b/frontend/src/app/templates/templates.component.scss index d46f485..152b485 100644 --- a/frontend/src/app/templates/templates.component.scss +++ b/frontend/src/app/templates/templates.component.scss @@ -225,3 +225,74 @@ .cdk-drag-animating { 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; } +} diff --git a/frontend/src/app/templates/templates.component.ts b/frontend/src/app/templates/templates.component.ts index 728a13a..615b3b8 100644 --- a/frontend/src/app/templates/templates.component.ts +++ b/frontend/src/app/templates/templates.component.ts @@ -55,6 +55,7 @@ export class TemplatesComponent implements OnInit { { label: 'TEXT', value: 'TEXT' }, { label: 'NUMBER', value: 'NUMBER' }, { label: 'DATE', value: 'DATE' }, + { label: 'DATETIME', value: 'DATETIME' }, { label: 'AMOUNT', value: 'AMOUNT' }, { label: 'ADDRESS', value: 'ADDRESS' }, { label: 'TABLE_COLUMN', value: 'TABLE_COLUMN' } @@ -79,6 +80,7 @@ export class TemplatesComponent implements OnInit { next: (res) => { this.documentId = res.id || res.pk_document_id; this.messageService.add({ severity: 'success', summary: 'Uploaded', detail: 'Document uploaded successfully.' }); + this.isScanning = true; this.fetchLayout(); }, error: (err) => { @@ -98,6 +100,8 @@ export class TemplatesComponent implements OnInit { setTimeout(() => this.fetchLayout(), 2000); return; } + + this.isScanning = false; this.layoutNodes = layouts; const pageMap = new Map(); @@ -300,6 +304,27 @@ export class TemplatesComponent implements OnInit { } else { 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) + } } }