Spaces:
Sleeping
Sleeping
Merge branch 'dev'
Browse files- frontend/src/components/JobSheetTemplate.tsx +14 -14
- frontend/src/components/PdfPreviewPanel.tsx +1 -0
- frontend/src/components/report-editor.js +29 -29
- frontend/src/pages/EditReportPage.tsx +3 -1
- server/app/services/pdf_export.py +17 -34
- server/app/services/pdf_reportlab.py +15 -15
- server/app/services/session_store.py +26 -0
frontend/src/components/JobSheetTemplate.tsx
CHANGED
|
@@ -36,37 +36,37 @@ type RatingTone = {
|
|
| 36 |
|
| 37 |
const CATEGORY_SCALE: Record<string, RatingTone> = {
|
| 38 |
"0": {
|
| 39 |
-
label: "100% Original Strength",
|
| 40 |
bg: "bg-green-100",
|
| 41 |
text: "text-green-800",
|
| 42 |
border: "border-green-200",
|
| 43 |
},
|
| 44 |
"1": {
|
| 45 |
-
label: "100% Original Strength",
|
| 46 |
bg: "bg-green-200",
|
| 47 |
text: "text-green-800",
|
| 48 |
border: "border-green-200",
|
| 49 |
},
|
| 50 |
"2": {
|
| 51 |
-
label: "95-100% Original Strength",
|
| 52 |
bg: "bg-yellow-100",
|
| 53 |
text: "text-yellow-800",
|
| 54 |
border: "border-yellow-200",
|
| 55 |
},
|
| 56 |
"3": {
|
| 57 |
-
label: "75-95% Original Strength",
|
| 58 |
bg: "bg-yellow-200",
|
| 59 |
text: "text-yellow-800",
|
| 60 |
border: "border-yellow-200",
|
| 61 |
},
|
| 62 |
"4": {
|
| 63 |
-
label: "50-75% Original Strength",
|
| 64 |
bg: "bg-orange-200",
|
| 65 |
text: "text-orange-800",
|
| 66 |
border: "border-orange-200",
|
| 67 |
},
|
| 68 |
"5": {
|
| 69 |
-
label: "<50% Original Strength",
|
| 70 |
bg: "bg-red-200",
|
| 71 |
text: "text-red-800",
|
| 72 |
border: "border-red-200",
|
|
@@ -75,31 +75,31 @@ const CATEGORY_SCALE: Record<string, RatingTone> = {
|
|
| 75 |
|
| 76 |
const PRIORITY_SCALE: Record<string, RatingTone> = {
|
| 77 |
"1": {
|
| 78 |
-
label: "Immediate",
|
| 79 |
bg: "bg-red-200",
|
| 80 |
text: "text-red-800",
|
| 81 |
border: "border-red-200",
|
| 82 |
},
|
| 83 |
"2": {
|
| 84 |
-
label: "1 Year",
|
| 85 |
bg: "bg-orange-200",
|
| 86 |
text: "text-orange-800",
|
| 87 |
border: "border-orange-200",
|
| 88 |
},
|
| 89 |
"3": {
|
| 90 |
-
label: "3 Years",
|
| 91 |
bg: "bg-green-200",
|
| 92 |
text: "text-green-800",
|
| 93 |
border: "border-green-200",
|
| 94 |
},
|
| 95 |
X: {
|
| 96 |
-
label: "At Use",
|
| 97 |
bg: "bg-purple-200",
|
| 98 |
text: "text-purple-800",
|
| 99 |
border: "border-purple-200",
|
| 100 |
},
|
| 101 |
M: {
|
| 102 |
-
label: "Monitor",
|
| 103 |
bg: "bg-blue-200",
|
| 104 |
text: "text-blue-800",
|
| 105 |
border: "border-blue-200",
|
|
@@ -111,7 +111,7 @@ function ratingKey(value: string) {
|
|
| 111 |
if (!raw) return "";
|
| 112 |
const match = raw.match(/^([0-9]|[xXmM])/);
|
| 113 |
if (match) return match[1].toUpperCase();
|
| 114 |
-
return raw.split("
|
| 115 |
}
|
| 116 |
|
| 117 |
function formatRating(value: string, scale: Record<string, RatingTone>) {
|
|
@@ -120,12 +120,12 @@ function formatRating(value: string, scale: Record<string, RatingTone>) {
|
|
| 120 |
const tone = key ? scale[key] : undefined;
|
| 121 |
if (!tone) {
|
| 122 |
return {
|
| 123 |
-
text: raw || "
|
| 124 |
className: "bg-gray-50 text-gray-700 border-gray-200",
|
| 125 |
};
|
| 126 |
}
|
| 127 |
return {
|
| 128 |
-
text: `${key}
|
| 129 |
className: `${tone.bg} ${tone.text} ${tone.border}`,
|
| 130 |
};
|
| 131 |
}
|
|
|
|
| 36 |
|
| 37 |
const CATEGORY_SCALE: Record<string, RatingTone> = {
|
| 38 |
"0": {
|
| 39 |
+
label: "(100% Original Strength)",
|
| 40 |
bg: "bg-green-100",
|
| 41 |
text: "text-green-800",
|
| 42 |
border: "border-green-200",
|
| 43 |
},
|
| 44 |
"1": {
|
| 45 |
+
label: "(100% Original Strength)",
|
| 46 |
bg: "bg-green-200",
|
| 47 |
text: "text-green-800",
|
| 48 |
border: "border-green-200",
|
| 49 |
},
|
| 50 |
"2": {
|
| 51 |
+
label: "(95-100% Original Strength)",
|
| 52 |
bg: "bg-yellow-100",
|
| 53 |
text: "text-yellow-800",
|
| 54 |
border: "border-yellow-200",
|
| 55 |
},
|
| 56 |
"3": {
|
| 57 |
+
label: "(75-95% Original Strength)",
|
| 58 |
bg: "bg-yellow-200",
|
| 59 |
text: "text-yellow-800",
|
| 60 |
border: "border-yellow-200",
|
| 61 |
},
|
| 62 |
"4": {
|
| 63 |
+
label: "(50-75% Original Strength)",
|
| 64 |
bg: "bg-orange-200",
|
| 65 |
text: "text-orange-800",
|
| 66 |
border: "border-orange-200",
|
| 67 |
},
|
| 68 |
"5": {
|
| 69 |
+
label: "(<50% Original Strength)",
|
| 70 |
bg: "bg-red-200",
|
| 71 |
text: "text-red-800",
|
| 72 |
border: "border-red-200",
|
|
|
|
| 75 |
|
| 76 |
const PRIORITY_SCALE: Record<string, RatingTone> = {
|
| 77 |
"1": {
|
| 78 |
+
label: "(Immediate)",
|
| 79 |
bg: "bg-red-200",
|
| 80 |
text: "text-red-800",
|
| 81 |
border: "border-red-200",
|
| 82 |
},
|
| 83 |
"2": {
|
| 84 |
+
label: "(1 Year)",
|
| 85 |
bg: "bg-orange-200",
|
| 86 |
text: "text-orange-800",
|
| 87 |
border: "border-orange-200",
|
| 88 |
},
|
| 89 |
"3": {
|
| 90 |
+
label: "(3 Years)",
|
| 91 |
bg: "bg-green-200",
|
| 92 |
text: "text-green-800",
|
| 93 |
border: "border-green-200",
|
| 94 |
},
|
| 95 |
X: {
|
| 96 |
+
label: "(At Use)",
|
| 97 |
bg: "bg-purple-200",
|
| 98 |
text: "text-purple-800",
|
| 99 |
border: "border-purple-200",
|
| 100 |
},
|
| 101 |
M: {
|
| 102 |
+
label: "(Monitor)",
|
| 103 |
bg: "bg-blue-200",
|
| 104 |
text: "text-blue-800",
|
| 105 |
border: "border-blue-200",
|
|
|
|
| 111 |
if (!raw) return "";
|
| 112 |
const match = raw.match(/^([0-9]|[xXmM])/);
|
| 113 |
if (match) return match[1].toUpperCase();
|
| 114 |
+
return raw.split("")[0].trim().toUpperCase();
|
| 115 |
}
|
| 116 |
|
| 117 |
function formatRating(value: string, scale: Record<string, RatingTone>) {
|
|
|
|
| 120 |
const tone = key ? scale[key] : undefined;
|
| 121 |
if (!tone) {
|
| 122 |
return {
|
| 123 |
+
text: raw || "",
|
| 124 |
className: "bg-gray-50 text-gray-700 border-gray-200",
|
| 125 |
};
|
| 126 |
}
|
| 127 |
return {
|
| 128 |
+
text: `${key} ${tone.label}`,
|
| 129 |
className: `${tone.bg} ${tone.text} ${tone.border}`,
|
| 130 |
};
|
| 131 |
}
|
frontend/src/components/PdfPreviewPanel.tsx
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
frontend/src/components/report-editor.js
CHANGED
|
@@ -2,20 +2,20 @@
|
|
| 2 |
import * as feather from "feather-icons";
|
| 3 |
|
| 4 |
const CATEGORY_SCALE = {
|
| 5 |
-
"0": { label: "100% Original Strength", className: "bg-green-100 text-green-800 border-green-200" },
|
| 6 |
-
"1": { label: "100% Original Strength", className: "bg-green-200 text-green-800 border-green-200" },
|
| 7 |
-
"2": { label: "95-100% Original Strength", className: "bg-yellow-100 text-yellow-800 border-yellow-200" },
|
| 8 |
-
"3": { label: "75-95% Original Strength", className: "bg-yellow-200 text-yellow-800 border-yellow-200" },
|
| 9 |
-
"4": { label: "50-75% Original Strength", className: "bg-orange-200 text-orange-800 border-orange-200" },
|
| 10 |
-
"5": { label: "<50% Original Strength", className: "bg-red-200 text-red-800 border-red-200" },
|
| 11 |
};
|
| 12 |
|
| 13 |
const PRIORITY_SCALE = {
|
| 14 |
-
"1": { label: "Immediate", className: "bg-red-200 text-red-800 border-red-200" },
|
| 15 |
-
"2": { label: "1 Year", className: "bg-orange-200 text-orange-800 border-orange-200" },
|
| 16 |
-
"3": { label: "3 Years", className: "bg-green-200 text-green-800 border-green-200" },
|
| 17 |
-
X: { label: "At Use", className: "bg-purple-200 text-purple-800 border-purple-200" },
|
| 18 |
-
M: { label: "Monitor", className: "bg-blue-200 text-blue-800 border-blue-200" },
|
| 19 |
};
|
| 20 |
|
| 21 |
const MAX_PHOTOS_PRIMARY_PAGE = 2;
|
|
@@ -29,14 +29,14 @@ function parseScaleCode(value) {
|
|
| 29 |
function buildScaleBadge(value, scale) {
|
| 30 |
const raw = String(value || "").trim();
|
| 31 |
if (!raw) {
|
| 32 |
-
return { text: "
|
| 33 |
}
|
| 34 |
const code = parseScaleCode(raw);
|
| 35 |
const tone = scale[code];
|
| 36 |
if (!tone) {
|
| 37 |
return { text: raw, className: "bg-gray-50 text-gray-700 border-gray-200" };
|
| 38 |
}
|
| 39 |
-
return { text: `${code}
|
| 40 |
}
|
| 41 |
class ReportEditor extends HTMLElement {
|
| 42 |
constructor() {
|
|
@@ -1034,29 +1034,29 @@ class ReportEditor extends HTMLElement {
|
|
| 1034 |
const requiredAction = template.required_action || "";
|
| 1035 |
|
| 1036 |
const categoryScale = {
|
| 1037 |
-
"0": { label: "
|
| 1038 |
-
"1": { label: "
|
| 1039 |
-
"2": { label: "
|
| 1040 |
-
"3": { label: "
|
| 1041 |
-
"4": { label: "
|
| 1042 |
-
"5": { label: "
|
| 1043 |
};
|
| 1044 |
const priorityScale = {
|
| 1045 |
-
"1": { label: "Immediate", bg: "bg-red-200", text: "text-red-800", border: "border-red-200" },
|
| 1046 |
-
"2": { label: "1 Year", bg: "bg-orange-200", text: "text-orange-800", border: "border-orange-200" },
|
| 1047 |
-
"3": { label: "3 Years", bg: "bg-green-200", text: "text-green-800", border: "border-green-200" },
|
| 1048 |
-
X: { label: "At Use", bg: "bg-purple-200", text: "text-purple-800", border: "border-purple-200" },
|
| 1049 |
-
M: { label: "Monitor", bg: "bg-blue-200", text: "text-blue-800", border: "border-blue-200" },
|
| 1050 |
};
|
| 1051 |
const categoryBadge = this._ratingBadge(category, categoryScale);
|
| 1052 |
const priorityBadge = this._ratingBadge(priority, priorityScale);
|
| 1053 |
const categoryOptions = ["0", "1", "2", "3", "4", "5"].map((key) => ({
|
| 1054 |
value: key,
|
| 1055 |
-
label: `${key}
|
| 1056 |
}));
|
| 1057 |
const priorityOptions = ["1", "2", "3", "X", "M"].map((key) => ({
|
| 1058 |
value: key,
|
| 1059 |
-
label: `${key}
|
| 1060 |
}));
|
| 1061 |
|
| 1062 |
const variant =
|
|
@@ -1372,16 +1372,16 @@ class ReportEditor extends HTMLElement {
|
|
| 1372 |
_ratingBadge(value, scale) {
|
| 1373 |
const raw = String(value || "").trim();
|
| 1374 |
if (!raw) {
|
| 1375 |
-
return { text: "
|
| 1376 |
}
|
| 1377 |
const match = raw.match(/^([0-9]|[xXmM])/);
|
| 1378 |
-
const key = match ? match[1].toUpperCase() : raw.split("
|
| 1379 |
const tone = scale[key];
|
| 1380 |
if (!tone) {
|
| 1381 |
return { text: raw, className: "bg-gray-50 text-gray-700 border-gray-200" };
|
| 1382 |
}
|
| 1383 |
return {
|
| 1384 |
-
text: `${key}
|
| 1385 |
className: `${tone.bg} ${tone.text} ${tone.border}`,
|
| 1386 |
};
|
| 1387 |
}
|
|
|
|
| 2 |
import * as feather from "feather-icons";
|
| 3 |
|
| 4 |
const CATEGORY_SCALE = {
|
| 5 |
+
"0": { label: "(100% Original Strength)", className: "bg-green-100 text-green-800 border-green-200" },
|
| 6 |
+
"1": { label: "(100% Original Strength)", className: "bg-green-200 text-green-800 border-green-200" },
|
| 7 |
+
"2": { label: "(95-100% Original Strength)", className: "bg-yellow-100 text-yellow-800 border-yellow-200" },
|
| 8 |
+
"3": { label: "(75-95% Original Strength)", className: "bg-yellow-200 text-yellow-800 border-yellow-200" },
|
| 9 |
+
"4": { label: "(50-75% Original Strength)", className: "bg-orange-200 text-orange-800 border-orange-200" },
|
| 10 |
+
"5": { label: "(<50% Original Strength)", className: "bg-red-200 text-red-800 border-red-200" },
|
| 11 |
};
|
| 12 |
|
| 13 |
const PRIORITY_SCALE = {
|
| 14 |
+
"1": { label: "(Immediate)", className: "bg-red-200 text-red-800 border-red-200" },
|
| 15 |
+
"2": { label: "(1 Year)", className: "bg-orange-200 text-orange-800 border-orange-200" },
|
| 16 |
+
"3": { label: "(3 Years)", className: "bg-green-200 text-green-800 border-green-200" },
|
| 17 |
+
X: { label: "(At Use)", className: "bg-purple-200 text-purple-800 border-purple-200" },
|
| 18 |
+
M: { label: "(Monitor)", className: "bg-blue-200 text-blue-800 border-blue-200" },
|
| 19 |
};
|
| 20 |
|
| 21 |
const MAX_PHOTOS_PRIMARY_PAGE = 2;
|
|
|
|
| 29 |
function buildScaleBadge(value, scale) {
|
| 30 |
const raw = String(value || "").trim();
|
| 31 |
if (!raw) {
|
| 32 |
+
return { text: "", className: "bg-gray-50 text-gray-700 border-gray-200" };
|
| 33 |
}
|
| 34 |
const code = parseScaleCode(raw);
|
| 35 |
const tone = scale[code];
|
| 36 |
if (!tone) {
|
| 37 |
return { text: raw, className: "bg-gray-50 text-gray-700 border-gray-200" };
|
| 38 |
}
|
| 39 |
+
return { text: `${code} ${tone.label}`, className: tone.className };
|
| 40 |
}
|
| 41 |
class ReportEditor extends HTMLElement {
|
| 42 |
constructor() {
|
|
|
|
| 1034 |
const requiredAction = template.required_action || "";
|
| 1035 |
|
| 1036 |
const categoryScale = {
|
| 1037 |
+
"0": { label: "(100% Original Strength)", bg: "bg-green-100", text: "text-green-800", border: "border-green-200" },
|
| 1038 |
+
"1": { label: "(100% Original Strength)", bg: "bg-green-200", text: "text-green-800", border: "border-green-200" },
|
| 1039 |
+
"2": { label: "(95-100% Original Strength)", bg: "bg-yellow-100", text: "text-yellow-800", border: "border-yellow-200" },
|
| 1040 |
+
"3": { label: "(75-95% Original Strength)", bg: "bg-yellow-200", text: "text-yellow-800", border: "border-yellow-200" },
|
| 1041 |
+
"4": { label: "(50-75% Original Strength)", bg: "bg-orange-200", text: "text-orange-800", border: "border-orange-200" },
|
| 1042 |
+
"5": { label: "(<50% Original Strength)", bg: "bg-red-200", text: "text-red-800", border: "border-red-200" },
|
| 1043 |
};
|
| 1044 |
const priorityScale = {
|
| 1045 |
+
"1": { label: "(Immediate)", bg: "bg-red-200", text: "text-red-800", border: "border-red-200" },
|
| 1046 |
+
"2": { label: "(1 Year)", bg: "bg-orange-200", text: "text-orange-800", border: "border-orange-200" },
|
| 1047 |
+
"3": { label: "(3 Years)", bg: "bg-green-200", text: "text-green-800", border: "border-green-200" },
|
| 1048 |
+
X: { label: "(At Use)", bg: "bg-purple-200", text: "text-purple-800", border: "border-purple-200" },
|
| 1049 |
+
M: { label: "(Monitor)", bg: "bg-blue-200", text: "text-blue-800", border: "border-blue-200" },
|
| 1050 |
};
|
| 1051 |
const categoryBadge = this._ratingBadge(category, categoryScale);
|
| 1052 |
const priorityBadge = this._ratingBadge(priority, priorityScale);
|
| 1053 |
const categoryOptions = ["0", "1", "2", "3", "4", "5"].map((key) => ({
|
| 1054 |
value: key,
|
| 1055 |
+
label: `${key} ${categoryScale[key].label}`,
|
| 1056 |
}));
|
| 1057 |
const priorityOptions = ["1", "2", "3", "X", "M"].map((key) => ({
|
| 1058 |
value: key,
|
| 1059 |
+
label: `${key} ${priorityScale[key].label}`,
|
| 1060 |
}));
|
| 1061 |
|
| 1062 |
const variant =
|
|
|
|
| 1372 |
_ratingBadge(value, scale) {
|
| 1373 |
const raw = String(value || "").trim();
|
| 1374 |
if (!raw) {
|
| 1375 |
+
return { text: "", className: "bg-gray-50 text-gray-700 border-gray-200" };
|
| 1376 |
}
|
| 1377 |
const match = raw.match(/^([0-9]|[xXmM])/);
|
| 1378 |
+
const key = match ? match[1].toUpperCase() : raw.split("")[0].trim().toUpperCase();
|
| 1379 |
const tone = scale[key];
|
| 1380 |
if (!tone) {
|
| 1381 |
return { text: raw, className: "bg-gray-50 text-gray-700 border-gray-200" };
|
| 1382 |
}
|
| 1383 |
return {
|
| 1384 |
+
text: `${key} ${tone.label}`,
|
| 1385 |
className: `${tone.bg} ${tone.text} ${tone.border}`,
|
| 1386 |
};
|
| 1387 |
}
|
frontend/src/pages/EditReportPage.tsx
CHANGED
|
@@ -96,7 +96,9 @@ export default function EditReportPage() {
|
|
| 96 |
void saveAndNavigate(`/report-viewer${sessionQuery}`);
|
| 97 |
};
|
| 98 |
editorEl.addEventListener("editor-closed", handleClose);
|
| 99 |
-
return () =>
|
|
|
|
|
|
|
| 100 |
}, [editorEl, saveAndNavigate, sessionQuery]);
|
| 101 |
|
| 102 |
useEffect(() => {
|
|
|
|
| 96 |
void saveAndNavigate(`/report-viewer${sessionQuery}`);
|
| 97 |
};
|
| 98 |
editorEl.addEventListener("editor-closed", handleClose);
|
| 99 |
+
return () => {
|
| 100 |
+
editorEl.removeEventListener("editor-closed", handleClose);
|
| 101 |
+
};
|
| 102 |
}, [editorEl, saveAndNavigate, sessionQuery]);
|
| 103 |
|
| 104 |
useEffect(() => {
|
server/app/services/pdf_export.py
CHANGED
|
@@ -1,59 +1,42 @@
|
|
| 1 |
-
|
| 2 |
|
| 3 |
-
import asyncio
|
| 4 |
-
import sys
|
| 5 |
from pathlib import Path
|
| 6 |
|
| 7 |
-
from playwright.
|
| 8 |
|
| 9 |
from ..core.config import get_settings
|
| 10 |
|
| 11 |
|
| 12 |
-
|
| 13 |
settings = get_settings()
|
| 14 |
base_url = settings.frontend_base_url.rstrip("/")
|
| 15 |
url = f"{base_url}/print/report?session={session_id}"
|
| 16 |
|
| 17 |
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 18 |
|
| 19 |
-
|
| 20 |
-
browser =
|
| 21 |
-
context =
|
| 22 |
-
page =
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
'[data-print-ready="true"]',
|
|
|
|
| 26 |
)
|
| 27 |
-
|
| 28 |
-
|
| 29 |
"Array.from(document.images || []).every(img => img.complete)"
|
| 30 |
)
|
| 31 |
|
| 32 |
-
|
| 33 |
path=str(output_path),
|
| 34 |
format="A4",
|
| 35 |
print_background=True,
|
| 36 |
prefer_css_page_size=True,
|
| 37 |
-
margin={"top": "
|
| 38 |
)
|
| 39 |
|
| 40 |
-
|
| 41 |
-
|
| 42 |
|
| 43 |
return output_path
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
def _run_with_proactor(coro: asyncio.Future) -> Path:
|
| 47 |
-
if sys.platform.startswith("win"):
|
| 48 |
-
loop = asyncio.ProactorEventLoop()
|
| 49 |
-
try:
|
| 50 |
-
asyncio.set_event_loop(loop)
|
| 51 |
-
return loop.run_until_complete(coro)
|
| 52 |
-
finally:
|
| 53 |
-
loop.close()
|
| 54 |
-
asyncio.set_event_loop(None)
|
| 55 |
-
return asyncio.run(coro)
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
def render_pdf_sync(session_id: str, output_path: Path) -> Path:
|
| 59 |
-
return _run_with_proactor(_render_pdf_async(session_id, output_path))
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
|
|
|
|
|
|
|
| 3 |
from pathlib import Path
|
| 4 |
|
| 5 |
+
from playwright.sync_api import sync_playwright
|
| 6 |
|
| 7 |
from ..core.config import get_settings
|
| 8 |
|
| 9 |
|
| 10 |
+
def render_pdf(session_id: str, output_path: Path) -> Path:
|
| 11 |
settings = get_settings()
|
| 12 |
base_url = settings.frontend_base_url.rstrip("/")
|
| 13 |
url = f"{base_url}/print/report?session={session_id}"
|
| 14 |
|
| 15 |
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 16 |
|
| 17 |
+
with sync_playwright() as playwright:
|
| 18 |
+
browser = playwright.chromium.launch(headless=True, args=["--no-sandbox"])
|
| 19 |
+
context = browser.new_context()
|
| 20 |
+
page = context.new_page()
|
| 21 |
+
page.goto(url, wait_until="networkidle", timeout=settings.pdf_timeout_ms)
|
| 22 |
+
page.wait_for_selector(
|
| 23 |
+
'[data-print-ready="true"]',
|
| 24 |
+
timeout=settings.pdf_timeout_ms,
|
| 25 |
)
|
| 26 |
+
page.wait_for_function("document.fonts && document.fonts.ready")
|
| 27 |
+
page.wait_for_function(
|
| 28 |
"Array.from(document.images || []).every(img => img.complete)"
|
| 29 |
)
|
| 30 |
|
| 31 |
+
page.pdf(
|
| 32 |
path=str(output_path),
|
| 33 |
format="A4",
|
| 34 |
print_background=True,
|
| 35 |
prefer_css_page_size=True,
|
| 36 |
+
margin={"top": "10mm", "right": "10mm", "bottom": "10mm", "left": "10mm"},
|
| 37 |
)
|
| 38 |
|
| 39 |
+
context.close()
|
| 40 |
+
browser.close()
|
| 41 |
|
| 42 |
return output_path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/app/services/pdf_reportlab.py
CHANGED
|
@@ -185,8 +185,8 @@ def _badge_style(value: str, scale: dict) -> tuple[str, colors.Color, colors.Col
|
|
| 185 |
key = match.group(1)
|
| 186 |
tone = scale.get(key)
|
| 187 |
if not tone:
|
| 188 |
-
return (raw or "
|
| 189 |
-
return f"{key}
|
| 190 |
|
| 191 |
|
| 192 |
def _resolve_logo_path(store: SessionStore, session: dict, raw: str) -> Optional[Path]:
|
|
@@ -579,19 +579,19 @@ def render_report_pdf(
|
|
| 579 |
category = _safe_text(template.get("category"))
|
| 580 |
priority = _safe_text(template.get("priority"))
|
| 581 |
cat_scale = {
|
| 582 |
-
"0": {"label": "100% Original Strength", "bg": green_100, "text": colors.HexColor("#166534")},
|
| 583 |
-
"1": {"label": "100% Original Strength", "bg": green_200, "text": colors.HexColor("#166534")},
|
| 584 |
-
"2": {"label": "95-100% Original Strength", "bg": yellow_100, "text": colors.HexColor("#854d0e")},
|
| 585 |
-
"3": {"label": "75-95% Original Strength", "bg": yellow_200, "text": colors.HexColor("#854d0e")},
|
| 586 |
-
"4": {"label": "50-75% Original Strength", "bg": orange_200, "text": colors.HexColor("#9a3412")},
|
| 587 |
-
"5": {"label": "<50% Original Strength", "bg": red_200, "text": colors.HexColor("#991b1b")},
|
| 588 |
}
|
| 589 |
pr_scale = {
|
| 590 |
-
"1": {"label": "Immediate", "bg": red_200, "text": colors.HexColor("#991b1b")},
|
| 591 |
-
"2": {"label": "1 Year", "bg": orange_200, "text": colors.HexColor("#9a3412")},
|
| 592 |
-
"3": {"label": "3 Years", "bg": green_200, "text": colors.HexColor("#166534")},
|
| 593 |
-
"X": {"label": "At Use", "bg": purple_200, "text": purple_800},
|
| 594 |
-
"M": {"label": "Monitor", "bg": blue_200, "text": blue_900},
|
| 595 |
}
|
| 596 |
cat_text, cat_bg, cat_text_color = _badge_style(category, cat_scale)
|
| 597 |
pr_text, pr_bg, pr_text_color = _badge_style(priority, pr_scale)
|
|
@@ -662,7 +662,7 @@ def render_report_pdf(
|
|
| 662 |
pdf.setStrokeColor(gray_200)
|
| 663 |
cond_lines = _wrap_lines(
|
| 664 |
pdf,
|
| 665 |
-
condition or "
|
| 666 |
width - 2 * margin - 4 * mm,
|
| 667 |
4,
|
| 668 |
"Helvetica",
|
|
@@ -698,7 +698,7 @@ def render_report_pdf(
|
|
| 698 |
pdf.setStrokeColor(gray_200)
|
| 699 |
action_lines = _wrap_lines(
|
| 700 |
pdf,
|
| 701 |
-
action or "
|
| 702 |
width - 2 * margin - 4 * mm,
|
| 703 |
4,
|
| 704 |
"Helvetica",
|
|
|
|
| 185 |
key = match.group(1)
|
| 186 |
tone = scale.get(key)
|
| 187 |
if not tone:
|
| 188 |
+
return (raw or ""), colors.HexColor("#f9fafb"), colors.HexColor("#374151")
|
| 189 |
+
return f"{key} {tone['label']}", tone["bg"], tone["text"]
|
| 190 |
|
| 191 |
|
| 192 |
def _resolve_logo_path(store: SessionStore, session: dict, raw: str) -> Optional[Path]:
|
|
|
|
| 579 |
category = _safe_text(template.get("category"))
|
| 580 |
priority = _safe_text(template.get("priority"))
|
| 581 |
cat_scale = {
|
| 582 |
+
"0": {"label": "(100% Original Strength)", "bg": green_100, "text": colors.HexColor("#166534")},
|
| 583 |
+
"1": {"label": "(100% Original Strength)", "bg": green_200, "text": colors.HexColor("#166534")},
|
| 584 |
+
"2": {"label": "(95-100% Original Strength)", "bg": yellow_100, "text": colors.HexColor("#854d0e")},
|
| 585 |
+
"3": {"label": "(75-95% Original Strength)", "bg": yellow_200, "text": colors.HexColor("#854d0e")},
|
| 586 |
+
"4": {"label": "(50-75% Original Strength)", "bg": orange_200, "text": colors.HexColor("#9a3412")},
|
| 587 |
+
"5": {"label": "(<50% Original Strength)", "bg": red_200, "text": colors.HexColor("#991b1b")},
|
| 588 |
}
|
| 589 |
pr_scale = {
|
| 590 |
+
"1": {"label": "(Immediate)", "bg": red_200, "text": colors.HexColor("#991b1b")},
|
| 591 |
+
"2": {"label": "(1 Year)", "bg": orange_200, "text": colors.HexColor("#9a3412")},
|
| 592 |
+
"3": {"label": "(3 Years)", "bg": green_200, "text": colors.HexColor("#166534")},
|
| 593 |
+
"X": {"label": "(At Use)", "bg": purple_200, "text": purple_800},
|
| 594 |
+
"M": {"label": "(Monitor)", "bg": blue_200, "text": blue_900},
|
| 595 |
}
|
| 596 |
cat_text, cat_bg, cat_text_color = _badge_style(category, cat_scale)
|
| 597 |
pr_text, pr_bg, pr_text_color = _badge_style(priority, pr_scale)
|
|
|
|
| 662 |
pdf.setStrokeColor(gray_200)
|
| 663 |
cond_lines = _wrap_lines(
|
| 664 |
pdf,
|
| 665 |
+
condition or "",
|
| 666 |
width - 2 * margin - 4 * mm,
|
| 667 |
4,
|
| 668 |
"Helvetica",
|
|
|
|
| 698 |
pdf.setStrokeColor(gray_200)
|
| 699 |
action_lines = _wrap_lines(
|
| 700 |
pdf,
|
| 701 |
+
action or "",
|
| 702 |
width - 2 * margin - 4 * mm,
|
| 703 |
4,
|
| 704 |
"Helvetica",
|
server/app/services/session_store.py
CHANGED
|
@@ -11,6 +11,7 @@ from typing import Iterable, List, Optional
|
|
| 11 |
from uuid import uuid4
|
| 12 |
|
| 13 |
from fastapi import UploadFile
|
|
|
|
| 14 |
|
| 15 |
from ..core.config import get_settings
|
| 16 |
|
|
@@ -18,6 +19,7 @@ from ..core.config import get_settings
|
|
| 18 |
IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".gif", ".webp"}
|
| 19 |
DOC_EXTS = {".pdf", ".doc", ".docx"}
|
| 20 |
DATA_EXTS = {".csv", ".xls", ".xlsx"}
|
|
|
|
| 21 |
SESSION_ID_RE = re.compile(r"^[0-9a-f]{32}$")
|
| 22 |
BUILTIN_PAGE_TEMPLATES = [
|
| 23 |
{
|
|
@@ -82,6 +84,27 @@ def _category_for(filename: str) -> str:
|
|
| 82 |
return "documents"
|
| 83 |
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
def _validate_session_id(session_id: str) -> str:
|
| 86 |
if not session_id:
|
| 87 |
raise ValueError("Invalid session id.")
|
|
@@ -585,6 +608,9 @@ class SessionStore:
|
|
| 585 |
handle.write(chunk)
|
| 586 |
|
| 587 |
category = _category_for(filename)
|
|
|
|
|
|
|
|
|
|
| 588 |
return StoredFile(
|
| 589 |
id=file_id,
|
| 590 |
name=filename,
|
|
|
|
| 11 |
from uuid import uuid4
|
| 12 |
|
| 13 |
from fastapi import UploadFile
|
| 14 |
+
from PIL import Image, ImageOps, UnidentifiedImageError
|
| 15 |
|
| 16 |
from ..core.config import get_settings
|
| 17 |
|
|
|
|
| 19 |
IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".gif", ".webp"}
|
| 20 |
DOC_EXTS = {".pdf", ".doc", ".docx"}
|
| 21 |
DATA_EXTS = {".csv", ".xls", ".xlsx"}
|
| 22 |
+
EXIF_NORMALIZE_EXTS = {".jpg", ".jpeg", ".png", ".webp"}
|
| 23 |
SESSION_ID_RE = re.compile(r"^[0-9a-f]{32}$")
|
| 24 |
BUILTIN_PAGE_TEMPLATES = [
|
| 25 |
{
|
|
|
|
| 84 |
return "documents"
|
| 85 |
|
| 86 |
|
| 87 |
+
def _normalize_uploaded_photo(path: Path) -> None:
|
| 88 |
+
ext = path.suffix.lower()
|
| 89 |
+
if ext not in EXIF_NORMALIZE_EXTS:
|
| 90 |
+
return
|
| 91 |
+
|
| 92 |
+
try:
|
| 93 |
+
with Image.open(path) as image:
|
| 94 |
+
normalized = ImageOps.exif_transpose(image)
|
| 95 |
+
format_name = image.format
|
| 96 |
+
if normalized.mode in ("RGBA", "LA", "P") and format_name in {"JPEG", "JPG"}:
|
| 97 |
+
normalized = normalized.convert("RGB")
|
| 98 |
+
save_kwargs = {"exif": b""}
|
| 99 |
+
if format_name in {"JPEG", "JPG"}:
|
| 100 |
+
save_kwargs["quality"] = 95
|
| 101 |
+
save_kwargs["optimize"] = True
|
| 102 |
+
normalized.save(path, format=format_name, **save_kwargs)
|
| 103 |
+
except (UnidentifiedImageError, OSError, ValueError, TypeError):
|
| 104 |
+
# Keep original bytes if the file cannot be decoded by Pillow.
|
| 105 |
+
return
|
| 106 |
+
|
| 107 |
+
|
| 108 |
def _validate_session_id(session_id: str) -> str:
|
| 109 |
if not session_id:
|
| 110 |
raise ValueError("Invalid session id.")
|
|
|
|
| 608 |
handle.write(chunk)
|
| 609 |
|
| 610 |
category = _category_for(filename)
|
| 611 |
+
if category == "photos":
|
| 612 |
+
_normalize_uploaded_photo(dest)
|
| 613 |
+
size = dest.stat().st_size
|
| 614 |
return StoredFile(
|
| 615 |
id=file_id,
|
| 616 |
name=filename,
|