|
|
|
|
@ -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/<path:filename>") |
|
|
|
|
@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"<b>Caption:</b> {caption}", styles["Normal"])) |
|
|
|
|
if notes: |
|
|
|
|
elements.append(Paragraph(f"<b>Notes:</b> {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")) |
|
|
|
|
|