Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import json | |
| from pathlib import Path | |
| from typing import List | |
| from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile, status | |
| from fastapi.responses import FileResponse | |
| from openpyxl import Workbook | |
| from ..deps import get_session_store | |
| from ..schemas import ( | |
| HeadingsRequest, | |
| HeadingsResponse, | |
| PageTemplatesRequest, | |
| PageTemplatesResponse, | |
| PagesRequest, | |
| PagesResponse, | |
| SectionsRequest, | |
| SectionsResponse, | |
| SelectionRequest, | |
| SessionResponse, | |
| SessionStatusResponse, | |
| ) | |
| from ...services import SessionStore | |
| from ...services.session_store import DATA_EXTS, IMAGE_EXTS | |
| from ...services.data_import import ( | |
| populate_session_from_data_files, | |
| reconcile_session_image_links, | |
| ) | |
| from ...services.pdf_reportlab import render_report_pdf | |
| router = APIRouter() | |
| def _normalize_session_id(session_id: str, store: SessionStore) -> str: | |
| try: | |
| return store.validate_session_id(session_id) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail=str(exc)) from exc | |
| def _attach_urls(session: dict) -> dict: | |
| session = dict(session) | |
| uploads = {} | |
| for category, items in (session.get("uploads") or {}).items(): | |
| enriched = [] | |
| for item in items: | |
| enriched.append( | |
| { | |
| **item, | |
| "url": f"/api/sessions/{session['id']}/uploads/{item['id']}", | |
| } | |
| ) | |
| uploads[category] = enriched | |
| session["uploads"] = uploads | |
| return session | |
| def _merge_text(primary: str, secondary: str) -> str: | |
| primary = (primary or "").strip() | |
| secondary = (secondary or "").strip() | |
| if not secondary: | |
| return primary | |
| if not primary: | |
| return secondary | |
| if secondary in primary: | |
| return primary | |
| return f"{primary} - {secondary}" | |
| def list_sessions(store: SessionStore = Depends(get_session_store)) -> List[SessionResponse]: | |
| sessions = store.list_sessions() | |
| sessions.sort(key=lambda item: item.get("created_at", ""), reverse=True) | |
| return [_attach_urls(session) for session in sessions] | |
| def delete_session( | |
| session_id: str, store: SessionStore = Depends(get_session_store) | |
| ) -> None: | |
| session_id = _normalize_session_id(session_id, store) | |
| deleted = store.delete_session(session_id) | |
| if not deleted: | |
| raise HTTPException(status_code=404, detail="Session not found.") | |
| def create_session( | |
| document_no: str = Form(""), | |
| inspection_date: str = Form(""), | |
| files: List[UploadFile] = File(...), | |
| store: SessionStore = Depends(get_session_store), | |
| ) -> SessionResponse: | |
| if not files: | |
| raise HTTPException(status_code=400, detail="At least one file is required.") | |
| session = store.create_session(document_no, inspection_date) | |
| saved_files = [] | |
| for upload in files: | |
| try: | |
| saved_files.append(store.save_upload(session["id"], upload)) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=413, detail=str(exc)) from exc | |
| finally: | |
| try: | |
| upload.file.close() | |
| except Exception: | |
| pass | |
| session = store.add_uploads(session, saved_files) | |
| try: | |
| session = populate_session_from_data_files(store, session) | |
| session = reconcile_session_image_links(store, session) | |
| except Exception: | |
| # Do not block upload if data parsing fails. | |
| pass | |
| return _attach_urls(session) | |
| def get_session(session_id: str, store: SessionStore = Depends(get_session_store)) -> SessionResponse: | |
| session_id = _normalize_session_id(session_id, store) | |
| session = store.get_session(session_id) | |
| if not session: | |
| raise HTTPException(status_code=404, detail="Session not found.") | |
| store.ensure_sections(session) | |
| try: | |
| session = reconcile_session_image_links(store, session) | |
| except Exception: | |
| pass | |
| return _attach_urls(session) | |
| def get_session_status( | |
| session_id: str, store: SessionStore = Depends(get_session_store) | |
| ) -> SessionStatusResponse: | |
| session_id = _normalize_session_id(session_id, store) | |
| session = store.get_session(session_id) | |
| if not session: | |
| raise HTTPException(status_code=404, detail="Session not found.") | |
| return SessionStatusResponse( | |
| id=session["id"], status=session["status"], updated_at=session["updated_at"] | |
| ) | |
| def update_selection( | |
| session_id: str, | |
| payload: SelectionRequest, | |
| store: SessionStore = Depends(get_session_store), | |
| ) -> SessionResponse: | |
| session_id = _normalize_session_id(session_id, store) | |
| session = store.get_session(session_id) | |
| if not session: | |
| raise HTTPException(status_code=404, detail="Session not found.") | |
| session = store.set_selected_photos(session, payload.selected_photo_ids) | |
| return _attach_urls(session) | |
| def get_pages(session_id: str, store: SessionStore = Depends(get_session_store)) -> PagesResponse: | |
| session_id = _normalize_session_id(session_id, store) | |
| session = store.get_session(session_id) | |
| if not session: | |
| raise HTTPException(status_code=404, detail="Session not found.") | |
| try: | |
| session = reconcile_session_image_links(store, session) | |
| except Exception: | |
| pass | |
| pages = store.ensure_pages(session) | |
| return PagesResponse(pages=pages) | |
| def save_pages( | |
| session_id: str, | |
| payload: PagesRequest, | |
| store: SessionStore = Depends(get_session_store), | |
| ) -> PagesResponse: | |
| session_id = _normalize_session_id(session_id, store) | |
| session = store.get_session(session_id) | |
| if not session: | |
| raise HTTPException(status_code=404, detail="Session not found.") | |
| session = store.set_pages(session, payload.pages) | |
| return PagesResponse(pages=session.get("pages") or []) | |
| def get_sections( | |
| session_id: str, store: SessionStore = Depends(get_session_store) | |
| ) -> SectionsResponse: | |
| session_id = _normalize_session_id(session_id, store) | |
| session = store.get_session(session_id) | |
| if not session: | |
| raise HTTPException(status_code=404, detail="Session not found.") | |
| try: | |
| session = reconcile_session_image_links(store, session) | |
| except Exception: | |
| pass | |
| sections = store.ensure_sections(session) | |
| return SectionsResponse(sections=sections) | |
| def save_sections( | |
| session_id: str, | |
| payload: SectionsRequest, | |
| store: SessionStore = Depends(get_session_store), | |
| ) -> SectionsResponse: | |
| session_id = _normalize_session_id(session_id, store) | |
| session = store.get_session(session_id) | |
| if not session: | |
| raise HTTPException(status_code=404, detail="Session not found.") | |
| session = store.set_sections(session, payload.sections) | |
| return SectionsResponse(sections=session.get("jobsheet_sections") or []) | |
| def save_headings( | |
| session_id: str, | |
| payload: HeadingsRequest, | |
| store: SessionStore = Depends(get_session_store), | |
| ) -> HeadingsResponse: | |
| session_id = _normalize_session_id(session_id, store) | |
| session = store.get_session(session_id) | |
| if not session: | |
| raise HTTPException(status_code=404, detail="Session not found.") | |
| session = store.set_headings(session, payload.headings) | |
| return HeadingsResponse(headings=session.get("headings") or []) | |
| def save_page_templates( | |
| session_id: str, | |
| payload: PageTemplatesRequest, | |
| store: SessionStore = Depends(get_session_store), | |
| ) -> PageTemplatesResponse: | |
| session_id = _normalize_session_id(session_id, store) | |
| session = store.get_session(session_id) | |
| if not session: | |
| raise HTTPException(status_code=404, detail="Session not found.") | |
| session = store.set_page_templates(session, payload.page_templates) | |
| return PageTemplatesResponse(page_templates=session.get("page_templates") or []) | |
| def get_upload( | |
| session_id: str, | |
| file_id: str, | |
| store: SessionStore = Depends(get_session_store), | |
| ) -> FileResponse: | |
| session_id = _normalize_session_id(session_id, store) | |
| session = store.get_session(session_id) | |
| if not session: | |
| raise HTTPException(status_code=404, detail="Session not found.") | |
| path = store.resolve_upload_path(session, file_id) | |
| if not path or not path.exists(): | |
| raise HTTPException(status_code=404, detail="File not found.") | |
| return FileResponse(path) | |
| def upload_data_file( | |
| session_id: str, | |
| file: UploadFile = File(...), | |
| store: SessionStore = Depends(get_session_store), | |
| ) -> SessionResponse: | |
| session_id = _normalize_session_id(session_id, store) | |
| session = store.get_session(session_id) | |
| if not session: | |
| raise HTTPException(status_code=404, detail="Session not found.") | |
| filename = (file.filename or "").lower() | |
| if filename and Path(filename).suffix.lower() not in DATA_EXTS: | |
| raise HTTPException(status_code=400, detail="Unsupported data file type.") | |
| try: | |
| saved_file = store.save_upload(session_id, file) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=413, detail=str(exc)) from exc | |
| finally: | |
| try: | |
| file.file.close() | |
| except Exception: | |
| pass | |
| session = store.add_uploads(session, [saved_file]) | |
| try: | |
| session = populate_session_from_data_files(store, session) | |
| session = reconcile_session_image_links(store, session) | |
| except Exception: | |
| pass | |
| return _attach_urls(session) | |
| def upload_photo( | |
| session_id: str, | |
| file: UploadFile = File(...), | |
| store: SessionStore = Depends(get_session_store), | |
| ) -> SessionResponse: | |
| session_id = _normalize_session_id(session_id, store) | |
| session = store.get_session(session_id) | |
| if not session: | |
| raise HTTPException(status_code=404, detail="Session not found.") | |
| filename = file.filename or "" | |
| if Path(filename).suffix.lower() not in IMAGE_EXTS: | |
| raise HTTPException(status_code=400, detail="Unsupported image file type.") | |
| try: | |
| saved_file = store.save_upload(session_id, file) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=413, detail=str(exc)) from exc | |
| finally: | |
| try: | |
| file.file.close() | |
| except Exception: | |
| pass | |
| session = store.add_uploads(session, [saved_file]) | |
| try: | |
| session = reconcile_session_image_links(store, session) | |
| except Exception: | |
| pass | |
| return _attach_urls(session) | |
| def import_json( | |
| session_id: str, | |
| file: UploadFile = File(...), | |
| store: SessionStore = Depends(get_session_store), | |
| ) -> SessionResponse: | |
| session_id = _normalize_session_id(session_id, store) | |
| session = store.get_session(session_id) | |
| if not session: | |
| raise HTTPException(status_code=404, detail="Session not found.") | |
| try: | |
| payload = json.loads(file.file.read()) | |
| except Exception as exc: | |
| raise HTTPException(status_code=400, detail="Invalid JSON file.") from exc | |
| finally: | |
| try: | |
| file.file.close() | |
| except Exception: | |
| pass | |
| imported_session = payload.get("session") if isinstance(payload, dict) else None | |
| pages = payload.get("pages") if isinstance(payload, dict) else None | |
| sections = payload.get("sections") if isinstance(payload, dict) else None | |
| if not sections and isinstance(imported_session, dict): | |
| sections = imported_session.get("jobsheet_sections") | |
| if not pages and isinstance(imported_session, dict): | |
| pages = imported_session.get("pages") | |
| if sections: | |
| session = store.set_sections(session, sections) | |
| if pages: | |
| session = store.set_pages(session, pages) | |
| if isinstance(imported_session, dict): | |
| document_no = _merge_text( | |
| imported_session.get("document_no", ""), | |
| imported_session.get("project_name", ""), | |
| ) | |
| if document_no: | |
| session["document_no"] = document_no | |
| if imported_session.get("inspection_date") is not None: | |
| session["inspection_date"] = imported_session["inspection_date"] | |
| if imported_session.get("selected_photo_ids") is not None: | |
| session["selected_photo_ids"] = imported_session["selected_photo_ids"] | |
| if imported_session.get("page_count") is not None: | |
| session["page_count"] = imported_session["page_count"] | |
| if imported_session.get("headings") is not None: | |
| session["headings"] = imported_session["headings"] | |
| if imported_session.get("page_templates") is not None: | |
| session["page_templates"] = imported_session["page_templates"] | |
| store.update_session(session) | |
| return _attach_urls(session) | |
| def export_package( | |
| session_id: str, store: SessionStore = Depends(get_session_store) | |
| ) -> FileResponse: | |
| session_id = _normalize_session_id(session_id, store) | |
| session = store.get_session(session_id) | |
| if not session: | |
| raise HTTPException(status_code=404, detail="Session not found.") | |
| sections = store.ensure_sections(session) | |
| pages = store.ensure_pages(session) | |
| export_path = Path(store.session_dir(session_id)) / "export.json" | |
| payload = { | |
| "session": session, | |
| "pages": pages, | |
| "sections": sections, | |
| "exported_at": session.get("updated_at"), | |
| } | |
| export_path.write_text( | |
| json.dumps(payload, indent=2), encoding="utf-8" | |
| ) | |
| return FileResponse( | |
| export_path, | |
| media_type="application/json", | |
| filename=f"repex_report_{session_id}.json", | |
| ) | |
| def export_pdf( | |
| session_id: str, store: SessionStore = Depends(get_session_store) | |
| ) -> FileResponse: | |
| session_id = _normalize_session_id(session_id, store) | |
| session = store.get_session(session_id) | |
| if not session: | |
| raise HTTPException(status_code=404, detail="Session not found.") | |
| try: | |
| session = reconcile_session_image_links(store, session) | |
| except Exception: | |
| pass | |
| sections = store.ensure_sections(session) | |
| export_path = Path(store.session_dir(session_id)) / "export.pdf" | |
| render_report_pdf(store, session, sections, export_path) | |
| return FileResponse( | |
| export_path, | |
| media_type="application/pdf", | |
| filename=f"repex_report_{session_id}.pdf", | |
| ) | |
| def export_excel( | |
| session_id: str, store: SessionStore = Depends(get_session_store) | |
| ) -> FileResponse: | |
| session_id = _normalize_session_id(session_id, store) | |
| session = store.get_session(session_id) | |
| if not session: | |
| raise HTTPException(status_code=404, detail="Session not found.") | |
| pages = store.ensure_pages(session) | |
| first_template = (pages[0].get("template") or {}) if pages else {} | |
| wb = Workbook() | |
| ws_general = wb.active | |
| ws_general.title = "General Information" | |
| ws_general.append(["Document No", session.get("document_no", "")]) | |
| ws_general.append(["Inspection Date", session.get("inspection_date", "")]) | |
| ws_general.append(["Inspector", first_template.get("inspector", "")]) | |
| ws_general.append(["Company Logo Image Name", first_template.get("company_logo", "")]) | |
| ws_headings = wb.create_sheet("Headings") | |
| ws_headings.append(["Heading Number", "Heading Name"]) | |
| headings = session.get("headings") or [] | |
| if isinstance(headings, dict): | |
| headings = [{"number": key, "name": value} for key, value in headings.items()] | |
| for heading in headings: | |
| if not isinstance(heading, dict): | |
| continue | |
| ws_headings.append( | |
| [heading.get("number", ""), heading.get("name", "")] | |
| ) | |
| ws_items = wb.create_sheet("Item Spesific") | |
| ws_items.append( | |
| [ | |
| "REF", | |
| "Area", | |
| "Location", | |
| "Item Description", | |
| "Category", | |
| "Priority", | |
| "Required Action", | |
| "Figure Caption", | |
| "Image Name 1", | |
| "Image Name 2", | |
| "Image Name 3", | |
| "Image Name 4", | |
| "Image Name 5", | |
| "Image Name 6", | |
| ] | |
| ) | |
| upload_lookup = { | |
| item.get("id"): item.get("name") | |
| for item in (session.get("uploads") or {}).get("photos", []) | |
| } | |
| for page in pages: | |
| template = page.get("template") or {} | |
| photo_names = [ | |
| upload_lookup.get(photo_id, "") | |
| for photo_id in (page.get("photo_ids") or []) | |
| ] | |
| while len(photo_names) < 6: | |
| photo_names.append("") | |
| ws_items.append( | |
| [ | |
| template.get("reference", ""), | |
| template.get("area", ""), | |
| template.get("functional_location", ""), | |
| template.get("item_description", ""), | |
| template.get("category", ""), | |
| template.get("priority", ""), | |
| template.get("required_action", ""), | |
| template.get("figure_caption", ""), | |
| *photo_names[:6], | |
| ] | |
| ) | |
| export_path = Path(store.session_dir(session_id)) / "export.xlsx" | |
| wb.save(export_path) | |
| return FileResponse( | |
| export_path, | |
| media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", | |
| filename=f"repex_report_{session_id}.xlsx", | |
| ) | |