diff --git a/docengine/app/api/v1/templates.py b/docengine/app/api/v1/templates.py index 4a1b410..d67f4b3 100644 --- a/docengine/app/api/v1/templates.py +++ b/docengine/app/api/v1/templates.py @@ -10,7 +10,13 @@ from app.core.database import get_db from app.core.dependencies import CurrentUser from app.core.logging_config import get_logger from app.repositories.document_repository import DocumentRepository, TemplateMatchRepository -from app.repositories.template_repository import TemplateRepository +from app.repositories.template_repository import ( + TemplateRepository, + DocumentRegionRepository, + DocumentCellRepository, + TableFormatRepository, + TableColumnRepository +) from app.schemas.common import PaginatedResponse, SuccessResponse from app.schemas.document import TemplateMatchRequest, TemplateMatchResponse from app.schemas.template import ( @@ -18,6 +24,8 @@ from app.schemas.template import ( TemplateRenderRequest, TemplateRenderResponse, TemplateResponse, + TemplateCreateRequest, + TemplateMappingSaveRequest, ) logger = get_logger(__name__) @@ -25,6 +33,132 @@ logger = get_logger(__name__) router = APIRouter(prefix="/templates", tags=["Templates"]) +@router.post( + "", + response_model=dict, + summary="Create Template", + description="Create a new template.", +) +def create_template( + payload: TemplateCreateRequest, + current_user: CurrentUser, + db: Session = Depends(get_db), +) -> dict: + """Create a template.""" + template_repo = TemplateRepository(db) + + template = template_repo.create_template( + name=payload.template_name, + page_width=1000.0, + page_height=1000.0, + source_document_id=uuid.UUID(payload.source_document_id) if payload.source_document_id else None + ) + cell_repo = DocumentCellRepository(db) + saved_fields = [] + + for index, field in enumerate(payload.fields): + cell = cell_repo.create_cell( + format_id=template.id, + page_number=1, + x=0.0, + y=0.0, + width=0.0, + height=0.0, + data_type=field.field_type, + field_name=field.field_label, + sequence=field.display_order or index, + is_dynamic=True + ) + saved_fields.append({ + "field_label": cell.field_name, + "field_type": cell.data_type + }) + + db.commit() + + return { + "pk_template_id": str(template.id), + "template_name": template.name, + "fields": saved_fields + } + + +@router.post( + "/{template_id}/mappings/save", + response_model=SuccessResponse, + summary="Save Template Mappings", + description="Save the field mappings for a template.", +) +def save_mappings( + template_id: uuid.UUID, + payload: list[TemplateMappingSaveRequest], + current_user: CurrentUser, + db: Session = Depends(get_db), +) -> SuccessResponse: + """Save template mappings.""" + template_repo = TemplateRepository(db) + template = template_repo.get_by_id(template_id) + if not template: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Template '{template_id}' not found", + ) + + region_repo = DocumentRegionRepository(db) + cell_repo = DocumentCellRepository(db) + table_repo = TableFormatRepository(db) + table_col_repo = TableColumnRepository(db) + + cells = cell_repo.get_template_cells(template_id) + cell_map = {c.field_name: c for c in cells} + table_format = None + + # Save each mapped node + for mapping in payload: + cell = cell_map.get(mapping.field_name) + is_table_column = cell and cell.data_type == 'TABLE_COLUMN' + + for node_idx, node in enumerate(mapping.mapped_nodes): + if is_table_column: + if not table_format: + table_format = table_repo.create_table_format( + format_id=template_id, + page_number=node.page_no or 1, + x=0.0, y=0.0, width=1000.0, height=1000.0, + rows=1, columns=10 + ) + table_col_repo.create_column( + table_format_id=table_format.id, + column_index=node_idx, + width=node.width, + header_text=mapping.field_name, + data_type="text" + ) + + region_repo.create_region( + format_id=template_id, + page_number=node.page_no or 1, + region_type="field_mapping", + x=node.x_coordinate, + y=node.y_coordinate, + width=node.width, + height=node.height, + content={ + "field_name": mapping.field_name, + "text_value": node.text_value + } + ) + + db.commit() + + # Generate fingerprint now that mappings are populated + from app.services.fingerprint_service import FingerprintService + FingerprintService(db).generate_fingerprint(template) + db.commit() + + return SuccessResponse(message="Mappings saved successfully") + + @router.get( "", response_model=PaginatedResponse[TemplateListResponse], @@ -32,9 +166,9 @@ router = APIRouter(prefix="/templates", tags=["Templates"]) description="List all active templates with pagination.", ) def list_templates( + current_user: CurrentUser, page: int = Query(default=1, ge=1), page_size: int = Query(default=20, ge=1, le=100), - current_user: CurrentUser = None, db: Session = Depends(get_db), ) -> PaginatedResponse[TemplateListResponse]: """List all active templates.""" @@ -61,7 +195,7 @@ def list_templates( ) def get_template( template_id: uuid.UUID, - current_user: CurrentUser = None, + current_user: CurrentUser, db: Session = Depends(get_db), ) -> TemplateResponse: """Get a template by ID.""" @@ -83,7 +217,7 @@ def get_template( ) def delete_template( template_id: uuid.UUID, - current_user: CurrentUser = None, + current_user: CurrentUser, db: Session = Depends(get_db), ) -> SuccessResponse: """Soft-delete a template.""" @@ -109,7 +243,7 @@ def delete_template( ) def match_template( payload: TemplateMatchRequest, - current_user: CurrentUser = None, + current_user: CurrentUser, db: Session = Depends(get_db), ) -> list[TemplateMatchResponse]: """Match a document against existing templates.""" @@ -162,7 +296,7 @@ def match_template( ) def render_template( payload: TemplateRenderRequest, - current_user: CurrentUser = None, + current_user: CurrentUser, db: Session = Depends(get_db), ) -> TemplateRenderResponse: """Render a template to PDF.""" @@ -199,8 +333,8 @@ def render_template( ) def download_rendered_pdf( template_id: uuid.UUID, + current_user: CurrentUser, filename: str = Query(..., description="Filename of the rendered PDF"), - current_user: CurrentUser = None, db: Session = Depends(get_db), ) -> FileResponse: """Download a rendered PDF.""" diff --git a/docengine/app/repositories/template_repository.py b/docengine/app/repositories/template_repository.py index 49baf11..b3d7d0f 100644 --- a/docengine/app/repositories/template_repository.py +++ b/docengine/app/repositories/template_repository.py @@ -70,6 +70,7 @@ class TemplateRepository(BaseRepository[DocumentFormat]): fingerprint: dict | None = None, source_document_id: uuid.UUID | None = None, created_by: uuid.UUID | None = None, + is_active: bool = True, ) -> DocumentFormat: """Create a new template.""" template = DocumentFormat( @@ -85,6 +86,7 @@ class TemplateRepository(BaseRepository[DocumentFormat]): fingerprint=fingerprint, source_document_id=source_document_id, created_by=created_by, + is_active=is_active, ) return self.create(template) diff --git a/docengine/app/schemas/template.py b/docengine/app/schemas/template.py index ebc850e..bf8285e 100644 --- a/docengine/app/schemas/template.py +++ b/docengine/app/schemas/template.py @@ -2,7 +2,7 @@ from __future__ import annotations import uuid from datetime import datetime -from typing import Any +from typing import Any, List, Optional, Union from pydantic import Field @@ -248,3 +248,30 @@ class TemplateRenderResponse(BaseSchema): file_size: int page_count: int rendered_at: datetime + + +class TemplateFieldCreate(BaseSchema): + field_label: str + field_type: str = "text" + display_order: int = 0 + required_flag: bool = False + +class TemplateCreateRequest(BaseSchema): + template_name: str + source_document_id: Optional[str] = None + fields: List[TemplateFieldCreate] = Field(default_factory=list) + +class MappingNodeRequest(BaseSchema): + pk_document_data_id: Union[int, str, None] = None + x_coordinate: float + y_coordinate: float + width: float + height: float + text_value: Optional[str] = None + page_width: Optional[float] = None + page_height: Optional[float] = None + page_no: Optional[int] = None + +class TemplateMappingSaveRequest(BaseSchema): + field_name: str + mapped_nodes: List[MappingNodeRequest] = Field(default_factory=list) diff --git a/docengine/app/services/document_service.py b/docengine/app/services/document_service.py index 60f1998..c8a21e5 100644 --- a/docengine/app/services/document_service.py +++ b/docengine/app/services/document_service.py @@ -58,11 +58,7 @@ class DocumentProcessingService: metadata["layout"] = layout_results document.document_metadata = metadata - # Step 3: Generate template - self.db.refresh(document) - template = self.template_service.generate_template(document) - - # Step 4: Update document status + # Step 3: Update document status self.doc_repo.update_status(document_id, "completed") self.db.commit() @@ -70,7 +66,6 @@ class DocumentProcessingService: "processing_completed", document_id=str(document_id), pages=document.page_count, - template_id=str(template.id), ) return document diff --git a/docengine/app/services/fingerprint_service.py b/docengine/app/services/fingerprint_service.py index 12d48be..2d43681 100644 --- a/docengine/app/services/fingerprint_service.py +++ b/docengine/app/services/fingerprint_service.py @@ -187,7 +187,7 @@ class FingerprintService: serialized = json.dumps(normalized, sort_keys=True, default=str) return hashlib.sha256(serialized.encode()).hexdigest() - def _normalize_coordinates(self, data: dict[str, Any]) -> dict[str, Any]: + def _normalize_coordinates(self, data: Any) -> Any: """Normalize coordinates by rounding to reduce sensitivity to small variations.""" if isinstance(data, dict): return {k: self._normalize_coordinates(v) for k, v in data.items()} diff --git a/docengine/app/services/template_service.py b/docengine/app/services/template_service.py index 2cf2e3f..6bf81cd 100644 --- a/docengine/app/services/template_service.py +++ b/docengine/app/services/template_service.py @@ -80,6 +80,7 @@ class TemplateService: description=f"Auto-generated template from {document.original_filename}", source_document_id=document.id, created_by=user_id, + is_active=False, ) # Process each page diff --git a/frontend/src/app/services/template.service.ts b/frontend/src/app/services/template.service.ts index a3bec0c..c00e63b 100644 --- a/frontend/src/app/services/template.service.ts +++ b/frontend/src/app/services/template.service.ts @@ -81,15 +81,19 @@ export class TemplateService { ); } - createTemplate(data: { template_name: string, fields: TemplateField[] }): Observable