Compare commits
10 Commits
template_e
...
fix_templa
| Author | SHA1 | Date | |
|---|---|---|---|
| cd29cfbbc3 | |||
| 85614c922d | |||
| b812d3cb82 | |||
| c725014788 | |||
| 360022820d | |||
| 460a1c5c51 | |||
| fb0a78a405 | |||
| 6fd90ef7d4 | |||
| ec16c50e17 | |||
| f28035487f |
16
check_matches.py
Normal file
16
check_matches.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.append(os.path.join(os.getcwd(), 'docengine'))
|
||||||
|
|
||||||
|
from app.core.database import SessionLocal
|
||||||
|
from app.models.document import TemplateMatch
|
||||||
|
from app.models.template import DocumentFormat
|
||||||
|
|
||||||
|
db = SessionLocal()
|
||||||
|
matches = db.query(TemplateMatch).order_by(TemplateMatch.created_at.desc()).limit(10).all()
|
||||||
|
print(f"Found {len(matches)} matches.")
|
||||||
|
for m in matches:
|
||||||
|
fmt = db.query(DocumentFormat).filter(DocumentFormat.id == m.format_id).first()
|
||||||
|
fmt_name = fmt.name if fmt else 'Unknown'
|
||||||
|
print(f"Match: doc_id={m.document_id}, format_id={m.format_id}, name={fmt_name}, score={m.confidence_score}")
|
||||||
|
print(f"Details: {m.match_details}")
|
||||||
26
debug_match.py
Normal file
26
debug_match.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.append(os.path.join(os.getcwd(), 'docengine'))
|
||||||
|
|
||||||
|
from app.core.database import SessionLocal
|
||||||
|
from app.models.document import Document
|
||||||
|
from app.services.matching_service import MatchingService
|
||||||
|
|
||||||
|
db = SessionLocal()
|
||||||
|
doc = db.query(Document).order_by(Document.created_at.desc()).first()
|
||||||
|
if not doc:
|
||||||
|
print("No documents found.")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
print(f"Latest document: {doc.id} (status: {doc.status})")
|
||||||
|
|
||||||
|
svc = MatchingService(db)
|
||||||
|
try:
|
||||||
|
# Match with min_confidence=0.0 so it returns EVERYTHING
|
||||||
|
matches = svc.match_document(doc.id, min_confidence=0.0)
|
||||||
|
print(f"Returned {len(matches)} matches.")
|
||||||
|
for m in matches:
|
||||||
|
print(f"Match: format_id={m.format_id}, score={m.confidence_score}")
|
||||||
|
print(f"Details: {m.match_details}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error matching: {e}")
|
||||||
2
docengine/.gitignore
vendored
2
docengine/.gitignore
vendored
@@ -58,7 +58,7 @@ ENV/
|
|||||||
*~
|
*~
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
storage/
|
/storage/
|
||||||
*.pid
|
*.pid
|
||||||
celerybeat-schedule
|
celerybeat-schedule
|
||||||
celerybeat.pid
|
celerybeat.pid
|
||||||
|
|||||||
@@ -235,6 +235,45 @@ def get_document_template_matches(
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/{document_id}/extraction",
|
||||||
|
response_model=dict,
|
||||||
|
summary="Extract Document Data",
|
||||||
|
description="Match a document against templates and extract key-value & table data.",
|
||||||
|
)
|
||||||
|
def extract_document_data(
|
||||||
|
document_id: uuid.UUID,
|
||||||
|
current_user: CurrentUser = None,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
) -> dict:
|
||||||
|
"""Extract document data using matched template mappings."""
|
||||||
|
doc_repo = DocumentRepository(db)
|
||||||
|
document = doc_repo.get_by_id(document_id)
|
||||||
|
if not document:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Document '{document_id}' not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
if document.status != "completed":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=f"Document must be in 'completed' status. Current status: '{document.status}'",
|
||||||
|
)
|
||||||
|
|
||||||
|
from app.services.extraction_service import ExtractionService
|
||||||
|
extraction_service = ExtractionService(db)
|
||||||
|
try:
|
||||||
|
result = extraction_service.extract_document_data(document_id)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("extraction_endpoint_failed", document_id=str(document_id), error=str(e))
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"Extraction failed: {str(e)}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
"/{document_id}",
|
"/{document_id}",
|
||||||
response_model=SuccessResponse,
|
response_model=SuccessResponse,
|
||||||
|
|||||||
@@ -10,7 +10,13 @@ from app.core.database import get_db
|
|||||||
from app.core.dependencies import CurrentUser
|
from app.core.dependencies import CurrentUser
|
||||||
from app.core.logging_config import get_logger
|
from app.core.logging_config import get_logger
|
||||||
from app.repositories.document_repository import DocumentRepository, TemplateMatchRepository
|
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.common import PaginatedResponse, SuccessResponse
|
||||||
from app.schemas.document import TemplateMatchRequest, TemplateMatchResponse
|
from app.schemas.document import TemplateMatchRequest, TemplateMatchResponse
|
||||||
from app.schemas.template import (
|
from app.schemas.template import (
|
||||||
@@ -18,6 +24,8 @@ from app.schemas.template import (
|
|||||||
TemplateRenderRequest,
|
TemplateRenderRequest,
|
||||||
TemplateRenderResponse,
|
TemplateRenderResponse,
|
||||||
TemplateResponse,
|
TemplateResponse,
|
||||||
|
TemplateCreateRequest,
|
||||||
|
TemplateMappingSaveRequest,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
@@ -25,6 +33,198 @@ logger = get_logger(__name__)
|
|||||||
router = APIRouter(prefix="/templates", tags=["Templates"])
|
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.put(
|
||||||
|
"/{template_id}",
|
||||||
|
response_model=dict,
|
||||||
|
summary="Update Template",
|
||||||
|
description="Update an existing template name and its fields.",
|
||||||
|
)
|
||||||
|
def update_template(
|
||||||
|
template_id: uuid.UUID,
|
||||||
|
payload: TemplateCreateRequest,
|
||||||
|
current_user: CurrentUser,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
) -> dict:
|
||||||
|
"""Update a template."""
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
|
||||||
|
if template.name != payload.template_name:
|
||||||
|
template.name = payload.template_name
|
||||||
|
|
||||||
|
from app.models.template import DocumentCell
|
||||||
|
db.query(DocumentCell).filter(DocumentCell.format_id == template_id).delete()
|
||||||
|
|
||||||
|
saved_fields = []
|
||||||
|
cell_repo = DocumentCellRepository(db)
|
||||||
|
for index, field in enumerate(payload.fields):
|
||||||
|
cell = cell_repo.create_cell(
|
||||||
|
format_id=template.id,
|
||||||
|
page_number=1,
|
||||||
|
row_no=0,
|
||||||
|
column_no=0,
|
||||||
|
field_name=field.field_label,
|
||||||
|
data_type=field.field_type,
|
||||||
|
x=0.0,
|
||||||
|
y=0.0,
|
||||||
|
width=0.0,
|
||||||
|
height=0.0,
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Delete existing field mappings and table formats for this template
|
||||||
|
from app.models.template import DocumentRegion, TableFormat
|
||||||
|
db.query(DocumentRegion).filter(
|
||||||
|
DocumentRegion.format_id == template_id,
|
||||||
|
DocumentRegion.region_type == "field_mapping"
|
||||||
|
).delete()
|
||||||
|
db.query(TableFormat).filter(TableFormat.format_id == template_id).delete()
|
||||||
|
db.flush()
|
||||||
|
|
||||||
|
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(
|
@router.get(
|
||||||
"",
|
"",
|
||||||
response_model=PaginatedResponse[TemplateListResponse],
|
response_model=PaginatedResponse[TemplateListResponse],
|
||||||
@@ -32,9 +232,9 @@ router = APIRouter(prefix="/templates", tags=["Templates"])
|
|||||||
description="List all active templates with pagination.",
|
description="List all active templates with pagination.",
|
||||||
)
|
)
|
||||||
def list_templates(
|
def list_templates(
|
||||||
|
current_user: CurrentUser,
|
||||||
page: int = Query(default=1, ge=1),
|
page: int = Query(default=1, ge=1),
|
||||||
page_size: int = Query(default=20, ge=1, le=100),
|
page_size: int = Query(default=20, ge=1, le=100),
|
||||||
current_user: CurrentUser = None,
|
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
) -> PaginatedResponse[TemplateListResponse]:
|
) -> PaginatedResponse[TemplateListResponse]:
|
||||||
"""List all active templates."""
|
"""List all active templates."""
|
||||||
@@ -61,7 +261,7 @@ def list_templates(
|
|||||||
)
|
)
|
||||||
def get_template(
|
def get_template(
|
||||||
template_id: uuid.UUID,
|
template_id: uuid.UUID,
|
||||||
current_user: CurrentUser = None,
|
current_user: CurrentUser,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
) -> TemplateResponse:
|
) -> TemplateResponse:
|
||||||
"""Get a template by ID."""
|
"""Get a template by ID."""
|
||||||
@@ -83,7 +283,7 @@ def get_template(
|
|||||||
)
|
)
|
||||||
def delete_template(
|
def delete_template(
|
||||||
template_id: uuid.UUID,
|
template_id: uuid.UUID,
|
||||||
current_user: CurrentUser = None,
|
current_user: CurrentUser,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
) -> SuccessResponse:
|
) -> SuccessResponse:
|
||||||
"""Soft-delete a template."""
|
"""Soft-delete a template."""
|
||||||
@@ -109,7 +309,7 @@ def delete_template(
|
|||||||
)
|
)
|
||||||
def match_template(
|
def match_template(
|
||||||
payload: TemplateMatchRequest,
|
payload: TemplateMatchRequest,
|
||||||
current_user: CurrentUser = None,
|
current_user: CurrentUser,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
) -> list[TemplateMatchResponse]:
|
) -> list[TemplateMatchResponse]:
|
||||||
"""Match a document against existing templates."""
|
"""Match a document against existing templates."""
|
||||||
@@ -162,7 +362,7 @@ def match_template(
|
|||||||
)
|
)
|
||||||
def render_template(
|
def render_template(
|
||||||
payload: TemplateRenderRequest,
|
payload: TemplateRenderRequest,
|
||||||
current_user: CurrentUser = None,
|
current_user: CurrentUser,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
) -> TemplateRenderResponse:
|
) -> TemplateRenderResponse:
|
||||||
"""Render a template to PDF."""
|
"""Render a template to PDF."""
|
||||||
@@ -199,8 +399,8 @@ def render_template(
|
|||||||
)
|
)
|
||||||
def download_rendered_pdf(
|
def download_rendered_pdf(
|
||||||
template_id: uuid.UUID,
|
template_id: uuid.UUID,
|
||||||
|
current_user: CurrentUser,
|
||||||
filename: str = Query(..., description="Filename of the rendered PDF"),
|
filename: str = Query(..., description="Filename of the rendered PDF"),
|
||||||
current_user: CurrentUser = None,
|
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
) -> FileResponse:
|
) -> FileResponse:
|
||||||
"""Download a rendered PDF."""
|
"""Download a rendered PDF."""
|
||||||
|
|||||||
@@ -83,13 +83,20 @@ class Settings(BaseSettings):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def parse_cors_origins(cls, v: Any) -> list[str]:
|
def parse_cors_origins(cls, v: Any) -> list[str]:
|
||||||
if isinstance(v, str):
|
if isinstance(v, str):
|
||||||
|
if not v.strip():
|
||||||
|
return []
|
||||||
try:
|
try:
|
||||||
parsed = json.loads(v)
|
parsed = json.loads(v)
|
||||||
if isinstance(parsed, list):
|
if isinstance(parsed, list):
|
||||||
return parsed
|
return [str(item).strip() for item in parsed]
|
||||||
|
elif isinstance(parsed, str):
|
||||||
|
return [parsed.strip()]
|
||||||
except (json.JSONDecodeError, TypeError):
|
except (json.JSONDecodeError, TypeError):
|
||||||
return [origin.strip() for origin in v.split(",") if origin.strip()]
|
pass
|
||||||
return v
|
return [origin.strip() for origin in v.split(",") if origin.strip()]
|
||||||
|
if isinstance(v, list):
|
||||||
|
return [str(item).strip() for item in v]
|
||||||
|
return []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def database_url(self) -> str:
|
def database_url(self) -> str:
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ class TemplateRepository(BaseRepository[DocumentFormat]):
|
|||||||
fingerprint: dict | None = None,
|
fingerprint: dict | None = None,
|
||||||
source_document_id: uuid.UUID | None = None,
|
source_document_id: uuid.UUID | None = None,
|
||||||
created_by: uuid.UUID | None = None,
|
created_by: uuid.UUID | None = None,
|
||||||
|
is_active: bool = True,
|
||||||
) -> DocumentFormat:
|
) -> DocumentFormat:
|
||||||
"""Create a new template."""
|
"""Create a new template."""
|
||||||
template = DocumentFormat(
|
template = DocumentFormat(
|
||||||
@@ -85,6 +86,7 @@ class TemplateRepository(BaseRepository[DocumentFormat]):
|
|||||||
fingerprint=fingerprint,
|
fingerprint=fingerprint,
|
||||||
source_document_id=source_document_id,
|
source_document_id=source_document_id,
|
||||||
created_by=created_by,
|
created_by=created_by,
|
||||||
|
is_active=is_active,
|
||||||
)
|
)
|
||||||
return self.create(template)
|
return self.create(template)
|
||||||
|
|
||||||
|
|||||||
@@ -130,5 +130,5 @@ class TemplateMatchRequest(BaseSchema):
|
|||||||
"""Request to match a document against templates."""
|
"""Request to match a document against templates."""
|
||||||
|
|
||||||
document_id: uuid.UUID
|
document_id: uuid.UUID
|
||||||
min_confidence: float = Field(default=0.5, ge=0.0, le=1.0)
|
min_confidence: float = Field(default=0.75, ge=0.0, le=1.0)
|
||||||
max_results: int = Field(default=5, ge=1, le=20)
|
max_results: int = Field(default=5, ge=1, le=20)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any, List, Optional, Union
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
|
|
||||||
@@ -248,3 +248,30 @@ class TemplateRenderResponse(BaseSchema):
|
|||||||
file_size: int
|
file_size: int
|
||||||
page_count: int
|
page_count: int
|
||||||
rendered_at: datetime
|
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)
|
||||||
|
|||||||
@@ -54,13 +54,11 @@ class DocumentProcessingService:
|
|||||||
|
|
||||||
# Step 2: Analyze layout
|
# Step 2: Analyze layout
|
||||||
layout_results = self.layout_service.analyze_document_layout(document)
|
layout_results = self.layout_service.analyze_document_layout(document)
|
||||||
document.document_metadata = document.document_metadata or {}
|
metadata = document.document_metadata or {}
|
||||||
document.document_metadata["layout"] = layout_results
|
metadata["layout"] = layout_results
|
||||||
|
document.document_metadata = metadata
|
||||||
|
|
||||||
# Step 3: Generate template
|
# Step 3: Update document status
|
||||||
template = self.template_service.generate_template(document)
|
|
||||||
|
|
||||||
# Step 4: Update document status
|
|
||||||
self.doc_repo.update_status(document_id, "completed")
|
self.doc_repo.update_status(document_id, "completed")
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
|
|
||||||
@@ -68,7 +66,6 @@ class DocumentProcessingService:
|
|||||||
"processing_completed",
|
"processing_completed",
|
||||||
document_id=str(document_id),
|
document_id=str(document_id),
|
||||||
pages=document.page_count,
|
pages=document.page_count,
|
||||||
template_id=str(template.id),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return document
|
return document
|
||||||
|
|||||||
295
docengine/app/services/extraction_service.py
Normal file
295
docengine/app/services/extraction_service.py
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.core.logging_config import get_logger
|
||||||
|
from app.repositories.document_repository import DocumentRepository
|
||||||
|
from app.repositories.template_repository import TemplateRepository
|
||||||
|
from app.services.matching_service import MatchingService
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ExtractionService:
|
||||||
|
"""Extract document values using matched template coordinates and structures."""
|
||||||
|
|
||||||
|
def __init__(self, db: Session) -> None:
|
||||||
|
self.db = db
|
||||||
|
self.matching_service = MatchingService(db)
|
||||||
|
self.doc_repo = DocumentRepository(db)
|
||||||
|
self.template_repo = TemplateRepository(db)
|
||||||
|
|
||||||
|
def extract_document_data(self, document_id: uuid.UUID) -> dict[str, Any]:
|
||||||
|
"""Perform template matching on the document and extract structured field values."""
|
||||||
|
document = self.doc_repo.get_with_pages(document_id)
|
||||||
|
if not document:
|
||||||
|
raise ValueError(f"Document '{document_id}' not found")
|
||||||
|
|
||||||
|
# 1. Match document against existing templates
|
||||||
|
matches = self.matching_service.match_document(document_id, min_confidence=0.75)
|
||||||
|
if not matches:
|
||||||
|
logger.info("extraction_failed_no_match", document_id=str(document_id))
|
||||||
|
return {
|
||||||
|
"template_matched": False,
|
||||||
|
"template_id": None,
|
||||||
|
"template_name": None,
|
||||||
|
"confidence_score": 0.0,
|
||||||
|
"extracted_data": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
best_match = matches[0]
|
||||||
|
template = self.template_repo.get_by_id(best_match.format_id)
|
||||||
|
if not template:
|
||||||
|
raise ValueError(f"Matched template '{best_match.format_id}' not found")
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"extraction_matched_template",
|
||||||
|
document_id=str(document_id),
|
||||||
|
template_id=str(template.id),
|
||||||
|
template_name=template.name,
|
||||||
|
score=best_match.confidence_score,
|
||||||
|
)
|
||||||
|
|
||||||
|
extracted_data: dict[str, Any] = {}
|
||||||
|
|
||||||
|
# Load all mapped regions for the template
|
||||||
|
regions = [r for r in template.regions if r.region_type == "field_mapping"]
|
||||||
|
regions_by_field: dict[str, list[Any]] = {}
|
||||||
|
for r in regions:
|
||||||
|
field_name = r.content.get("field_name") if r.content else None
|
||||||
|
if field_name:
|
||||||
|
regions_by_field.setdefault(field_name, []).append(r)
|
||||||
|
|
||||||
|
# 2. Divide fields into Scalar vs Table Column types
|
||||||
|
scalar_cells = [cell for cell in template.cells if cell.data_type != "TABLE_COLUMN"]
|
||||||
|
table_column_cells = [cell for cell in template.cells if cell.data_type == "TABLE_COLUMN"]
|
||||||
|
|
||||||
|
# 3. Extract Scalar Fields
|
||||||
|
for cell in scalar_cells:
|
||||||
|
field_name = cell.field_name
|
||||||
|
if not field_name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
field_regions = regions_by_field.get(field_name, [])
|
||||||
|
if not field_regions:
|
||||||
|
extracted_data[field_name] = ""
|
||||||
|
continue
|
||||||
|
|
||||||
|
extracted_values = []
|
||||||
|
extracted_block_ids = set()
|
||||||
|
sorted_regions = sorted(field_regions, key=lambda r: (r.page_number, r.sequence, r.y, r.x))
|
||||||
|
|
||||||
|
for region in sorted_regions:
|
||||||
|
page = next((p for p in document.pages if p.page_number == region.page_number), None)
|
||||||
|
if not page:
|
||||||
|
continue
|
||||||
|
best_block = self._find_best_overlapping_block(page, region)
|
||||||
|
if best_block and best_block.id not in extracted_block_ids:
|
||||||
|
extracted_block_ids.add(best_block.id)
|
||||||
|
val = best_block.text.strip()
|
||||||
|
if val:
|
||||||
|
extracted_values.append(val)
|
||||||
|
|
||||||
|
extracted_data[field_name] = " ".join(extracted_values)
|
||||||
|
|
||||||
|
# 4. Extract Table Column Fields
|
||||||
|
if table_column_cells:
|
||||||
|
table_column_names = {cell.field_name for cell in table_column_cells if cell.field_name}
|
||||||
|
table_regions = [
|
||||||
|
r for r in regions
|
||||||
|
if r.content and r.content.get("field_name") in table_column_names
|
||||||
|
]
|
||||||
|
|
||||||
|
if table_regions:
|
||||||
|
# Determine vertical boundaries of the table area
|
||||||
|
table_start_y = min((r.y for r in table_regions), default=0.0)
|
||||||
|
table_start_y = max(0.0, table_start_y - 10.0) # subtract buffer
|
||||||
|
|
||||||
|
# Process page where the table coordinates are mapped
|
||||||
|
page_number = min((r.page_number for r in table_regions), default=1)
|
||||||
|
page = next((p for p in document.pages if p.page_number == page_number), None)
|
||||||
|
|
||||||
|
# Determine table vertical end Y by finding any summary scalar fields below the table
|
||||||
|
summary_keywords = {"tax", "total", "shipping", "discount", "vat", "handling", "duty", "subtotal", "grand"}
|
||||||
|
summary_regions = []
|
||||||
|
for col_name, regs in regions_by_field.items():
|
||||||
|
if col_name in table_column_names:
|
||||||
|
continue
|
||||||
|
for r in regs:
|
||||||
|
if r.y > table_start_y and any(kw in col_name.lower() for kw in summary_keywords):
|
||||||
|
# Skip left-aligned metadata fields (like Shipping Method)
|
||||||
|
if page and r.x < page.width * 0.4:
|
||||||
|
continue
|
||||||
|
summary_regions.append(r)
|
||||||
|
|
||||||
|
table_end_y = min((r.y for r in summary_regions), default=99999.0)
|
||||||
|
|
||||||
|
# Check if template has a footer to define the end vertical boundary
|
||||||
|
footer_regions = [r for r in template.regions if r.region_type == "footer"]
|
||||||
|
if footer_regions:
|
||||||
|
table_end_y = min(table_end_y, min(r.y for r in footer_regions))
|
||||||
|
|
||||||
|
# Determine horizontal ranges (X span) for each column in the template
|
||||||
|
col_x_spans: dict[str, tuple[float, float]] = {}
|
||||||
|
for col_name in table_column_names:
|
||||||
|
col_regs = [r for r in table_regions if r.content and r.content.get("field_name") == col_name]
|
||||||
|
if col_regs:
|
||||||
|
x_min = min(r.x for r in col_regs)
|
||||||
|
x_max = max(r.x + r.width for r in col_regs)
|
||||||
|
# Add a 15px margin to accommodate layout differences
|
||||||
|
col_x_spans[col_name] = (max(0.0, x_min - 15.0), x_max + 15.0)
|
||||||
|
else:
|
||||||
|
col_x_spans[col_name] = (0.0, 0.0)
|
||||||
|
|
||||||
|
if page:
|
||||||
|
# Check for summary keyword text blocks (e.g. Total, Tax) as vertical boundary fallback
|
||||||
|
summary_labels = {"subtotal", "total", "grand total", "tax", "shipping & handling", "discount"}
|
||||||
|
for block in page.text_blocks:
|
||||||
|
if block.y > table_start_y:
|
||||||
|
# Skip left-aligned text blocks
|
||||||
|
if block.x < page.width * 0.4:
|
||||||
|
continue
|
||||||
|
block_text_clean = block.text.strip().lower()
|
||||||
|
if any(label in block_text_clean for label in summary_labels):
|
||||||
|
table_end_y = min(table_end_y, block.y)
|
||||||
|
|
||||||
|
# Collect all document blocks inside Y range
|
||||||
|
candidate_blocks = [
|
||||||
|
b for b in page.text_blocks
|
||||||
|
if b.y >= table_start_y and b.y < table_end_y
|
||||||
|
]
|
||||||
|
# Sort blocks by Y coordinate
|
||||||
|
sorted_blocks = sorted(candidate_blocks, key=lambda b: b.y)
|
||||||
|
|
||||||
|
# Group blocks into rows by vertical center alignment (15px threshold)
|
||||||
|
rows: list[list[Any]] = []
|
||||||
|
current_row: list[Any] = []
|
||||||
|
current_y_center = None
|
||||||
|
|
||||||
|
for block in sorted_blocks:
|
||||||
|
if not block.text.strip():
|
||||||
|
continue
|
||||||
|
block_y_center = block.y + block.height / 2
|
||||||
|
if current_y_center is None:
|
||||||
|
current_row.append(block)
|
||||||
|
current_y_center = block_y_center
|
||||||
|
elif abs(block_y_center - current_y_center) < 15.0:
|
||||||
|
current_row.append(block)
|
||||||
|
else:
|
||||||
|
rows.append(current_row)
|
||||||
|
current_row = [block]
|
||||||
|
current_y_center = block_y_center
|
||||||
|
if current_row:
|
||||||
|
rows.append(current_row)
|
||||||
|
|
||||||
|
# Map candidate blocks in each row to columns
|
||||||
|
rows_data: list[dict[str, str]] = []
|
||||||
|
for row_blocks in rows:
|
||||||
|
row_dict: dict[str, list[Any]] = {name: [] for name in table_column_names}
|
||||||
|
for block in row_blocks:
|
||||||
|
best_col = None
|
||||||
|
best_overlap = 0.0
|
||||||
|
for col_name, (x_min, x_max) in col_x_spans.items():
|
||||||
|
overlap = max(
|
||||||
|
0.0,
|
||||||
|
min(x_max, block.x + block.width) - max(x_min, block.x)
|
||||||
|
)
|
||||||
|
overlap_ratio = overlap / block.width if block.width > 0 else 0.0
|
||||||
|
if overlap_ratio > 0.2 and overlap_ratio > best_overlap:
|
||||||
|
best_overlap = overlap_ratio
|
||||||
|
best_col = col_name
|
||||||
|
if best_col:
|
||||||
|
row_dict[best_col].append(block)
|
||||||
|
|
||||||
|
# Construct row values
|
||||||
|
row_values: dict[str, str] = {}
|
||||||
|
for col_name, blocks in row_dict.items():
|
||||||
|
sorted_blocks_in_col = sorted(blocks, key=lambda b: b.x)
|
||||||
|
row_values[col_name] = " ".join(
|
||||||
|
b.text.strip() for b in sorted_blocks_in_col
|
||||||
|
)
|
||||||
|
|
||||||
|
# Filter out table header rows
|
||||||
|
is_header = False
|
||||||
|
for col_name, val in row_values.items():
|
||||||
|
val_lower = val.lower()
|
||||||
|
if (
|
||||||
|
val_lower == col_name.lower() or
|
||||||
|
val_lower in [
|
||||||
|
"item", "items", "qty", "quantity", "price",
|
||||||
|
"amount", "total", "subtotal", "description"
|
||||||
|
]
|
||||||
|
):
|
||||||
|
is_header = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# Append if not header and at least one cell has a value
|
||||||
|
if not is_header and any(row_values.values()):
|
||||||
|
rows_data.append(row_values)
|
||||||
|
|
||||||
|
# Merge continuation lines (descriptions spanning multiple rows with empty sibling columns)
|
||||||
|
merged_rows: list[dict[str, str]] = []
|
||||||
|
for row in rows_data:
|
||||||
|
non_empty_cols = [k for k, v in row.items() if v.strip()]
|
||||||
|
if len(merged_rows) > 0 and len(non_empty_cols) == 1:
|
||||||
|
col_name = non_empty_cols[0]
|
||||||
|
last_row = merged_rows[-1]
|
||||||
|
if last_row.get(col_name):
|
||||||
|
last_row[col_name] = last_row[col_name] + " " + row[col_name]
|
||||||
|
else:
|
||||||
|
last_row[col_name] = row[col_name]
|
||||||
|
else:
|
||||||
|
merged_rows.append(row.copy())
|
||||||
|
|
||||||
|
rows_data = merged_rows
|
||||||
|
|
||||||
|
# Pivot list of rows into parallel arrays under column names
|
||||||
|
for col_name in table_column_names:
|
||||||
|
extracted_data[col_name] = []
|
||||||
|
for row in rows_data:
|
||||||
|
for col_name in table_column_names:
|
||||||
|
extracted_data[col_name].append(row.get(col_name, ""))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"template_matched": True,
|
||||||
|
"template_id": str(template.id),
|
||||||
|
"template_name": template.name,
|
||||||
|
"confidence_score": best_match.confidence_score,
|
||||||
|
"extracted_data": extracted_data,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _find_best_overlapping_block(self, page: Any, region: Any) -> Any | None:
|
||||||
|
"""Find the document text block overlapping most with the template region."""
|
||||||
|
best_block = None
|
||||||
|
best_overlap = 0.0
|
||||||
|
for block in page.text_blocks:
|
||||||
|
x_overlap = max(
|
||||||
|
0.0,
|
||||||
|
min(region.x + region.width, block.x + block.width) - max(region.x, block.x)
|
||||||
|
)
|
||||||
|
y_overlap = max(
|
||||||
|
0.0,
|
||||||
|
min(region.y + region.height, block.y + block.height) - max(region.y, block.y)
|
||||||
|
)
|
||||||
|
overlap = x_overlap * y_overlap
|
||||||
|
if overlap > best_overlap:
|
||||||
|
best_overlap = overlap
|
||||||
|
best_block = block
|
||||||
|
|
||||||
|
# Fallback: if no overlapping block, find the closest center-to-center
|
||||||
|
if not best_block:
|
||||||
|
min_dist = 100.0 # max 100px center-to-center distance
|
||||||
|
for block in page.text_blocks:
|
||||||
|
c_rx = region.x + region.width / 2
|
||||||
|
c_ry = region.y + region.height / 2
|
||||||
|
c_bx = block.x + block.width / 2
|
||||||
|
c_by = block.y + block.height / 2
|
||||||
|
dist = ((c_rx - c_bx) ** 2 + (c_ry - c_by) ** 2) ** 0.5
|
||||||
|
if dist < min_dist:
|
||||||
|
min_dist = dist
|
||||||
|
best_block = block
|
||||||
|
|
||||||
|
return best_block
|
||||||
@@ -145,6 +145,11 @@ class FingerprintService:
|
|||||||
if not template.table_formats:
|
if not template.table_formats:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Filter out dummy tables created by manual mappings (width=1000, height=1000)
|
||||||
|
valid_tables = [tf for tf in template.table_formats if tf.width != 1000.0 and tf.height != 1000.0]
|
||||||
|
if not valid_tables:
|
||||||
|
return None
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
@@ -156,15 +161,36 @@ class FingerprintService:
|
|||||||
"rows": tf.rows,
|
"rows": tf.rows,
|
||||||
"columns": tf.columns,
|
"columns": tf.columns,
|
||||||
}
|
}
|
||||||
for tf in template.table_formats
|
for tf in valid_tables
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
def _extract_cell_coordinates(self, template: DocumentFormat) -> dict[str, Any] | None:
|
def _extract_cell_coordinates(self, template: DocumentFormat) -> dict[str, Any] | None:
|
||||||
"""Extract cell coordinates from template."""
|
"""Extract cell coordinates from template."""
|
||||||
|
# For manually mapped templates, prefer the actual mapped regions
|
||||||
|
field_regions = [r for r in template.regions if r.region_type == "field_mapping"]
|
||||||
|
if field_regions:
|
||||||
|
return {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"page": r.page_number,
|
||||||
|
"x": r.x,
|
||||||
|
"y": r.y,
|
||||||
|
"width": r.width,
|
||||||
|
"height": r.height,
|
||||||
|
}
|
||||||
|
for r in field_regions
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Otherwise fallback to template cells, but skip if they are all 0,0 (unmapped placeholders)
|
||||||
if not template.cells:
|
if not template.cells:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
has_real_coords = any(c.width > 0 or c.height > 0 for c in template.cells)
|
||||||
|
if not has_real_coords:
|
||||||
|
return None
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
@@ -187,7 +213,7 @@ class FingerprintService:
|
|||||||
serialized = json.dumps(normalized, sort_keys=True, default=str)
|
serialized = json.dumps(normalized, sort_keys=True, default=str)
|
||||||
return hashlib.sha256(serialized.encode()).hexdigest()
|
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."""
|
"""Normalize coordinates by rounding to reduce sensitivity to small variations."""
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
return {k: self._normalize_coordinates(v) for k, v in data.items()}
|
return {k: self._normalize_coordinates(v) for k, v in data.items()}
|
||||||
@@ -215,44 +241,49 @@ class FingerprintService:
|
|||||||
weights.append(3.0)
|
weights.append(3.0)
|
||||||
|
|
||||||
# Logo coordinates similarity
|
# Logo coordinates similarity
|
||||||
logo_score = self._compare_coordinates(
|
if fingerprint1.logo_coordinates and fingerprint1.logo_coordinates.get("items"):
|
||||||
fingerprint1.logo_coordinates,
|
logo_score = self._compare_coordinates(
|
||||||
fingerprint2_data.get("logo_coordinates"),
|
fingerprint1.logo_coordinates,
|
||||||
)
|
fingerprint2_data.get("logo_coordinates"),
|
||||||
scores.append(logo_score)
|
)
|
||||||
weights.append(2.0)
|
scores.append(logo_score)
|
||||||
|
weights.append(2.0)
|
||||||
|
|
||||||
# Header coordinates similarity
|
# Header coordinates similarity
|
||||||
header_score = self._compare_coordinates(
|
if fingerprint1.header_coordinates and fingerprint1.header_coordinates.get("items"):
|
||||||
fingerprint1.header_coordinates,
|
header_score = self._compare_coordinates(
|
||||||
fingerprint2_data.get("header_coordinates"),
|
fingerprint1.header_coordinates,
|
||||||
)
|
fingerprint2_data.get("header_coordinates"),
|
||||||
scores.append(header_score)
|
)
|
||||||
weights.append(2.0)
|
scores.append(header_score)
|
||||||
|
weights.append(2.0)
|
||||||
|
|
||||||
# Footer coordinates similarity
|
# Footer coordinates similarity
|
||||||
footer_score = self._compare_coordinates(
|
if fingerprint1.footer_coordinates and fingerprint1.footer_coordinates.get("items"):
|
||||||
fingerprint1.footer_coordinates,
|
footer_score = self._compare_coordinates(
|
||||||
fingerprint2_data.get("footer_coordinates"),
|
fingerprint1.footer_coordinates,
|
||||||
)
|
fingerprint2_data.get("footer_coordinates"),
|
||||||
scores.append(footer_score)
|
)
|
||||||
weights.append(1.5)
|
scores.append(footer_score)
|
||||||
|
weights.append(1.5)
|
||||||
|
|
||||||
# Table coordinates similarity
|
# Table coordinates similarity
|
||||||
table_score = self._compare_coordinates(
|
if fingerprint1.table_coordinates and fingerprint1.table_coordinates.get("items"):
|
||||||
fingerprint1.table_coordinates,
|
table_score = self._compare_coordinates(
|
||||||
fingerprint2_data.get("table_coordinates"),
|
fingerprint1.table_coordinates,
|
||||||
)
|
fingerprint2_data.get("table_coordinates"),
|
||||||
scores.append(table_score)
|
)
|
||||||
weights.append(2.5)
|
scores.append(table_score)
|
||||||
|
weights.append(2.0)
|
||||||
|
|
||||||
# Cell coordinates similarity
|
# Cell coordinates similarity
|
||||||
cell_score = self._compare_coordinates(
|
if fingerprint1.cell_coordinates and fingerprint1.cell_coordinates.get("items"):
|
||||||
fingerprint1.cell_coordinates,
|
cell_score = self._compare_coordinates(
|
||||||
fingerprint2_data.get("cell_coordinates"),
|
fingerprint1.cell_coordinates,
|
||||||
)
|
fingerprint2_data.get("cell_coordinates"),
|
||||||
scores.append(cell_score)
|
)
|
||||||
weights.append(1.5)
|
scores.append(cell_score)
|
||||||
|
weights.append(1.5)
|
||||||
|
|
||||||
# Weighted average
|
# Weighted average
|
||||||
total_weight = sum(weights)
|
total_weight = sum(weights)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class MatchingService:
|
|||||||
def match_document(
|
def match_document(
|
||||||
self,
|
self,
|
||||||
document_id: uuid.UUID,
|
document_id: uuid.UUID,
|
||||||
min_confidence: float = 0.5,
|
min_confidence: float = 0.75,
|
||||||
max_results: int = 5,
|
max_results: int = 5,
|
||||||
) -> list[TemplateMatch]:
|
) -> list[TemplateMatch]:
|
||||||
"""Match a document against all existing templates."""
|
"""Match a document against all existing templates."""
|
||||||
|
|||||||
@@ -107,49 +107,94 @@ class NativePDFService:
|
|||||||
|
|
||||||
def _extract_text_blocks(self, doc_page: DocumentPage, page: fitz.Page) -> None:
|
def _extract_text_blocks(self, doc_page: DocumentPage, page: fitz.Page) -> None:
|
||||||
"""Extract text blocks with positioning and font information."""
|
"""Extract text blocks with positioning and font information."""
|
||||||
blocks = page.get_text("dict", flags=fitz.TEXT_PRESERVE_WHITESPACE)["blocks"]
|
blocks = page.get_text("rawdict")["blocks"]
|
||||||
sequence = 0
|
sequence = 0
|
||||||
|
chunks = []
|
||||||
|
|
||||||
for block in blocks:
|
for block in blocks:
|
||||||
if block["type"] != 0: # Skip non-text blocks
|
if block["type"] != 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
block_text_parts = []
|
current_chunk = None
|
||||||
font_info = {"family": None, "size": None, "color": None, "style": None}
|
space_count = 0
|
||||||
|
|
||||||
for line in block.get("lines", []):
|
for line in block.get("lines", []):
|
||||||
for span in line.get("spans", []):
|
for span in line.get("spans", []):
|
||||||
text = span.get("text", "").strip()
|
font_size = span.get("size", 12.0)
|
||||||
if text:
|
|
||||||
block_text_parts.append(text)
|
|
||||||
# Capture font info from the first non-empty span
|
|
||||||
if font_info["family"] is None:
|
|
||||||
font_info["family"] = span.get("font", None)
|
|
||||||
font_info["size"] = span.get("size", None)
|
|
||||||
color_int = span.get("color", 0)
|
|
||||||
font_info["color"] = f"#{color_int:06x}" if isinstance(color_int, int) else None
|
|
||||||
flags = span.get("flags", 0)
|
|
||||||
styles = []
|
|
||||||
if flags & 1:
|
|
||||||
styles.append("superscript")
|
|
||||||
if flags & 2:
|
|
||||||
styles.append("italic")
|
|
||||||
if flags & 4:
|
|
||||||
styles.append("serif")
|
|
||||||
if flags & 8:
|
|
||||||
styles.append("monospace")
|
|
||||||
if flags & 16:
|
|
||||||
styles.append("bold")
|
|
||||||
font_info["style"] = ",".join(styles) if styles else "regular"
|
|
||||||
|
|
||||||
full_text = " ".join(block_text_parts)
|
# Ignore massive text (like diagonal watermarks)
|
||||||
if not full_text.strip():
|
if font_size > 60:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
space_threshold = font_size * 1.5
|
||||||
|
|
||||||
|
font_family = span.get("font", None)
|
||||||
|
color_int = span.get("color", 0)
|
||||||
|
font_color = f"#{color_int:06x}" if isinstance(color_int, int) else None
|
||||||
|
|
||||||
|
flags = span.get("flags", 0)
|
||||||
|
styles = []
|
||||||
|
if flags & 1: styles.append("superscript")
|
||||||
|
if flags & 2: styles.append("italic")
|
||||||
|
if flags & 4: styles.append("serif")
|
||||||
|
if flags & 8: styles.append("monospace")
|
||||||
|
if flags & 16: styles.append("bold")
|
||||||
|
font_style = ",".join(styles) if styles else "regular"
|
||||||
|
|
||||||
|
font_info = {
|
||||||
|
"family": font_family,
|
||||||
|
"size": font_size,
|
||||||
|
"color": font_color,
|
||||||
|
"style": font_style
|
||||||
|
}
|
||||||
|
|
||||||
|
for char in span.get("chars", []):
|
||||||
|
c = char["c"]
|
||||||
|
bbox = char["bbox"]
|
||||||
|
|
||||||
|
if c == ' ':
|
||||||
|
space_count += 1
|
||||||
|
if space_count >= 2:
|
||||||
|
if current_chunk and current_chunk["text"].strip():
|
||||||
|
chunks.append(current_chunk)
|
||||||
|
current_chunk = None
|
||||||
|
elif current_chunk:
|
||||||
|
current_chunk["text"] += c
|
||||||
|
current_chunk["bbox"][2] = max(current_chunk["bbox"][2], bbox[2])
|
||||||
|
current_chunk["bbox"][3] = max(current_chunk["bbox"][3], bbox[3])
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
space_count = 0
|
||||||
|
|
||||||
|
if current_chunk is None:
|
||||||
|
current_chunk = {"text": c, "bbox": list(bbox), "font_info": font_info}
|
||||||
|
continue
|
||||||
|
|
||||||
|
prev_x1 = current_chunk["bbox"][2]
|
||||||
|
distance = bbox[0] - prev_x1
|
||||||
|
|
||||||
|
if distance > space_threshold:
|
||||||
|
if current_chunk["text"].strip():
|
||||||
|
chunks.append(current_chunk)
|
||||||
|
current_chunk = {"text": c, "bbox": list(bbox), "font_info": font_info}
|
||||||
|
else:
|
||||||
|
current_chunk["text"] += c
|
||||||
|
current_chunk["bbox"][2] = max(current_chunk["bbox"][2], bbox[2])
|
||||||
|
current_chunk["bbox"][3] = max(current_chunk["bbox"][3], bbox[3])
|
||||||
|
current_chunk["bbox"][1] = min(current_chunk["bbox"][1], bbox[1])
|
||||||
|
current_chunk["bbox"][0] = min(current_chunk["bbox"][0], bbox[0])
|
||||||
|
|
||||||
|
if current_chunk and current_chunk["text"].strip():
|
||||||
|
chunks.append(current_chunk)
|
||||||
|
current_chunk = None
|
||||||
|
|
||||||
|
for chunk in chunks:
|
||||||
|
bbox = chunk["bbox"]
|
||||||
|
font_info = chunk["font_info"]
|
||||||
|
|
||||||
bbox = block["bbox"]
|
|
||||||
self.text_block_repo.create_text_block(
|
self.text_block_repo.create_text_block(
|
||||||
page_id=doc_page.id,
|
page_id=doc_page.id,
|
||||||
text=full_text,
|
text=chunk["text"].strip(),
|
||||||
x=bbox[0],
|
x=bbox[0],
|
||||||
y=bbox[1],
|
y=bbox[1],
|
||||||
width=bbox[2] - bbox[0],
|
width=bbox[2] - bbox[0],
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ class TemplateService:
|
|||||||
description=f"Auto-generated template from {document.original_filename}",
|
description=f"Auto-generated template from {document.original_filename}",
|
||||||
source_document_id=document.id,
|
source_document_id=document.id,
|
||||||
created_by=user_id,
|
created_by=user_id,
|
||||||
|
is_active=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Process each page
|
# Process each page
|
||||||
|
|||||||
1
docengine/app/storage/__init__.py
Normal file
1
docengine/app/storage/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Storage module
|
||||||
163
docengine/app/storage/provider.py
Normal file
163
docengine/app/storage/provider.py
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import uuid
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.core.exceptions import StorageError
|
||||||
|
from app.core.logging_config import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class StorageProvider(ABC):
|
||||||
|
"""Abstract base class for storage providers."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def save_file(self, file_data: bytes, directory: str, filename: str | None = None) -> str:
|
||||||
|
"""Save file data and return the storage path."""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def read_file(self, storage_path: str) -> bytes:
|
||||||
|
"""Read file data from storage."""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def delete_file(self, storage_path: str) -> bool:
|
||||||
|
"""Delete a file from storage. Returns True if successful."""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def file_exists(self, storage_path: str) -> bool:
|
||||||
|
"""Check if a file exists in storage."""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_file_size(self, storage_path: str) -> int:
|
||||||
|
"""Get the file size in bytes."""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_absolute_path(self, storage_path: str) -> str:
|
||||||
|
"""Get the absolute filesystem path for a storage path."""
|
||||||
|
...
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def compute_checksum(data: bytes, algorithm: str = "sha256") -> str:
|
||||||
|
"""Compute checksum of file data."""
|
||||||
|
hasher = hashlib.new(algorithm)
|
||||||
|
hasher.update(data)
|
||||||
|
return hasher.hexdigest()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_filename(original_filename: str) -> str:
|
||||||
|
"""Generate a unique filename preserving the original extension."""
|
||||||
|
ext = Path(original_filename).suffix.lower()
|
||||||
|
return f"{uuid.uuid4().hex}{ext}"
|
||||||
|
|
||||||
|
|
||||||
|
class LocalStorageProvider(StorageProvider):
|
||||||
|
"""Local filesystem storage provider."""
|
||||||
|
|
||||||
|
def __init__(self, base_path: str | None = None) -> None:
|
||||||
|
self.base_path = Path(base_path or settings.storage_local_path).resolve()
|
||||||
|
self._ensure_directories()
|
||||||
|
|
||||||
|
def _ensure_directories(self) -> None:
|
||||||
|
"""Create required storage directories."""
|
||||||
|
for subdir in ("documents", "templates", "images", "temp", "rendered"):
|
||||||
|
(self.base_path / subdir).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
def _resolve_path(self, storage_path: str) -> Path:
|
||||||
|
"""Resolve a storage path to an absolute path."""
|
||||||
|
resolved = (self.base_path / storage_path).resolve()
|
||||||
|
if not str(resolved).startswith(str(self.base_path)):
|
||||||
|
raise StorageError(f"Path traversal detected: {storage_path}")
|
||||||
|
return resolved
|
||||||
|
|
||||||
|
def save_file(self, file_data: bytes, directory: str, filename: str | None = None) -> str:
|
||||||
|
"""Save file data to local storage."""
|
||||||
|
if filename is None:
|
||||||
|
filename = f"{uuid.uuid4().hex}.bin"
|
||||||
|
|
||||||
|
dir_path = self.base_path / directory
|
||||||
|
dir_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
file_path = dir_path / filename
|
||||||
|
try:
|
||||||
|
file_path.write_bytes(file_data)
|
||||||
|
storage_path = str(file_path.relative_to(self.base_path))
|
||||||
|
logger.info("file_saved", storage_path=storage_path, size=len(file_data))
|
||||||
|
return storage_path
|
||||||
|
except OSError as e:
|
||||||
|
raise StorageError(f"Failed to save file: {e}") from e
|
||||||
|
|
||||||
|
def read_file(self, storage_path: str) -> bytes:
|
||||||
|
"""Read file data from local storage."""
|
||||||
|
file_path = self._resolve_path(storage_path)
|
||||||
|
if not file_path.exists():
|
||||||
|
raise StorageError(f"File not found: {storage_path}")
|
||||||
|
try:
|
||||||
|
return file_path.read_bytes()
|
||||||
|
except OSError as e:
|
||||||
|
raise StorageError(f"Failed to read file: {e}") from e
|
||||||
|
|
||||||
|
def delete_file(self, storage_path: str) -> bool:
|
||||||
|
"""Delete a file from local storage."""
|
||||||
|
file_path = self._resolve_path(storage_path)
|
||||||
|
if not file_path.exists():
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
file_path.unlink()
|
||||||
|
logger.info("file_deleted", storage_path=storage_path)
|
||||||
|
return True
|
||||||
|
except OSError as e:
|
||||||
|
logger.error("file_delete_failed", storage_path=storage_path, error=str(e))
|
||||||
|
raise StorageError(f"Failed to delete file: {e}") from e
|
||||||
|
|
||||||
|
def file_exists(self, storage_path: str) -> bool:
|
||||||
|
"""Check if a file exists in local storage."""
|
||||||
|
file_path = self._resolve_path(storage_path)
|
||||||
|
return file_path.exists()
|
||||||
|
|
||||||
|
def get_file_size(self, storage_path: str) -> int:
|
||||||
|
"""Get the file size in bytes."""
|
||||||
|
file_path = self._resolve_path(storage_path)
|
||||||
|
if not file_path.exists():
|
||||||
|
raise StorageError(f"File not found: {storage_path}")
|
||||||
|
return file_path.stat().st_size
|
||||||
|
|
||||||
|
def get_absolute_path(self, storage_path: str) -> str:
|
||||||
|
"""Get the absolute filesystem path."""
|
||||||
|
return str(self._resolve_path(storage_path))
|
||||||
|
|
||||||
|
def save_temp_file(self, file_data: bytes, filename: str) -> str:
|
||||||
|
"""Save a temporary file."""
|
||||||
|
return self.save_file(file_data, "temp", filename)
|
||||||
|
|
||||||
|
def cleanup_temp(self) -> int:
|
||||||
|
"""Remove all files in the temp directory."""
|
||||||
|
temp_dir = self.base_path / "temp"
|
||||||
|
count = 0
|
||||||
|
if temp_dir.exists():
|
||||||
|
for item in temp_dir.iterdir():
|
||||||
|
if item.is_file():
|
||||||
|
item.unlink()
|
||||||
|
count += 1
|
||||||
|
elif item.is_dir():
|
||||||
|
shutil.rmtree(item)
|
||||||
|
count += 1
|
||||||
|
logger.info("temp_cleanup", files_removed=count)
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
def get_storage_provider() -> StorageProvider:
|
||||||
|
"""Factory function to get the configured storage provider."""
|
||||||
|
if settings.storage_provider == "local":
|
||||||
|
return LocalStorageProvider()
|
||||||
|
raise StorageError(f"Unknown storage provider: {settings.storage_provider}")
|
||||||
@@ -57,7 +57,7 @@ def process_document_task(self, document_id: str) -> dict: # noqa: ANN001
|
|||||||
def match_document_task(
|
def match_document_task(
|
||||||
self, # noqa: ANN001
|
self, # noqa: ANN001
|
||||||
document_id: str,
|
document_id: str,
|
||||||
min_confidence: float = 0.5,
|
min_confidence: float = 0.75,
|
||||||
max_results: int = 5,
|
max_results: int = 5,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Celery task to match a document against templates."""
|
"""Celery task to match a document against templates."""
|
||||||
|
|||||||
62
docengine/app/utils/sanitizers.py
Normal file
62
docengine/app/utils/sanitizers.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import re
|
||||||
|
from typing import Optional
|
||||||
|
from datetime import datetime
|
||||||
|
from dateutil import parser
|
||||||
|
|
||||||
|
def sanitize_amount(text: str) -> Optional[float]:
|
||||||
|
"""
|
||||||
|
Sanitizes a string representing an amount/number (e.g., "$1,234.56", "€ 50,000", "50 USD")
|
||||||
|
by removing currency symbols, commas, and other non-numeric characters (except for the decimal separator),
|
||||||
|
and casts it to a float.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text (str): The raw extracted text from the document.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[float]: The sanitized numeric value, or None if no number could be extracted.
|
||||||
|
"""
|
||||||
|
if not text:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Remove obvious alphabetic currency codes, spaces, and commas
|
||||||
|
# We keep digits, period, and minus sign
|
||||||
|
cleaned_text = re.sub(r'[^\d\.-]', '', text)
|
||||||
|
|
||||||
|
if not cleaned_text:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Handle cases where multiple periods or dashes might exist incorrectly
|
||||||
|
# We just try to cast to float. If the OCR produced something like "1.23.45", this will fail.
|
||||||
|
# A more robust regex can handle exact capture if needed.
|
||||||
|
# But this basic cast covers 95% of standard sanitized strings.
|
||||||
|
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
|
||||||
|
|
||||||
@@ -25,6 +25,7 @@ celery_app.conf.update(
|
|||||||
task_reject_on_worker_lost=True,
|
task_reject_on_worker_lost=True,
|
||||||
broker_connection_retry_on_startup=True,
|
broker_connection_retry_on_startup=True,
|
||||||
result_expires=86400,
|
result_expires=86400,
|
||||||
|
task_default_queue="docengine_default",
|
||||||
task_routes={
|
task_routes={
|
||||||
"app.tasks.document_tasks.*": {"queue": "document_processing"},
|
"app.tasks.document_tasks.*": {"queue": "document_processing"},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export interface DocumentLayout {
|
|||||||
height: number;
|
height: number;
|
||||||
confidence: number;
|
confidence: number;
|
||||||
sequence_no: number;
|
sequence_no: number;
|
||||||
|
page_width: number;
|
||||||
|
page_height: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TemplateField {
|
export interface TemplateField {
|
||||||
@@ -66,7 +68,9 @@ export class TemplateService {
|
|||||||
width: tb.width,
|
width: tb.width,
|
||||||
height: tb.height,
|
height: tb.height,
|
||||||
confidence: tb.confidence || 0,
|
confidence: tb.confidence || 0,
|
||||||
sequence_no: tb.sequence || 0
|
sequence_no: tb.sequence || 0,
|
||||||
|
page_width: page.width || 1000,
|
||||||
|
page_height: page.height || 1000
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -77,15 +81,23 @@ export class TemplateService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
createTemplate(data: { template_name: string, fields: TemplateField[] }): Observable<Template> {
|
createTemplate(payload: any): Observable<any> {
|
||||||
return this.http.post<Template>(`${this.apiUrl}/templates`, data);
|
return this.http.post<any>(`${this.apiUrl}/templates`, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
saveMappings(templateId: number, mappings: any[]): Observable<any> {
|
updateTemplate(templateId: string, payload: any): Observable<any> {
|
||||||
|
return this.http.put<any>(`${this.apiUrl}/templates/${templateId}`, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveMappings(templateId: string, mappings: any[]): Observable<any> {
|
||||||
return this.http.post(`${this.apiUrl}/templates/${templateId}/mappings/save`, mappings);
|
return this.http.post(`${this.apiUrl}/templates/${templateId}/mappings/save`, mappings);
|
||||||
}
|
}
|
||||||
|
|
||||||
recognizeTemplate(documentId: string): Observable<any> {
|
getTemplate(templateId: string): Observable<any> {
|
||||||
return this.http.post(`${this.apiUrl}/documents/${documentId}/recognize`, {});
|
return this.http.get(`${this.apiUrl}/templates/${templateId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
recognizeTemplate(documentId: string): Observable<any[]> {
|
||||||
|
return this.http.post<any[]>(`${this.apiUrl}/templates/match`, { document_id: documentId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<p-toast></p-toast>
|
<p-toast></p-toast>
|
||||||
<div class="template-mapping-container">
|
<div class="template-mapping-container">
|
||||||
<p-splitter [panelSizes]="[50, 50]" [minSizes]="[30, 30]" styleClass="h-full w-full">
|
<p-splitter [panelSizes]="[70, 30]" [minSizes]="[50, 20]" styleClass="h-full w-full">
|
||||||
|
|
||||||
<!-- LEFT PANEL: DOCUMENT PREVIEW -->
|
<!-- LEFT PANEL: DOCUMENT PREVIEW -->
|
||||||
<ng-template pTemplate>
|
<ng-template pTemplate>
|
||||||
@@ -13,21 +13,53 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="preview-area"
|
<div class="preview-area p-4" style="overflow-y: auto; position: relative;">
|
||||||
id="document-layout-list"
|
<div *ngIf="layoutNodes.length === 0 && !isScanning" class="empty-state">
|
||||||
cdkDropList
|
<i class="pi pi-file text-4xl mb-3"></i>
|
||||||
[cdkDropListData]="layoutNodes"
|
|
||||||
[cdkDropListConnectedTo]="getConnectedDropLists()"
|
|
||||||
(cdkDropListDropped)="drop($event)">
|
|
||||||
|
|
||||||
<div *ngIf="layoutNodes.length === 0" class="empty-state">
|
|
||||||
<i class="pi pi-file-pdf 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>
|
||||||
|
|
||||||
<div *ngFor="let node of layoutNodes" cdkDrag class="layout-node" [ngClass]="node.block_type.toLowerCase()">
|
<!-- Scanning Overlay -->
|
||||||
<div class="node-type">{{ node.block_type }}</div>
|
<div *ngIf="isScanning" class="scanning-overlay">
|
||||||
<div class="node-text">{{ node.text_value }}</div>
|
<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 class="page-header text-sm font-semibold text-gray-500 mb-2">Page {{ page.page_number }}</div>
|
||||||
|
|
||||||
|
<div class="document-canvas"
|
||||||
|
[id]="'document-layout-list-' + i"
|
||||||
|
cdkDropList
|
||||||
|
[cdkDropListData]="page.nodes"
|
||||||
|
[cdkDropListConnectedTo]="getConnectedDropLists()"
|
||||||
|
(cdkDropListDropped)="drop($event)"
|
||||||
|
[style.aspect-ratio]="page.width + ' / ' + page.height">
|
||||||
|
|
||||||
|
<div *ngFor="let node of page.nodes; let j = index"
|
||||||
|
cdkDrag
|
||||||
|
[cdkDragData]="node"
|
||||||
|
class="layout-node"
|
||||||
|
(mouseup)="onNodeMouseUp(node, $event)"
|
||||||
|
[ngClass]="node.block_type.toLowerCase()"
|
||||||
|
[style.left.%]="(node.x_coordinate / node.page_width) * 100"
|
||||||
|
[style.top.%]="(node.y_coordinate / node.page_height) * 100"
|
||||||
|
[style.width.%]="(node.width / node.page_width) * 100"
|
||||||
|
[style.height.%]="(node.height / node.page_height) * 100">
|
||||||
|
<i class="pi pi-bars drag-handle" cdkDragHandle title="Drag Item"></i>
|
||||||
|
<i class="pi pi-times close-icon" (click)="removeNode(i, j, $event)" title="Delete Item"></i>
|
||||||
|
<div class="node-type" *ngIf="node.height > 20">{{ node.block_type }}</div>
|
||||||
|
<div class="node-text">{{ node.text_value }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,24 +87,29 @@
|
|||||||
|
|
||||||
<div class="field-list">
|
<div class="field-list">
|
||||||
<div *ngFor="let field of templateFields" class="template-field-container">
|
<div *ngFor="let field of templateFields" class="template-field-container">
|
||||||
<div class="field-header">
|
<div class="field-header flex justify-content-between align-items-center">
|
||||||
<strong>{{ field.field_label }}</strong>
|
<div>
|
||||||
<span class="badge">{{ field.field_type }}</span>
|
<strong>{{ field.field_label }}</strong>
|
||||||
|
<span class="badge ml-2">{{ field.field_type }}</span>
|
||||||
|
</div>
|
||||||
|
<p-button icon="pi pi-trash" styleClass="p-button-rounded p-button-danger p-button-text p-button-sm" (onClick)="removeField(field.pk_template_field_id)"></p-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="drop-zone"
|
<div class="drop-zone"
|
||||||
[id]="'field-' + field.pk_template_field_id"
|
[id]="'field-' + field.pk_template_field_id"
|
||||||
cdkDropList
|
cdkDropList
|
||||||
[cdkDropListData]="mappings[field.pk_template_field_id]"
|
[cdkDropListData]="mappings[field.pk_template_field_id]"
|
||||||
[cdkDropListConnectedTo]="['document-layout-list']"
|
[cdkDropListConnectedTo]="getCanvasDropLists()"
|
||||||
(cdkDropListDropped)="drop($event, field.pk_template_field_id)">
|
(cdkDropListDropped)="drop($event, field.pk_template_field_id)">
|
||||||
|
|
||||||
<div class="placeholder" *ngIf="mappings[field.pk_template_field_id].length === 0">
|
<div class="placeholder" *ngIf="mappings[field.pk_template_field_id].length === 0">
|
||||||
Drop value here...
|
Drop value here...
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngFor="let mappedNode of mappings[field.pk_template_field_id]" cdkDrag class="mapped-node">
|
<div *ngFor="let mappedNode of mappings[field.pk_template_field_id]; let k = index" cdkDrag [cdkDragData]="mappedNode" class="mapped-node">
|
||||||
<i class="pi pi-arrow-left text-xs mr-2"></i> {{ mappedNode.text_value }}
|
<i class="pi pi-arrow-left text-xs mr-2"></i>
|
||||||
|
<span class="flex-1 overflow-hidden white-space-nowrap text-overflow-ellipsis">{{ mappedNode.text_value }}</span>
|
||||||
|
<i class="pi pi-times cursor-pointer text-red-500 hover:text-red-700 ml-2" (click)="removeFromField(field.pk_template_field_id!, k)" title="Remove"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -47,45 +47,137 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.document-canvas {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
/* Draggable Nodes */
|
/* Draggable Nodes */
|
||||||
.layout-node {
|
.layout-node {
|
||||||
padding: 0.75rem;
|
position: absolute;
|
||||||
margin-bottom: 0.5rem;
|
box-sizing: content-box !important;
|
||||||
background: white;
|
border: 6px solid transparent;
|
||||||
border: 1px solid var(--surface-border);
|
margin: -6px !important;
|
||||||
border-left: 4px solid var(--primary-color);
|
background-clip: padding-box;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: grab;
|
transition: box-shadow 0.2s, border-color 0.2s;
|
||||||
transition: box-shadow 0.2s;
|
overflow: hidden;
|
||||||
|
resize: both;
|
||||||
&:hover {
|
display: flex;
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
flex-direction: column;
|
||||||
}
|
justify-content: center;
|
||||||
|
min-width: 60px;
|
||||||
|
min-height: 25px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-type {
|
.node-type {
|
||||||
font-size: 0.7rem;
|
font-size: 0.5rem;
|
||||||
color: var(--text-color-secondary);
|
color: var(--text-color-secondary);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.1rem;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-text {
|
.node-text {
|
||||||
font-size: 0.9rem;
|
font-size: 0.65rem;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
line-height: 1.2;
|
||||||
|
white-space: normal;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Color coding by block type */
|
/* Color coding by block type - use borders now instead of thick left border */
|
||||||
&.header { border-left-color: #3B82F6; }
|
&.header { box-shadow: inset 0 0 0 1.5px #3B82F6; }
|
||||||
&.vendor { border-left-color: #8B5CF6; }
|
&.vendor { box-shadow: inset 0 0 0 1.5px #8B5CF6; }
|
||||||
&.table_header { border-left-color: #F59E0B; }
|
&.table_header { box-shadow: inset 0 0 0 1.5px #F59E0B; }
|
||||||
&.total { border-left-color: #10B981; }
|
&.total { box-shadow: inset 0 0 0 1.5px #10B981; }
|
||||||
&.tax { border-left-color: #EF4444; }
|
&.tax { box-shadow: inset 0 0 0 1.5px #EF4444; }
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
z-index: 10;
|
||||||
|
&.header { box-shadow: inset 0 0 0 2px #3B82F6, 0 4px 8px rgba(59, 130, 246, 0.2); }
|
||||||
|
&.vendor { box-shadow: inset 0 0 0 2px #8B5CF6, 0 4px 8px rgba(139, 92, 246, 0.2); }
|
||||||
|
&.table_header { box-shadow: inset 0 0 0 2px #F59E0B, 0 4px 8px rgba(245, 158, 11, 0.2); }
|
||||||
|
&.total { box-shadow: inset 0 0 0 2px #10B981, 0 4px 8px rgba(16, 185, 129, 0.2); }
|
||||||
|
&.tax { box-shadow: inset 0 0 0 2px #EF4444, 0 4px 8px rgba(239, 68, 68, 0.2); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle {
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
font-size: 0.55rem;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
background: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: grab;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s, transform 0.2s;
|
||||||
|
z-index: 25;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
||||||
|
border: 1px solid var(--surface-border);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--primary-color);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .drag-handle {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
right: 0px;
|
||||||
|
font-size: 0.55rem;
|
||||||
|
color: white;
|
||||||
|
background: #EF4444;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s, transform 0.2s;
|
||||||
|
z-index: 25;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #DC2626;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .close-icon {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Template Mapping Side */
|
/* Template Mapping Side */
|
||||||
@@ -152,3 +244,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; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { DragDropModule, CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
|
import { DragDropModule, CdkDragDrop, moveItemInArray, transferArrayItem, copyArrayItem } from '@angular/cdk/drag-drop';
|
||||||
import { SplitterModule } from 'primeng/splitter';
|
import { SplitterModule } from 'primeng/splitter';
|
||||||
import { ButtonModule } from 'primeng/button';
|
import { ButtonModule } from 'primeng/button';
|
||||||
import { DialogModule } from 'primeng/dialog';
|
import { DialogModule } from 'primeng/dialog';
|
||||||
@@ -36,6 +36,9 @@ export class TemplatesComponent implements OnInit {
|
|||||||
// Document State
|
// Document State
|
||||||
documentId: string | null = null;
|
documentId: string | null = null;
|
||||||
layoutNodes: DocumentLayout[] = [];
|
layoutNodes: DocumentLayout[] = [];
|
||||||
|
pages: { page_number: number, nodes: DocumentLayout[], width: number, height: number }[] = [];
|
||||||
|
pageWidth: number = 800;
|
||||||
|
pageHeight: number = 1100;
|
||||||
|
|
||||||
// Template State
|
// Template State
|
||||||
templateName: string = '';
|
templateName: string = '';
|
||||||
@@ -44,11 +47,15 @@ export class TemplatesComponent implements OnInit {
|
|||||||
|
|
||||||
// UI State
|
// UI State
|
||||||
displayAddField: boolean = false;
|
displayAddField: boolean = false;
|
||||||
|
isScanning: boolean = false;
|
||||||
|
scanProgress: number = 0;
|
||||||
|
currentTemplateId: string | null = null;
|
||||||
newField: TemplateField = { field_label: '', field_type: 'TEXT', display_order: 0, required_flag: false };
|
newField: TemplateField = { field_label: '', field_type: 'TEXT', display_order: 0, required_flag: false };
|
||||||
fieldTypes = [
|
fieldTypes = [
|
||||||
{ 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' }
|
||||||
@@ -64,10 +71,16 @@ export class TemplatesComponent implements OnInit {
|
|||||||
onFileUpload(event: any) {
|
onFileUpload(event: any) {
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
|
this.currentTemplateId = null;
|
||||||
|
this.templateName = '';
|
||||||
|
this.templateFields = [];
|
||||||
|
this.mappings = {};
|
||||||
|
|
||||||
this.templateService.uploadDocument(file).subscribe({
|
this.templateService.uploadDocument(file).subscribe({
|
||||||
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) => {
|
||||||
@@ -87,7 +100,30 @@ 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 }>();
|
||||||
|
|
||||||
|
layouts.forEach(node => {
|
||||||
|
if (!pageMap.has(node.page_no)) {
|
||||||
|
pageMap.set(node.page_no, {
|
||||||
|
page_number: node.page_no,
|
||||||
|
nodes: [],
|
||||||
|
width: node.page_width,
|
||||||
|
height: node.page_height
|
||||||
|
});
|
||||||
|
}
|
||||||
|
pageMap.get(node.page_no)!.nodes.push(node);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.pages = Array.from(pageMap.values()).sort((a, b) => a.page_number - b.page_number);
|
||||||
|
|
||||||
|
if (this.pages.length > 0) {
|
||||||
|
this.pageWidth = this.pages[0].width;
|
||||||
|
this.pageHeight = this.pages[0].height;
|
||||||
|
}
|
||||||
// Trigger auto-recognition
|
// Trigger auto-recognition
|
||||||
this.autoRecognize();
|
this.autoRecognize();
|
||||||
}
|
}
|
||||||
@@ -97,10 +133,72 @@ export class TemplatesComponent implements OnInit {
|
|||||||
autoRecognize() {
|
autoRecognize() {
|
||||||
if (!this.documentId) return;
|
if (!this.documentId) return;
|
||||||
this.templateService.recognizeTemplate(this.documentId).subscribe({
|
this.templateService.recognizeTemplate(this.documentId).subscribe({
|
||||||
next: (res) => {
|
next: (matches) => {
|
||||||
if (res.templateMatched) {
|
if (matches && matches.length > 0 && matches[0].confidence_score >= 0.75) {
|
||||||
this.messageService.add({ severity: 'info', summary: 'Template Recognized', detail: `Confidence: ${res.confidence}%` });
|
const match = matches[0];
|
||||||
// In a full implementation, we'd load the template fields and populate the mappings.
|
this.messageService.add({ severity: 'info', summary: 'Template Recognized', detail: `Confidence: ${(match.confidence_score * 100).toFixed(1)}%` });
|
||||||
|
|
||||||
|
this.templateService.getTemplate(match.format_id).subscribe({
|
||||||
|
next: (templateData) => {
|
||||||
|
this.templateName = templateData.name;
|
||||||
|
this.currentTemplateId = match.format_id;
|
||||||
|
this.templateFields = [];
|
||||||
|
this.mappings = {};
|
||||||
|
|
||||||
|
if (templateData.cells) {
|
||||||
|
templateData.cells.forEach((cell: any) => {
|
||||||
|
if (cell.is_dynamic) {
|
||||||
|
const fieldId = new Date().getTime() + Math.random();
|
||||||
|
this.templateFields.push({
|
||||||
|
pk_template_field_id: fieldId as any,
|
||||||
|
field_label: cell.field_name,
|
||||||
|
field_type: cell.data_type,
|
||||||
|
display_order: cell.sequence,
|
||||||
|
required_flag: false
|
||||||
|
});
|
||||||
|
this.mappings[fieldId] = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (templateData.regions) {
|
||||||
|
templateData.regions.forEach((region: any) => {
|
||||||
|
if (region.region_type === 'field_mapping' && region.content && region.content.field_name) {
|
||||||
|
const field = this.templateFields.find(f => f.field_label === region.content.field_name);
|
||||||
|
if (field && field.pk_template_field_id) {
|
||||||
|
// Find the corresponding page in the current document
|
||||||
|
const page = this.pages.find(p => p.page_number === region.page_number);
|
||||||
|
if (page) {
|
||||||
|
let bestMatchIdx = -1;
|
||||||
|
let bestOverlap = 0;
|
||||||
|
|
||||||
|
// Find the text node on the canvas that overlaps most with this region's bounding box
|
||||||
|
for (let i = 0; i < page.nodes.length; i++) {
|
||||||
|
const node = page.nodes[i];
|
||||||
|
const x_overlap = Math.max(0, Math.min(region.x + region.width, node.x_coordinate + node.width) - Math.max(region.x, node.x_coordinate));
|
||||||
|
const y_overlap = Math.max(0, Math.min(region.y + region.height, node.y_coordinate + node.height) - Math.max(region.y, node.y_coordinate));
|
||||||
|
const overlapArea = x_overlap * y_overlap;
|
||||||
|
|
||||||
|
if (overlapArea > bestOverlap) {
|
||||||
|
bestOverlap = overlapArea;
|
||||||
|
bestMatchIdx = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we found a matching node (at least some overlap), clone it to mappings
|
||||||
|
if (bestMatchIdx !== -1) {
|
||||||
|
const matchedNode = page.nodes[bestMatchIdx];
|
||||||
|
|
||||||
|
// Because *ngFor tracks objects by reference, we clone it
|
||||||
|
this.mappings[field.pk_template_field_id].push({...matchedNode});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -119,6 +217,12 @@ export class TemplatesComponent implements OnInit {
|
|||||||
this.displayAddField = false;
|
this.displayAddField = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeField(fieldId?: number) {
|
||||||
|
if (!fieldId) return;
|
||||||
|
this.templateFields = this.templateFields.filter(f => f.pk_template_field_id !== fieldId);
|
||||||
|
delete this.mappings[fieldId];
|
||||||
|
}
|
||||||
|
|
||||||
saveTemplateAndMappings() {
|
saveTemplateAndMappings() {
|
||||||
if (!this.templateName || this.templateName.trim() === '') {
|
if (!this.templateName || this.templateName.trim() === '') {
|
||||||
this.messageService.add({ severity: 'warn', summary: 'Warning', detail: 'Please provide a template name.' });
|
this.messageService.add({ severity: 'warn', summary: 'Warning', detail: 'Please provide a template name.' });
|
||||||
@@ -130,13 +234,47 @@ export class TemplatesComponent implements OnInit {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.templateService.createTemplate({
|
const payload = {
|
||||||
template_name: this.templateName,
|
template_name: this.templateName,
|
||||||
|
source_document_id: this.documentId || undefined,
|
||||||
fields: this.templateFields
|
fields: this.templateFields
|
||||||
}).subscribe({
|
};
|
||||||
next: (res) => {
|
|
||||||
// In a real scenario, we pass res.templateId and map the layoutNodes
|
const saveRequest = this.currentTemplateId
|
||||||
this.messageService.add({ severity: 'success', summary: 'Success', detail: 'Template & Mappings saved successfully.' });
|
? this.templateService.updateTemplate(this.currentTemplateId, payload)
|
||||||
|
: this.templateService.createTemplate(payload);
|
||||||
|
|
||||||
|
saveRequest.subscribe({
|
||||||
|
next: (res: any) => {
|
||||||
|
const templateId = res.pk_template_id;
|
||||||
|
|
||||||
|
// Construct the mappings payload
|
||||||
|
const mappingsPayload: any[] = [];
|
||||||
|
this.templateFields.forEach(field => {
|
||||||
|
const fieldId = field.pk_template_field_id;
|
||||||
|
if (fieldId && this.mappings[fieldId] && this.mappings[fieldId].length > 0) {
|
||||||
|
mappingsPayload.push({
|
||||||
|
field_name: field.field_label,
|
||||||
|
mapped_nodes: this.mappings[fieldId]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mappingsPayload.length > 0) {
|
||||||
|
this.templateService.saveMappings(templateId, mappingsPayload).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.messageService.add({ severity: 'success', summary: 'Success', detail: 'Template & Mappings saved successfully.' });
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Failed to save mappings.' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.messageService.add({ severity: 'success', summary: 'Success', detail: 'Template saved successfully (No mappings).' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Failed to save template.' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -144,14 +282,93 @@ export class TemplatesComponent implements OnInit {
|
|||||||
// Drag and Drop Logic
|
// Drag and Drop Logic
|
||||||
drop(event: CdkDragDrop<DocumentLayout[]>, fieldId?: number) {
|
drop(event: CdkDragDrop<DocumentLayout[]>, fieldId?: number) {
|
||||||
if (event.previousContainer === event.container) {
|
if (event.previousContainer === event.container) {
|
||||||
moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
|
// Do nothing! This allows the item to naturally snap back to its original position
|
||||||
|
// since it's a failed drop (didn't land in a mapping field).
|
||||||
} else {
|
} else {
|
||||||
transferArrayItem(
|
const isFromCanvas = event.previousContainer.id.startsWith('document-layout-list');
|
||||||
event.previousContainer.data,
|
const isToCanvas = event.container.id.startsWith('document-layout-list');
|
||||||
event.container.data,
|
|
||||||
event.previousIndex,
|
if (isFromCanvas && !isToCanvas) {
|
||||||
event.currentIndex,
|
// Drag from Canvas -> Field
|
||||||
);
|
// We use event.item.data which perfectly tracks the dragged object regardless of DOM indexes
|
||||||
|
const clonedNode = JSON.parse(JSON.stringify(event.item.data));
|
||||||
|
|
||||||
|
// Sanitize the value if dropped on an AMOUNT or NUMBER field
|
||||||
|
if (fieldId) {
|
||||||
|
const field = this.templateFields.find(f => f.pk_template_field_id === fieldId);
|
||||||
|
if (field && (field.field_type === 'AMOUNT' || field.field_type === 'NUMBER') && clonedNode.text_value) {
|
||||||
|
const numericText = clonedNode.text_value.replace(/[^\d\.-]/g, '');
|
||||||
|
const floatValue = parseFloat(numericText);
|
||||||
|
if (!isNaN(floatValue)) {
|
||||||
|
clonedNode.text_value = floatValue.toString();
|
||||||
|
} 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the clone into the destination mapping field
|
||||||
|
event.container.data.splice(event.currentIndex, 0, clonedNode);
|
||||||
|
|
||||||
|
// Force Angular to completely recreate the DOM elements for this specific canvas page.
|
||||||
|
// By using .map(node => ({...node})), we change every object's identity.
|
||||||
|
// This forces Angular to destroy the corrupted DOM element (which CDK moved and left a translate3d on)
|
||||||
|
// and recreate it fresh with its original absolute coordinates!
|
||||||
|
const pageIndex = parseInt(event.previousContainer.id.split('-').pop() || '0');
|
||||||
|
if (!isNaN(pageIndex) && this.pages[pageIndex]) {
|
||||||
|
this.pages[pageIndex].nodes = this.pages[pageIndex].nodes.map(node => ({...node}));
|
||||||
|
|
||||||
|
// Also explicitly clear the transform on the dragged element just in case CDK holds a ref to it
|
||||||
|
event.item.element.nativeElement.style.transform = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (!isFromCanvas && isToCanvas) {
|
||||||
|
// Drag from Field -> Canvas (Delete from Field)
|
||||||
|
event.previousContainer.data.splice(event.previousIndex, 1);
|
||||||
|
} else {
|
||||||
|
// Drag from Field -> Field (Move)
|
||||||
|
transferArrayItem(
|
||||||
|
event.previousContainer.data,
|
||||||
|
event.container.data,
|
||||||
|
event.previousIndex,
|
||||||
|
event.currentIndex,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onNodeMouseUp(node: DocumentLayout, event: MouseEvent) {
|
||||||
|
// If the user resized the node using the native CSS resize handle, save the new size
|
||||||
|
const el = event.currentTarget as HTMLElement;
|
||||||
|
if (el.style.width && el.style.width.endsWith('px')) {
|
||||||
|
const parentRect = el.parentElement!.getBoundingClientRect();
|
||||||
|
node.width = (el.offsetWidth / parentRect.width) * node.page_width;
|
||||||
|
node.height = (el.offsetHeight / parentRect.height) * node.page_height;
|
||||||
|
|
||||||
|
// Clear the inline pixel styles so Angular bindings take over smoothly
|
||||||
|
el.style.width = '';
|
||||||
|
el.style.height = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,4 +376,19 @@ export class TemplatesComponent implements OnInit {
|
|||||||
// Return all field ID strings as drop lists
|
// Return all field ID strings as drop lists
|
||||||
return this.templateFields.map(f => 'field-' + f.pk_template_field_id);
|
return this.templateFields.map(f => 'field-' + f.pk_template_field_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCanvasDropLists() {
|
||||||
|
return this.pages.map((_, i) => 'document-layout-list-' + i);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeNode(pageIndex: number, nodeIndex: number, event: Event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
this.pages[pageIndex].nodes.splice(nodeIndex, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFromField(fieldId: number, nodeIndex: number) {
|
||||||
|
if (this.mappings[fieldId]) {
|
||||||
|
this.mappings[fieldId].splice(nodeIndex, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
list_templates.py
Normal file
11
list_templates.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.append(os.path.join(os.getcwd(), 'docengine'))
|
||||||
|
|
||||||
|
from app.core.database import SessionLocal
|
||||||
|
from app.models.template import DocumentFormat
|
||||||
|
|
||||||
|
db = SessionLocal()
|
||||||
|
templates = db.query(DocumentFormat).all()
|
||||||
|
for t in templates:
|
||||||
|
print(f"ID: {t.id}, Name: {t.name}, Source: {t.source_document_id}, Active: {t.is_active}")
|
||||||
BIN
ocr.backup
Normal file
BIN
ocr.backup
Normal file
Binary file not shown.
BIN
oss-microservices/.DS_Store
vendored
BIN
oss-microservices/.DS_Store
vendored
Binary file not shown.
@@ -1,5 +0,0 @@
|
|||||||
DB_HOST=192.168.0.111
|
|
||||||
DB_PORT=7925
|
|
||||||
DB_NAME=ocr
|
|
||||||
DB_USER=postgres
|
|
||||||
DB_PASSWORD=M@tr!x#149@dm!N
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.27" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8">
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\AdminService.Infrastructure\AdminService.Infrastructure.csproj" />
|
|
||||||
<ProjectReference Include="..\AdminService.Domain\AdminService.Domain.csproj" />
|
|
||||||
<ProjectReference Include="..\..\Shared.Contracts\Shared.Contracts.csproj" />
|
|
||||||
<ProjectReference Include="..\..\Shared.Commons\Shared.Commons.csproj" />
|
|
||||||
<ProjectReference Include="..\AdminService.Application\AdminService.Application.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
@AdminService.API_HostAddress = http://localhost:5066
|
|
||||||
|
|
||||||
GET {{AdminService.API_HostAddress}}/weatherforecast/
|
|
||||||
Accept: application/json
|
|
||||||
|
|
||||||
###
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
using AdminService.Infrastructure;
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
|
||||||
|
|
||||||
builder.Services.AddInfrastructure(builder.Configuration);
|
|
||||||
|
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
|
||||||
builder.Services.AddSwaggerGen();
|
|
||||||
|
|
||||||
var app = builder.Build();
|
|
||||||
|
|
||||||
if (app.Environment.IsDevelopment())
|
|
||||||
{
|
|
||||||
app.UseSwagger();
|
|
||||||
app.UseSwaggerUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
|
||||||
|
|
||||||
app.UseAuthorization();
|
|
||||||
|
|
||||||
app.MapControllers();
|
|
||||||
|
|
||||||
app.Run();
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
|
||||||
"iisSettings": {
|
|
||||||
"windowsAuthentication": false,
|
|
||||||
"anonymousAuthentication": true,
|
|
||||||
"iisExpress": {
|
|
||||||
"applicationUrl": "http://localhost:20091",
|
|
||||||
"sslPort": 44325
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"profiles": {
|
|
||||||
"http": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"dotnetRunMessages": true,
|
|
||||||
"launchBrowser": true,
|
|
||||||
"launchUrl": "swagger",
|
|
||||||
"applicationUrl": "http://localhost:5066",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"https": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"dotnetRunMessages": true,
|
|
||||||
"launchBrowser": true,
|
|
||||||
"launchUrl": "swagger",
|
|
||||||
"applicationUrl": "https://localhost:7244;http://localhost:5066",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"IIS Express": {
|
|
||||||
"commandName": "IISExpress",
|
|
||||||
"launchBrowser": true,
|
|
||||||
"launchUrl": "swagger",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"ConnectionStrings": {
|
|
||||||
"DefaultConnection": "Host=192.168.0.111;Port=7925;Database=ocr;Username=postgres;Password=M@tr!x#149@dm!N"
|
|
||||||
},
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AllowedHosts": "*"
|
|
||||||
}
|
|
||||||
Binary file not shown.
@@ -1,968 +0,0 @@
|
|||||||
{
|
|
||||||
"runtimeTarget": {
|
|
||||||
"name": ".NETCoreApp,Version=v8.0",
|
|
||||||
"signature": ""
|
|
||||||
},
|
|
||||||
"compilationOptions": {},
|
|
||||||
"targets": {
|
|
||||||
".NETCoreApp,Version=v8.0": {
|
|
||||||
"AdminService.API/1.0.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"AdminService.Application": "1.0.0",
|
|
||||||
"AdminService.Domain": "1.0.0",
|
|
||||||
"AdminService.Infrastructure": "1.0.0",
|
|
||||||
"Microsoft.AspNetCore.OpenApi": "8.0.27",
|
|
||||||
"Microsoft.EntityFrameworkCore.Design": "8.0.8",
|
|
||||||
"Shared.Commons": "1.0.0",
|
|
||||||
"Shared.Contracts": "1.0.0",
|
|
||||||
"Swashbuckle.AspNetCore": "6.6.2"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"AdminService.API.dll": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Humanizer.Core/2.14.1": {
|
|
||||||
"runtime": {
|
|
||||||
"lib/net6.0/Humanizer.dll": {
|
|
||||||
"assemblyVersion": "2.14.0.0",
|
|
||||||
"fileVersion": "2.14.1.48190"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.AspNetCore.OpenApi/8.0.27": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.OpenApi": "1.6.14"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net8.0/Microsoft.AspNetCore.OpenApi.dll": {
|
|
||||||
"assemblyVersion": "8.0.27.0",
|
|
||||||
"fileVersion": "8.0.2726.23008"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Bcl.AsyncInterfaces/6.0.0": {
|
|
||||||
"runtime": {
|
|
||||||
"lib/netstandard2.1/Microsoft.Bcl.AsyncInterfaces.dll": {
|
|
||||||
"assemblyVersion": "6.0.0.0",
|
|
||||||
"fileVersion": "6.0.21.52210"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.CodeAnalysis.Analyzers/3.3.3": {},
|
|
||||||
"Microsoft.CodeAnalysis.Common/4.5.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.CodeAnalysis.Analyzers": "3.3.3",
|
|
||||||
"System.Collections.Immutable": "6.0.0",
|
|
||||||
"System.Reflection.Metadata": "6.0.1",
|
|
||||||
"System.Runtime.CompilerServices.Unsafe": "6.0.0",
|
|
||||||
"System.Text.Encoding.CodePages": "6.0.0"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/netcoreapp3.1/Microsoft.CodeAnalysis.dll": {
|
|
||||||
"assemblyVersion": "4.5.0.0",
|
|
||||||
"fileVersion": "4.500.23.10905"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"resources": {
|
|
||||||
"lib/netcoreapp3.1/cs/Microsoft.CodeAnalysis.resources.dll": {
|
|
||||||
"locale": "cs"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/de/Microsoft.CodeAnalysis.resources.dll": {
|
|
||||||
"locale": "de"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/es/Microsoft.CodeAnalysis.resources.dll": {
|
|
||||||
"locale": "es"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/fr/Microsoft.CodeAnalysis.resources.dll": {
|
|
||||||
"locale": "fr"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/it/Microsoft.CodeAnalysis.resources.dll": {
|
|
||||||
"locale": "it"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/ja/Microsoft.CodeAnalysis.resources.dll": {
|
|
||||||
"locale": "ja"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/ko/Microsoft.CodeAnalysis.resources.dll": {
|
|
||||||
"locale": "ko"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/pl/Microsoft.CodeAnalysis.resources.dll": {
|
|
||||||
"locale": "pl"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/pt-BR/Microsoft.CodeAnalysis.resources.dll": {
|
|
||||||
"locale": "pt-BR"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/ru/Microsoft.CodeAnalysis.resources.dll": {
|
|
||||||
"locale": "ru"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/tr/Microsoft.CodeAnalysis.resources.dll": {
|
|
||||||
"locale": "tr"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/zh-Hans/Microsoft.CodeAnalysis.resources.dll": {
|
|
||||||
"locale": "zh-Hans"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/zh-Hant/Microsoft.CodeAnalysis.resources.dll": {
|
|
||||||
"locale": "zh-Hant"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.CodeAnalysis.CSharp/4.5.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.CodeAnalysis.Common": "4.5.0"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/netcoreapp3.1/Microsoft.CodeAnalysis.CSharp.dll": {
|
|
||||||
"assemblyVersion": "4.5.0.0",
|
|
||||||
"fileVersion": "4.500.23.10905"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"resources": {
|
|
||||||
"lib/netcoreapp3.1/cs/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
|
||||||
"locale": "cs"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/de/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
|
||||||
"locale": "de"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/es/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
|
||||||
"locale": "es"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/fr/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
|
||||||
"locale": "fr"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/it/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
|
||||||
"locale": "it"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/ja/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
|
||||||
"locale": "ja"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/ko/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
|
||||||
"locale": "ko"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/pl/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
|
||||||
"locale": "pl"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/pt-BR/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
|
||||||
"locale": "pt-BR"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/ru/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
|
||||||
"locale": "ru"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/tr/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
|
||||||
"locale": "tr"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/zh-Hans/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
|
||||||
"locale": "zh-Hans"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/zh-Hant/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
|
||||||
"locale": "zh-Hant"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.CodeAnalysis.CSharp.Workspaces/4.5.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"Humanizer.Core": "2.14.1",
|
|
||||||
"Microsoft.CodeAnalysis.CSharp": "4.5.0",
|
|
||||||
"Microsoft.CodeAnalysis.Common": "4.5.0",
|
|
||||||
"Microsoft.CodeAnalysis.Workspaces.Common": "4.5.0"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/netcoreapp3.1/Microsoft.CodeAnalysis.CSharp.Workspaces.dll": {
|
|
||||||
"assemblyVersion": "4.5.0.0",
|
|
||||||
"fileVersion": "4.500.23.10905"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"resources": {
|
|
||||||
"lib/netcoreapp3.1/cs/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
|
|
||||||
"locale": "cs"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/de/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
|
|
||||||
"locale": "de"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/es/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
|
|
||||||
"locale": "es"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/fr/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
|
|
||||||
"locale": "fr"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/it/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
|
|
||||||
"locale": "it"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/ja/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
|
|
||||||
"locale": "ja"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/ko/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
|
|
||||||
"locale": "ko"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/pl/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
|
|
||||||
"locale": "pl"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/pt-BR/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
|
|
||||||
"locale": "pt-BR"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/ru/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
|
|
||||||
"locale": "ru"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/tr/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
|
|
||||||
"locale": "tr"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/zh-Hans/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
|
|
||||||
"locale": "zh-Hans"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/zh-Hant/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
|
|
||||||
"locale": "zh-Hant"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.CodeAnalysis.Workspaces.Common/4.5.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"Humanizer.Core": "2.14.1",
|
|
||||||
"Microsoft.Bcl.AsyncInterfaces": "6.0.0",
|
|
||||||
"Microsoft.CodeAnalysis.Common": "4.5.0",
|
|
||||||
"System.Composition": "6.0.0",
|
|
||||||
"System.IO.Pipelines": "6.0.3",
|
|
||||||
"System.Threading.Channels": "6.0.0"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/netcoreapp3.1/Microsoft.CodeAnalysis.Workspaces.dll": {
|
|
||||||
"assemblyVersion": "4.5.0.0",
|
|
||||||
"fileVersion": "4.500.23.10905"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"resources": {
|
|
||||||
"lib/netcoreapp3.1/cs/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
|
|
||||||
"locale": "cs"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/de/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
|
|
||||||
"locale": "de"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/es/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
|
|
||||||
"locale": "es"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/fr/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
|
|
||||||
"locale": "fr"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/it/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
|
|
||||||
"locale": "it"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/ja/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
|
|
||||||
"locale": "ja"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/ko/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
|
|
||||||
"locale": "ko"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/pl/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
|
|
||||||
"locale": "pl"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/pt-BR/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
|
|
||||||
"locale": "pt-BR"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/ru/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
|
|
||||||
"locale": "ru"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/tr/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
|
|
||||||
"locale": "tr"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/zh-Hans/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
|
|
||||||
"locale": "zh-Hans"
|
|
||||||
},
|
|
||||||
"lib/netcoreapp3.1/zh-Hant/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
|
|
||||||
"locale": "zh-Hant"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.EntityFrameworkCore/8.0.8": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.EntityFrameworkCore.Abstractions": "8.0.8",
|
|
||||||
"Microsoft.EntityFrameworkCore.Analyzers": "8.0.8",
|
|
||||||
"Microsoft.Extensions.Caching.Memory": "8.0.0",
|
|
||||||
"Microsoft.Extensions.Logging": "8.0.0"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net8.0/Microsoft.EntityFrameworkCore.dll": {
|
|
||||||
"assemblyVersion": "8.0.8.0",
|
|
||||||
"fileVersion": "8.0.824.36704"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.EntityFrameworkCore.Abstractions/8.0.8": {
|
|
||||||
"runtime": {
|
|
||||||
"lib/net8.0/Microsoft.EntityFrameworkCore.Abstractions.dll": {
|
|
||||||
"assemblyVersion": "8.0.8.0",
|
|
||||||
"fileVersion": "8.0.824.36704"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.EntityFrameworkCore.Analyzers/8.0.8": {},
|
|
||||||
"Microsoft.EntityFrameworkCore.Design/8.0.8": {
|
|
||||||
"dependencies": {
|
|
||||||
"Humanizer.Core": "2.14.1",
|
|
||||||
"Microsoft.CodeAnalysis.CSharp.Workspaces": "4.5.0",
|
|
||||||
"Microsoft.EntityFrameworkCore.Relational": "8.0.8",
|
|
||||||
"Microsoft.Extensions.DependencyModel": "8.0.1",
|
|
||||||
"Mono.TextTemplating": "2.2.1"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net8.0/Microsoft.EntityFrameworkCore.Design.dll": {
|
|
||||||
"assemblyVersion": "8.0.8.0",
|
|
||||||
"fileVersion": "8.0.824.36704"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.EntityFrameworkCore.Relational/8.0.8": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.EntityFrameworkCore": "8.0.8",
|
|
||||||
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net8.0/Microsoft.EntityFrameworkCore.Relational.dll": {
|
|
||||||
"assemblyVersion": "8.0.8.0",
|
|
||||||
"fileVersion": "8.0.824.36704"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.ApiDescription.Server/6.0.5": {},
|
|
||||||
"Microsoft.Extensions.Caching.Abstractions/8.0.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Caching.Memory/8.0.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Caching.Abstractions": "8.0.0",
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
|
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.0",
|
|
||||||
"Microsoft.Extensions.Options": "8.0.0",
|
|
||||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Configuration.Abstractions/8.0.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.DependencyInjection/8.0.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions/8.0.0": {},
|
|
||||||
"Microsoft.Extensions.DependencyModel/8.0.1": {
|
|
||||||
"dependencies": {
|
|
||||||
"System.Text.Encodings.Web": "8.0.0",
|
|
||||||
"System.Text.Json": "8.0.4"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net8.0/Microsoft.Extensions.DependencyModel.dll": {
|
|
||||||
"assemblyVersion": "8.0.0.1",
|
|
||||||
"fileVersion": "8.0.724.31311"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Logging/8.0.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.DependencyInjection": "8.0.0",
|
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.0",
|
|
||||||
"Microsoft.Extensions.Options": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Logging.Abstractions/8.0.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Options/8.0.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
|
|
||||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Primitives/8.0.0": {},
|
|
||||||
"Microsoft.OpenApi/1.6.14": {
|
|
||||||
"runtime": {
|
|
||||||
"lib/netstandard2.0/Microsoft.OpenApi.dll": {
|
|
||||||
"assemblyVersion": "1.6.14.0",
|
|
||||||
"fileVersion": "1.6.14.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Mono.TextTemplating/2.2.1": {
|
|
||||||
"dependencies": {
|
|
||||||
"System.CodeDom": "4.4.0"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/netstandard2.0/Mono.TextTemplating.dll": {
|
|
||||||
"assemblyVersion": "2.2.0.0",
|
|
||||||
"fileVersion": "2.2.1.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Npgsql/8.0.4": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.0"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net8.0/Npgsql.dll": {
|
|
||||||
"assemblyVersion": "8.0.4.0",
|
|
||||||
"fileVersion": "8.0.4.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Npgsql.EntityFrameworkCore.PostgreSQL/8.0.8": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.EntityFrameworkCore": "8.0.8",
|
|
||||||
"Microsoft.EntityFrameworkCore.Abstractions": "8.0.8",
|
|
||||||
"Microsoft.EntityFrameworkCore.Relational": "8.0.8",
|
|
||||||
"Npgsql": "8.0.4"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net8.0/Npgsql.EntityFrameworkCore.PostgreSQL.dll": {
|
|
||||||
"assemblyVersion": "8.0.8.0",
|
|
||||||
"fileVersion": "8.0.8.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Swashbuckle.AspNetCore/6.6.2": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.ApiDescription.Server": "6.0.5",
|
|
||||||
"Swashbuckle.AspNetCore.Swagger": "6.6.2",
|
|
||||||
"Swashbuckle.AspNetCore.SwaggerGen": "6.6.2",
|
|
||||||
"Swashbuckle.AspNetCore.SwaggerUI": "6.6.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Swashbuckle.AspNetCore.Swagger/6.6.2": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.OpenApi": "1.6.14"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net8.0/Swashbuckle.AspNetCore.Swagger.dll": {
|
|
||||||
"assemblyVersion": "6.6.2.0",
|
|
||||||
"fileVersion": "6.6.2.401"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Swashbuckle.AspNetCore.SwaggerGen/6.6.2": {
|
|
||||||
"dependencies": {
|
|
||||||
"Swashbuckle.AspNetCore.Swagger": "6.6.2"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net8.0/Swashbuckle.AspNetCore.SwaggerGen.dll": {
|
|
||||||
"assemblyVersion": "6.6.2.0",
|
|
||||||
"fileVersion": "6.6.2.401"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Swashbuckle.AspNetCore.SwaggerUI/6.6.2": {
|
|
||||||
"runtime": {
|
|
||||||
"lib/net8.0/Swashbuckle.AspNetCore.SwaggerUI.dll": {
|
|
||||||
"assemblyVersion": "6.6.2.0",
|
|
||||||
"fileVersion": "6.6.2.401"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.CodeDom/4.4.0": {
|
|
||||||
"runtime": {
|
|
||||||
"lib/netstandard2.0/System.CodeDom.dll": {
|
|
||||||
"assemblyVersion": "4.0.0.0",
|
|
||||||
"fileVersion": "4.6.25519.3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Collections.Immutable/6.0.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Composition/6.0.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"System.Composition.AttributedModel": "6.0.0",
|
|
||||||
"System.Composition.Convention": "6.0.0",
|
|
||||||
"System.Composition.Hosting": "6.0.0",
|
|
||||||
"System.Composition.Runtime": "6.0.0",
|
|
||||||
"System.Composition.TypedParts": "6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Composition.AttributedModel/6.0.0": {
|
|
||||||
"runtime": {
|
|
||||||
"lib/net6.0/System.Composition.AttributedModel.dll": {
|
|
||||||
"assemblyVersion": "6.0.0.0",
|
|
||||||
"fileVersion": "6.0.21.52210"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Composition.Convention/6.0.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"System.Composition.AttributedModel": "6.0.0"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net6.0/System.Composition.Convention.dll": {
|
|
||||||
"assemblyVersion": "6.0.0.0",
|
|
||||||
"fileVersion": "6.0.21.52210"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Composition.Hosting/6.0.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"System.Composition.Runtime": "6.0.0"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net6.0/System.Composition.Hosting.dll": {
|
|
||||||
"assemblyVersion": "6.0.0.0",
|
|
||||||
"fileVersion": "6.0.21.52210"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Composition.Runtime/6.0.0": {
|
|
||||||
"runtime": {
|
|
||||||
"lib/net6.0/System.Composition.Runtime.dll": {
|
|
||||||
"assemblyVersion": "6.0.0.0",
|
|
||||||
"fileVersion": "6.0.21.52210"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Composition.TypedParts/6.0.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"System.Composition.AttributedModel": "6.0.0",
|
|
||||||
"System.Composition.Hosting": "6.0.0",
|
|
||||||
"System.Composition.Runtime": "6.0.0"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net6.0/System.Composition.TypedParts.dll": {
|
|
||||||
"assemblyVersion": "6.0.0.0",
|
|
||||||
"fileVersion": "6.0.21.52210"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.IO.Pipelines/6.0.3": {},
|
|
||||||
"System.Reflection.Metadata/6.0.1": {
|
|
||||||
"dependencies": {
|
|
||||||
"System.Collections.Immutable": "6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Runtime.CompilerServices.Unsafe/6.0.0": {},
|
|
||||||
"System.Text.Encoding.CodePages/6.0.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Text.Encodings.Web/8.0.0": {},
|
|
||||||
"System.Text.Json/8.0.4": {
|
|
||||||
"dependencies": {
|
|
||||||
"System.Text.Encodings.Web": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Threading.Channels/6.0.0": {},
|
|
||||||
"AdminService.Application/1.0.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"AdminService.Domain": "1.0.0",
|
|
||||||
"AdminService.Infrastructure": "1.0.0"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"AdminService.Application.dll": {
|
|
||||||
"assemblyVersion": "1.0.0",
|
|
||||||
"fileVersion": "1.0.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AdminService.Domain/1.0.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"Shared.Commons": "1.0.0"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"AdminService.Domain.dll": {
|
|
||||||
"assemblyVersion": "1.0.0",
|
|
||||||
"fileVersion": "1.0.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AdminService.Infrastructure/1.0.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"AdminService.Domain": "1.0.0",
|
|
||||||
"Microsoft.EntityFrameworkCore": "8.0.8",
|
|
||||||
"Npgsql.EntityFrameworkCore.PostgreSQL": "8.0.8"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"AdminService.Infrastructure.dll": {
|
|
||||||
"assemblyVersion": "1.0.0",
|
|
||||||
"fileVersion": "1.0.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Shared.Commons/1.0.0": {
|
|
||||||
"runtime": {
|
|
||||||
"Shared.Commons.dll": {
|
|
||||||
"assemblyVersion": "1.0.0",
|
|
||||||
"fileVersion": "1.0.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Shared.Contracts/1.0.0": {
|
|
||||||
"runtime": {
|
|
||||||
"Shared.Contracts.dll": {
|
|
||||||
"assemblyVersion": "1.0.0",
|
|
||||||
"fileVersion": "1.0.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"libraries": {
|
|
||||||
"AdminService.API/1.0.0": {
|
|
||||||
"type": "project",
|
|
||||||
"serviceable": false,
|
|
||||||
"sha512": ""
|
|
||||||
},
|
|
||||||
"Humanizer.Core/2.14.1": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==",
|
|
||||||
"path": "humanizer.core/2.14.1",
|
|
||||||
"hashPath": "humanizer.core.2.14.1.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.AspNetCore.OpenApi/8.0.27": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-lbjwtlQ1ICGKp3UyhoF9i4APpzX01CqtY8vVba2vL32Nm3v5ir9/FJ0PHXfxgsO5sr1Bb4KkCOH7FSauEHUgRQ==",
|
|
||||||
"path": "microsoft.aspnetcore.openapi/8.0.27",
|
|
||||||
"hashPath": "microsoft.aspnetcore.openapi.8.0.27.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.Bcl.AsyncInterfaces/6.0.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==",
|
|
||||||
"path": "microsoft.bcl.asyncinterfaces/6.0.0",
|
|
||||||
"hashPath": "microsoft.bcl.asyncinterfaces.6.0.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.CodeAnalysis.Analyzers/3.3.3": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-j/rOZtLMVJjrfLRlAMckJLPW/1rze9MT1yfWqSIbUPGRu1m1P0fuo9PmqapwsmePfGB5PJrudQLvmUOAMF0DqQ==",
|
|
||||||
"path": "microsoft.codeanalysis.analyzers/3.3.3",
|
|
||||||
"hashPath": "microsoft.codeanalysis.analyzers.3.3.3.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.CodeAnalysis.Common/4.5.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-lwAbIZNdnY0SUNoDmZHkVUwLO8UyNnyyh1t/4XsbFxi4Ounb3xszIYZaWhyj5ZjyfcwqwmtMbE7fUTVCqQEIdQ==",
|
|
||||||
"path": "microsoft.codeanalysis.common/4.5.0",
|
|
||||||
"hashPath": "microsoft.codeanalysis.common.4.5.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.CodeAnalysis.CSharp/4.5.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-cM59oMKAOxvdv76bdmaKPy5hfj+oR+zxikWoueEB7CwTko7mt9sVKZI8Qxlov0C/LuKEG+WQwifepqL3vuTiBQ==",
|
|
||||||
"path": "microsoft.codeanalysis.csharp/4.5.0",
|
|
||||||
"hashPath": "microsoft.codeanalysis.csharp.4.5.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.CodeAnalysis.CSharp.Workspaces/4.5.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-h74wTpmGOp4yS4hj+EvNzEiPgg/KVs2wmSfTZ81upJZOtPkJsVkgfsgtxxqmAeapjT/vLKfmYV0bS8n5MNVP+g==",
|
|
||||||
"path": "microsoft.codeanalysis.csharp.workspaces/4.5.0",
|
|
||||||
"hashPath": "microsoft.codeanalysis.csharp.workspaces.4.5.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.CodeAnalysis.Workspaces.Common/4.5.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-l4dDRmGELXG72XZaonnOeORyD/T5RpEu5LGHOUIhnv+MmUWDY/m1kWXGwtcgQ5CJ5ynkFiRnIYzTKXYjUs7rbw==",
|
|
||||||
"path": "microsoft.codeanalysis.workspaces.common/4.5.0",
|
|
||||||
"hashPath": "microsoft.codeanalysis.workspaces.common.4.5.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.EntityFrameworkCore/8.0.8": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-iK+jrJzkfbIxutB7or808BPmJtjUEi5O+eSM7cLDwsyde6+3iOujCSfWnrHrLxY3u+EQrJD+aD8DJ6ogPA2Rtw==",
|
|
||||||
"path": "microsoft.entityframeworkcore/8.0.8",
|
|
||||||
"hashPath": "microsoft.entityframeworkcore.8.0.8.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.EntityFrameworkCore.Abstractions/8.0.8": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-9mMQkZsfL1c2iifBD8MWRmwy59rvsVtR9NOezJj7+g1j4P7g49MJHd8k8faC/v7d5KuHkQ6KOQiSItvoRt9PXA==",
|
|
||||||
"path": "microsoft.entityframeworkcore.abstractions/8.0.8",
|
|
||||||
"hashPath": "microsoft.entityframeworkcore.abstractions.8.0.8.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.EntityFrameworkCore.Analyzers/8.0.8": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-OlAXMU+VQgLz5y5/SBkLvAa9VeiR3dlJqgIebEEH2M2NGA3evm68/Tv7SLWmSxwnEAtA3nmDEZF2pacK6eXh4Q==",
|
|
||||||
"path": "microsoft.entityframeworkcore.analyzers/8.0.8",
|
|
||||||
"hashPath": "microsoft.entityframeworkcore.analyzers.8.0.8.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.EntityFrameworkCore.Design/8.0.8": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-MmQAMHdjZR8Iyn/FVQrh9weJQTn0HqtKa3vELS9ffQJat/qXgnTam9M9jqvePphjkYp5Scee+Hy+EJR4nmWmOA==",
|
|
||||||
"path": "microsoft.entityframeworkcore.design/8.0.8",
|
|
||||||
"hashPath": "microsoft.entityframeworkcore.design.8.0.8.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.EntityFrameworkCore.Relational/8.0.8": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-3WnrwdXxKg4L98cDx0lNEEau8U2lsfuBJCs0Yzht+5XVTmahboM7MukKfQHAzVsHUPszm6ci929S7Qas0WfVHA==",
|
|
||||||
"path": "microsoft.entityframeworkcore.relational/8.0.8",
|
|
||||||
"hashPath": "microsoft.entityframeworkcore.relational.8.0.8.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.ApiDescription.Server/6.0.5": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-Ckb5EDBUNJdFWyajfXzUIMRkhf52fHZOQuuZg/oiu8y7zDCVwD0iHhew6MnThjHmevanpxL3f5ci2TtHQEN6bw==",
|
|
||||||
"path": "microsoft.extensions.apidescription.server/6.0.5",
|
|
||||||
"hashPath": "microsoft.extensions.apidescription.server.6.0.5.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Caching.Abstractions/8.0.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-3KuSxeHoNYdxVYfg2IRZCThcrlJ1XJqIXkAWikCsbm5C/bCjv7G0WoKDyuR98Q+T607QT2Zl5GsbGRkENcV2yQ==",
|
|
||||||
"path": "microsoft.extensions.caching.abstractions/8.0.0",
|
|
||||||
"hashPath": "microsoft.extensions.caching.abstractions.8.0.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Caching.Memory/8.0.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-7pqivmrZDzo1ADPkRwjy+8jtRKWRCPag9qPI+p7sgu7Q4QreWhcvbiWXsbhP+yY8XSiDvZpu2/LWdBv7PnmOpQ==",
|
|
||||||
"path": "microsoft.extensions.caching.memory/8.0.0",
|
|
||||||
"hashPath": "microsoft.extensions.caching.memory.8.0.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Configuration.Abstractions/8.0.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==",
|
|
||||||
"path": "microsoft.extensions.configuration.abstractions/8.0.0",
|
|
||||||
"hashPath": "microsoft.extensions.configuration.abstractions.8.0.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.DependencyInjection/8.0.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==",
|
|
||||||
"path": "microsoft.extensions.dependencyinjection/8.0.0",
|
|
||||||
"hashPath": "microsoft.extensions.dependencyinjection.8.0.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions/8.0.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==",
|
|
||||||
"path": "microsoft.extensions.dependencyinjection.abstractions/8.0.0",
|
|
||||||
"hashPath": "microsoft.extensions.dependencyinjection.abstractions.8.0.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.DependencyModel/8.0.1": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-5Ou6varcxLBzQ+Agfm0k0pnH7vrEITYlXMDuE6s7ZHlZHz6/G8XJ3iISZDr5rfwfge6RnXJ1+Wc479mMn52vjA==",
|
|
||||||
"path": "microsoft.extensions.dependencymodel/8.0.1",
|
|
||||||
"hashPath": "microsoft.extensions.dependencymodel.8.0.1.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Logging/8.0.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-tvRkov9tAJ3xP51LCv3FJ2zINmv1P8Hi8lhhtcKGqM+ImiTCC84uOPEI4z8Cdq2C3o9e+Aa0Gw0rmrsJD77W+w==",
|
|
||||||
"path": "microsoft.extensions.logging/8.0.0",
|
|
||||||
"hashPath": "microsoft.extensions.logging.8.0.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Logging.Abstractions/8.0.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==",
|
|
||||||
"path": "microsoft.extensions.logging.abstractions/8.0.0",
|
|
||||||
"hashPath": "microsoft.extensions.logging.abstractions.8.0.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Options/8.0.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==",
|
|
||||||
"path": "microsoft.extensions.options/8.0.0",
|
|
||||||
"hashPath": "microsoft.extensions.options.8.0.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Primitives/8.0.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==",
|
|
||||||
"path": "microsoft.extensions.primitives/8.0.0",
|
|
||||||
"hashPath": "microsoft.extensions.primitives.8.0.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.OpenApi/1.6.14": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-tTaBT8qjk3xINfESyOPE2rIellPvB7qpVqiWiyA/lACVvz+xOGiXhFUfohcx82NLbi5avzLW0lx+s6oAqQijfw==",
|
|
||||||
"path": "microsoft.openapi/1.6.14",
|
|
||||||
"hashPath": "microsoft.openapi.1.6.14.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Mono.TextTemplating/2.2.1": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-KZYeKBET/2Z0gY1WlTAK7+RHTl7GSbtvTLDXEZZojUdAPqpQNDL6tHv7VUpqfX5VEOh+uRGKaZXkuD253nEOBQ==",
|
|
||||||
"path": "mono.texttemplating/2.2.1",
|
|
||||||
"hashPath": "mono.texttemplating.2.2.1.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Npgsql/8.0.4": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-vaYEUlF/pB9m8bs21wQv3Da0kMHT4A9USe47VfY/L2BO97xz5KfIxhEu22QS9d68ZrLxvtL3wQDfDLPr2OjbjA==",
|
|
||||||
"path": "npgsql/8.0.4",
|
|
||||||
"hashPath": "npgsql.8.0.4.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Npgsql.EntityFrameworkCore.PostgreSQL/8.0.8": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-D5WWJZJTgZYUmGv66BARXbTlinp2a5f5RueJqGYoHuWJw02J0i2va/RA+8N4A5hLORK5YKMRqXhFWtKsZdrksw==",
|
|
||||||
"path": "npgsql.entityframeworkcore.postgresql/8.0.8",
|
|
||||||
"hashPath": "npgsql.entityframeworkcore.postgresql.8.0.8.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Swashbuckle.AspNetCore/6.6.2": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-+NB4UYVYN6AhDSjW0IJAd1AGD8V33gemFNLPaxKTtPkHB+HaKAKf9MGAEUPivEWvqeQfcKIw8lJaHq6LHljRuw==",
|
|
||||||
"path": "swashbuckle.aspnetcore/6.6.2",
|
|
||||||
"hashPath": "swashbuckle.aspnetcore.6.6.2.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Swashbuckle.AspNetCore.Swagger/6.6.2": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-ovgPTSYX83UrQUWiS5vzDcJ8TEX1MAxBgDFMK45rC24MorHEPQlZAHlaXj/yth4Zf6xcktpUgTEBvffRQVwDKA==",
|
|
||||||
"path": "swashbuckle.aspnetcore.swagger/6.6.2",
|
|
||||||
"hashPath": "swashbuckle.aspnetcore.swagger.6.6.2.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Swashbuckle.AspNetCore.SwaggerGen/6.6.2": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-zv4ikn4AT1VYuOsDCpktLq4QDq08e7Utzbir86M5/ZkRaLXbCPF11E1/vTmOiDzRTl0zTZINQU2qLKwTcHgfrA==",
|
|
||||||
"path": "swashbuckle.aspnetcore.swaggergen/6.6.2",
|
|
||||||
"hashPath": "swashbuckle.aspnetcore.swaggergen.6.6.2.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Swashbuckle.AspNetCore.SwaggerUI/6.6.2": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-mBBb+/8Hm2Q3Wygag+hu2jj69tZW5psuv0vMRXY07Wy+Rrj40vRP8ZTbKBhs91r45/HXT4aY4z0iSBYx1h6JvA==",
|
|
||||||
"path": "swashbuckle.aspnetcore.swaggerui/6.6.2",
|
|
||||||
"hashPath": "swashbuckle.aspnetcore.swaggerui.6.6.2.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"System.CodeDom/4.4.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-2sCCb7doXEwtYAbqzbF/8UAeDRMNmPaQbU2q50Psg1J9KzumyVVCgKQY8s53WIPTufNT0DpSe9QRvVjOzfDWBA==",
|
|
||||||
"path": "system.codedom/4.4.0",
|
|
||||||
"hashPath": "system.codedom.4.4.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"System.Collections.Immutable/6.0.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-l4zZJ1WU2hqpQQHXz1rvC3etVZN+2DLmQMO79FhOTZHMn8tDRr+WU287sbomD0BETlmKDn0ygUgVy9k5xkkJdA==",
|
|
||||||
"path": "system.collections.immutable/6.0.0",
|
|
||||||
"hashPath": "system.collections.immutable.6.0.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"System.Composition/6.0.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-d7wMuKQtfsxUa7S13tITC8n1cQzewuhD5iDjZtK2prwFfKVzdYtgrTHgjaV03Zq7feGQ5gkP85tJJntXwInsJA==",
|
|
||||||
"path": "system.composition/6.0.0",
|
|
||||||
"hashPath": "system.composition.6.0.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"System.Composition.AttributedModel/6.0.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-WK1nSDLByK/4VoC7fkNiFuTVEiperuCN/Hyn+VN30R+W2ijO1d0Z2Qm0ScEl9xkSn1G2MyapJi8xpf4R8WRa/w==",
|
|
||||||
"path": "system.composition.attributedmodel/6.0.0",
|
|
||||||
"hashPath": "system.composition.attributedmodel.6.0.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"System.Composition.Convention/6.0.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-XYi4lPRdu5bM4JVJ3/UIHAiG6V6lWWUlkhB9ab4IOq0FrRsp0F4wTyV4Dj+Ds+efoXJ3qbLqlvaUozDO7OLeXA==",
|
|
||||||
"path": "system.composition.convention/6.0.0",
|
|
||||||
"hashPath": "system.composition.convention.6.0.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"System.Composition.Hosting/6.0.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-w/wXjj7kvxuHPLdzZ0PAUt++qJl03t7lENmb2Oev0n3zbxyNULbWBlnd5J5WUMMv15kg5o+/TCZFb6lSwfaUUQ==",
|
|
||||||
"path": "system.composition.hosting/6.0.0",
|
|
||||||
"hashPath": "system.composition.hosting.6.0.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"System.Composition.Runtime/6.0.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-qkRH/YBaMPTnzxrS5RDk1juvqed4A6HOD/CwRcDGyPpYps1J27waBddiiq1y93jk2ZZ9wuA/kynM+NO0kb3PKg==",
|
|
||||||
"path": "system.composition.runtime/6.0.0",
|
|
||||||
"hashPath": "system.composition.runtime.6.0.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"System.Composition.TypedParts/6.0.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-iUR1eHrL8Cwd82neQCJ00MpwNIBs4NZgXzrPqx8NJf/k4+mwBO0XCRmHYJT4OLSwDDqh5nBLJWkz5cROnrGhRA==",
|
|
||||||
"path": "system.composition.typedparts/6.0.0",
|
|
||||||
"hashPath": "system.composition.typedparts.6.0.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"System.IO.Pipelines/6.0.3": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-ryTgF+iFkpGZY1vRQhfCzX0xTdlV3pyaTTqRu2ETbEv+HlV7O6y7hyQURnghNIXvctl5DuZ//Dpks6HdL/Txgw==",
|
|
||||||
"path": "system.io.pipelines/6.0.3",
|
|
||||||
"hashPath": "system.io.pipelines.6.0.3.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"System.Reflection.Metadata/6.0.1": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-III/lNMSn0ZRBuM9m5Cgbiho5j81u0FAEagFX5ta2DKbljZ3T0IpD8j+BIiHQPeKqJppWS9bGEp6JnKnWKze0g==",
|
|
||||||
"path": "system.reflection.metadata/6.0.1",
|
|
||||||
"hashPath": "system.reflection.metadata.6.0.1.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"System.Runtime.CompilerServices.Unsafe/6.0.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==",
|
|
||||||
"path": "system.runtime.compilerservices.unsafe/6.0.0",
|
|
||||||
"hashPath": "system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"System.Text.Encoding.CodePages/6.0.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-ZFCILZuOvtKPauZ/j/swhvw68ZRi9ATCfvGbk1QfydmcXBkIWecWKn/250UH7rahZ5OoDBaiAudJtPvLwzw85A==",
|
|
||||||
"path": "system.text.encoding.codepages/6.0.0",
|
|
||||||
"hashPath": "system.text.encoding.codepages.6.0.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"System.Text.Encodings.Web/8.0.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==",
|
|
||||||
"path": "system.text.encodings.web/8.0.0",
|
|
||||||
"hashPath": "system.text.encodings.web.8.0.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"System.Text.Json/8.0.4": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-bAkhgDJ88XTsqczoxEMliSrpijKZHhbJQldhAmObj/RbrN3sU5dcokuXmWJWsdQAhiMJ9bTayWsL1C9fbbCRhw==",
|
|
||||||
"path": "system.text.json/8.0.4",
|
|
||||||
"hashPath": "system.text.json.8.0.4.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"System.Threading.Channels/6.0.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-TY8/9+tI0mNaUMgntOxxaq2ndTkdXqLSxvPmas7XEqOlv9lQtB7wLjYGd756lOaO7Dvb5r/WXhluM+0Xe87v5Q==",
|
|
||||||
"path": "system.threading.channels/6.0.0",
|
|
||||||
"hashPath": "system.threading.channels.6.0.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"AdminService.Application/1.0.0": {
|
|
||||||
"type": "project",
|
|
||||||
"serviceable": false,
|
|
||||||
"sha512": ""
|
|
||||||
},
|
|
||||||
"AdminService.Domain/1.0.0": {
|
|
||||||
"type": "project",
|
|
||||||
"serviceable": false,
|
|
||||||
"sha512": ""
|
|
||||||
},
|
|
||||||
"AdminService.Infrastructure/1.0.0": {
|
|
||||||
"type": "project",
|
|
||||||
"serviceable": false,
|
|
||||||
"sha512": ""
|
|
||||||
},
|
|
||||||
"Shared.Commons/1.0.0": {
|
|
||||||
"type": "project",
|
|
||||||
"serviceable": false,
|
|
||||||
"sha512": ""
|
|
||||||
},
|
|
||||||
"Shared.Contracts/1.0.0": {
|
|
||||||
"type": "project",
|
|
||||||
"serviceable": false,
|
|
||||||
"sha512": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"runtimeOptions": {
|
|
||||||
"tfm": "net8.0",
|
|
||||||
"frameworks": [
|
|
||||||
{
|
|
||||||
"name": "Microsoft.NETCore.App",
|
|
||||||
"version": "8.0.0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Microsoft.AspNetCore.App",
|
|
||||||
"version": "8.0.0"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"configProperties": {
|
|
||||||
"System.GC.Server": true,
|
|
||||||
"System.Reflection.NullabilityInfoContext.IsSupported": true,
|
|
||||||
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"ConnectionStrings": {
|
|
||||||
"DefaultConnection": "Host=192.168.0.111;Port=7925;Database=ocr;Username=postgres;Password=M@tr!x#149@dm!N"
|
|
||||||
},
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AllowedHosts": "*"
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user