diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index d1509fa..5725750 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -1,5 +1,18 @@ # OTB Cloud Project State +## v2.1.0 - 20260502-005134 + +Feature release: PDF Report Workshop (interactive document builder). + +- Added PDF Report Workshop accessible from Archive Workspace. +- Users can view staged images and add per-image captions and notes. +- Added report-level metadata (job title, customer, address, technician, date). +- Added secure staged image preview route for workshop UI. +- PDF output now supports structured job reports, not just image dumps. + +--- + + ## v2.0.0 - 20260501-063031 Milestone release: Archive Workspace now supports PDF Job Report generation. diff --git a/README.md b/README.md index f8db635..c853e56 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,17 @@ # OTB Cloud +## v2.1.0 - 20260502-005134 + +PDF Report Workshop introduced. + +- Build structured job reports from staged images. +- Add captions, notes, and job metadata. +- Preview images securely inside the workshop. +- Generate client-ready PDF reports. + +--- + + ## v2.0.0 - 20260501-063031 Major milestone release. diff --git a/VERSION b/VERSION index 46b105a..1defe53 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v2.0.0 +v2.1.0 diff --git a/app/main/routes.py b/app/main/routes.py index 21b440b..dd02af4 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -13,7 +13,7 @@ import re import hashlib from werkzeug.utils import secure_filename -from flask import Blueprint, flash, redirect, render_template, request, session, url_for, current_app, send_file, jsonify +from flask import send_file, Blueprint, flash, redirect, render_template, request, session, url_for, current_app, send_file, jsonify from app.db import get_db from app.auth.utils import create_device_directories, remove_device_directories, slugify_device_name, compute_sha256 @@ -2412,3 +2412,108 @@ def image_process(): }) return jsonify({"ok": True, "processed": processed}) + +@bp.route("/workspace/pdf-report/preview/") +@portal_session_required +def pdf_report_preview(filename: str): + tenant_root = _tenant_root() + staging_dir = tenant_root / "zip_staging" + file_path = staging_dir / filename + + try: + file_path = file_path.resolve() + staging_resolved = staging_dir.resolve() + if staging_resolved not in file_path.parents and file_path != staging_resolved: + abort(403) + if not file_path.exists() or not file_path.is_file(): + abort(404) + if file_path.suffix.lower() not in [".jpg", ".jpeg", ".png", ".webp"]: + abort(404) + + return send_file(file_path) + except Exception: + abort(404) + +@bp.route("/workspace/pdf-report") +@portal_session_required +def pdf_report_workshop(): + tenant_root = _tenant_root() + staging_dir = tenant_root / "zip_staging" + staging_dir.mkdir(parents=True, exist_ok=True) + + staged_files = [] + for p in sorted(staging_dir.iterdir(), key=lambda x: x.name.lower()): + if p.is_file() and p.suffix.lower() in [".jpg", ".jpeg", ".png", ".webp"]: + staged_files.append({ + "name": p.name, + "path": str(p), + }) + + return render_template( + "cloud/pdf_report_workshop.html", + staged_files=staged_files + ) + + +@bp.route("/workspace/pdf-report/create", methods=["POST"]) +@portal_session_required +def create_pdf_report(): + tenant_root = _tenant_root() + staging_dir = tenant_root / "zip_staging" + exports_dir = tenant_root / "exports" + exports_dir.mkdir(parents=True, exist_ok=True) + + archive_name = (request.form.get("report_name") or "").strip() + if not archive_name: + archive_name = f"otb-report-{datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S')}" + + archive_name = re.sub(r"[^A-Za-z0-9._-]+", "_", archive_name) + pdf_path = exports_dir / f"{archive_name}.pdf" + + doc = SimpleDocTemplate(str(pdf_path), pagesize=letter) + styles = getSampleStyleSheet() + elements = [] + + # Report metadata + elements.append(Paragraph(request.form.get("job_title", "Job Report"), styles["Title"])) + elements.append(Spacer(1, 12)) + elements.append(Paragraph(f"Customer: {request.form.get('customer', '')}", styles["Normal"])) + elements.append(Paragraph(f"Address: {request.form.get('address', '')}", styles["Normal"])) + elements.append(Paragraph(f"Technician: {request.form.get('technician', '')}", styles["Normal"])) + elements.append(Paragraph(f"Date: {request.form.get('date', '')}", styles["Normal"])) + elements.append(Spacer(1, 24)) + + staged = sorted([p for p in staging_dir.iterdir() if p.is_file()]) + + for idx, p in enumerate(staged): + if p.suffix.lower() not in [".jpg", ".jpeg", ".png", ".webp"]: + continue + + caption = request.form.get(f"caption_{idx}", "") + notes = request.form.get(f"notes_{idx}", "") + + elements.append(Paragraph(p.name, styles["Heading3"])) + elements.append(Spacer(1, 6)) + + try: + with PILImage.open(p) as im: + w, h = im.size + scale = min(500 / w, 600 / h, 1.0) + elements.append(ReportLabImage(str(p), width=w*scale, height=h*scale)) + except: + elements.append(Paragraph("Image failed to load", styles["Normal"])) + + elements.append(Spacer(1, 8)) + + if caption: + elements.append(Paragraph(f"Caption: {caption}", styles["Normal"])) + if notes: + elements.append(Paragraph(f"Notes: {notes}", styles["Normal"])) + + elements.append(Spacer(1, 18)) + elements.append(PageBreak()) + + doc.build(elements) + + flash(f"PDF Report created: {archive_name}.pdf", "success") + return redirect(url_for("main.zip_workspace")) diff --git a/app/templates/cloud/pdf_report_workshop.html b/app/templates/cloud/pdf_report_workshop.html new file mode 100644 index 0000000..3b770ff --- /dev/null +++ b/app/templates/cloud/pdf_report_workshop.html @@ -0,0 +1,38 @@ +{% extends "portal_base.html" %} + +{% block title %}PDF Report Workshop{% endblock %} + +{% block portal_content %} +

PDF Report Workshop

+ +
+ +

Report Info

+
+
+
+
+
+
+ +
+ +

Images

+ +{% for file in staged_files %} +
+
+ {{ file.name }}
+ + Caption:
+
+ + Notes:
+ +
+{% endfor %} + + + +
+{% endblock %} diff --git a/app/templates/cloud/zip_workspace.html b/app/templates/cloud/zip_workspace.html index ffc0126..49bcdbf 100644 --- a/app/templates/cloud/zip_workspace.html +++ b/app/templates/cloud/zip_workspace.html @@ -14,6 +14,7 @@
+ Open PDF Report Workshop Back to Dashboard View LTS
@@ -73,10 +74,6 @@ PDF — Job Report (images only) -