Spaces:
Sleeping
Sleeping
Commit ·
802cdd2
1
Parent(s): de747ba
PDF Template Update
Browse files
frontend/public/templates/job-sheet-template.html
CHANGED
|
@@ -31,7 +31,7 @@
|
|
| 31 |
}
|
| 32 |
|
| 33 |
.template-field-multiline {
|
| 34 |
-
min-height:
|
| 35 |
white-space: pre-wrap;
|
| 36 |
}
|
| 37 |
|
|
@@ -46,13 +46,13 @@
|
|
| 46 |
<!-- A4-focused container -->
|
| 47 |
<main class="mx-auto max-w-[210mm] bg-white shadow-sm ring-1 ring-gray-200 rounded-xl p-5 print:ring-0 print:shadow-none print:rounded-none">
|
| 48 |
<!-- Header -->
|
| 49 |
-
<header class="mb-
|
| 50 |
<div class="grid grid-cols-[auto,1fr,auto] items-center gap-3">
|
| 51 |
<div class="flex items-center">
|
| 52 |
<img
|
| 53 |
src="../assets/prosento-logo.png"
|
| 54 |
alt="Prosento logo"
|
| 55 |
-
class="h-
|
| 56 |
loading="eager"
|
| 57 |
/>
|
| 58 |
</div>
|
|
@@ -66,14 +66,13 @@
|
|
| 66 |
style="display:inline-block; min-width: 180px;"
|
| 67 |
></span>
|
| 68 |
</div>
|
| 69 |
-
<div class="text-[10px] text-gray-500 whitespace-nowrap">Document No</div>
|
| 70 |
</div>
|
| 71 |
|
| 72 |
<div class="flex items-center justify-end">
|
| 73 |
<img
|
| 74 |
src="../assets/client-logo.png"
|
| 75 |
alt="Company logo"
|
| 76 |
-
class="h-
|
| 77 |
loading="eager"
|
| 78 |
/>
|
| 79 |
</div>
|
|
@@ -81,47 +80,47 @@
|
|
| 81 |
</header>
|
| 82 |
|
| 83 |
<!-- Observations and Findings -->
|
| 84 |
-
<section class="mb-
|
| 85 |
-
<h2 id="observations-title" class="text-
|
| 86 |
Observations and Findings
|
| 87 |
</h2>
|
| 88 |
|
| 89 |
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-
|
| 90 |
<div class="md:col-span-2">
|
| 91 |
-
<div class="grid grid-cols-3 gap-
|
| 92 |
<div class="space-y-0.5">
|
| 93 |
-
<div class="text-
|
| 94 |
-
<div class="template-field text-
|
| 95 |
</div>
|
| 96 |
|
| 97 |
<div class="space-y-0.5">
|
| 98 |
-
<div class="text-
|
| 99 |
-
<div class="template-field text-
|
| 100 |
</div>
|
| 101 |
|
| 102 |
<div class="space-y-0.5">
|
| 103 |
-
<div class="text-
|
| 104 |
-
<div class="template-field text-
|
| 105 |
</div>
|
| 106 |
</div>
|
| 107 |
</div>
|
| 108 |
|
| 109 |
<!-- Centered Category + Priority (must be direct child of the grid) -->
|
| 110 |
<div class="md:col-span-2 flex justify-center">
|
| 111 |
-
<div class="inline-flex items-center gap-
|
| 112 |
<div class="text-center space-y-1">
|
| 113 |
-
<div class="text-
|
| 114 |
<span
|
| 115 |
-
class="template-field inline-flex items-center justify-center rounded-md border px-3 py-1 text-
|
| 116 |
contenteditable="true"
|
| 117 |
data-placeholder="3 - Poor"
|
| 118 |
>3 - Poor</span>
|
| 119 |
</div>
|
| 120 |
|
| 121 |
<div class="text-center space-y-1">
|
| 122 |
-
<div class="text-
|
| 123 |
<span
|
| 124 |
-
class="template-field inline-flex items-center justify-center rounded-md border px-3 py-1 text-
|
| 125 |
contenteditable="true"
|
| 126 |
data-placeholder="3 - 3 Years"
|
| 127 |
>3 - 3 Years</span>
|
|
@@ -132,17 +131,17 @@
|
|
| 132 |
|
| 133 |
<!-- Full-width: Condition Description -->
|
| 134 |
<div class="md:col-span-2 space-y-1">
|
| 135 |
-
<div class="text-
|
| 136 |
-
<div class="bg-
|
| 137 |
-
<p class="template-field template-field-multiline text-
|
| 138 |
</div>
|
| 139 |
</div>
|
| 140 |
|
| 141 |
<!-- Full-width: Action Required -->
|
| 142 |
<div class="md:col-span-2 space-y-1">
|
| 143 |
-
<div class="text-
|
| 144 |
-
<div class="bg-
|
| 145 |
-
<p class="template-field template-field-multiline text-
|
| 146 |
</div>
|
| 147 |
</div>
|
| 148 |
</div>
|
|
@@ -155,56 +154,56 @@
|
|
| 155 |
</h2>
|
| 156 |
|
| 157 |
<div class="columns-2" style="column-gap:0.75rem;">
|
| 158 |
-
<figure class="border border-gray-200 rounded-lg p-2 bg-gray-50
|
| 159 |
<div class="h-40 w-full flex items-center justify-center text-xs text-gray-400 border border-dashed border-gray-300 rounded-md">
|
| 160 |
Photo slot
|
| 161 |
</div>
|
| 162 |
-
<figcaption class="text-[
|
| 163 |
Figure 1
|
| 164 |
</figcaption>
|
| 165 |
</figure>
|
| 166 |
|
| 167 |
-
<figure class="border border-gray-200 rounded-lg p-2 bg-gray-50
|
| 168 |
<div class="h-40 w-full flex items-center justify-center text-xs text-gray-400 border border-dashed border-gray-300 rounded-md">
|
| 169 |
Photo slot
|
| 170 |
</div>
|
| 171 |
-
<figcaption class="text-[
|
| 172 |
Figure 2
|
| 173 |
</figcaption>
|
| 174 |
</figure>
|
| 175 |
|
| 176 |
-
<figure class="border border-gray-200 rounded-lg p-2 bg-gray-50
|
| 177 |
<div class="h-40 w-full flex items-center justify-center text-xs text-gray-400 border border-dashed border-gray-300 rounded-md">
|
| 178 |
Photo slot
|
| 179 |
</div>
|
| 180 |
-
<figcaption class="text-[
|
| 181 |
Figure 3
|
| 182 |
</figcaption>
|
| 183 |
</figure>
|
| 184 |
|
| 185 |
-
<figure class="border border-gray-200 rounded-lg p-2 bg-gray-50
|
| 186 |
<div class="h-40 w-full flex items-center justify-center text-xs text-gray-400 border border-dashed border-gray-300 rounded-md">
|
| 187 |
Photo slot
|
| 188 |
</div>
|
| 189 |
-
<figcaption class="text-[
|
| 190 |
Figure 4
|
| 191 |
</figcaption>
|
| 192 |
</figure>
|
| 193 |
|
| 194 |
-
<figure class="border border-gray-200 rounded-lg p-2 bg-gray-50
|
| 195 |
<div class="h-40 w-full flex items-center justify-center text-xs text-gray-400 border border-dashed border-gray-300 rounded-md">
|
| 196 |
Photo slot
|
| 197 |
</div>
|
| 198 |
-
<figcaption class="text-[
|
| 199 |
Figure 5
|
| 200 |
</figcaption>
|
| 201 |
</figure>
|
| 202 |
|
| 203 |
-
<figure class="border border-gray-200 rounded-lg p-2 bg-gray-50
|
| 204 |
<div class="h-40 w-full flex items-center justify-center text-xs text-gray-400 border border-dashed border-gray-300 rounded-md">
|
| 205 |
Photo slot
|
| 206 |
</div>
|
| 207 |
-
<figcaption class="text-[
|
| 208 |
Figure 6
|
| 209 |
</figcaption>
|
| 210 |
</figure>
|
|
|
|
| 31 |
}
|
| 32 |
|
| 33 |
.template-field-multiline {
|
| 34 |
+
min-height: 1.8em;
|
| 35 |
white-space: pre-wrap;
|
| 36 |
}
|
| 37 |
|
|
|
|
| 46 |
<!-- A4-focused container -->
|
| 47 |
<main class="mx-auto max-w-[210mm] bg-white shadow-sm ring-1 ring-gray-200 rounded-xl p-5 print:ring-0 print:shadow-none print:rounded-none">
|
| 48 |
<!-- Header -->
|
| 49 |
+
<header class="mb-3 border-b border-gray-200 pb-2">
|
| 50 |
<div class="grid grid-cols-[auto,1fr,auto] items-center gap-3">
|
| 51 |
<div class="flex items-center">
|
| 52 |
<img
|
| 53 |
src="../assets/prosento-logo.png"
|
| 54 |
alt="Prosento logo"
|
| 55 |
+
class="h-14 w-auto object-contain"
|
| 56 |
loading="eager"
|
| 57 |
/>
|
| 58 |
</div>
|
|
|
|
| 66 |
style="display:inline-block; min-width: 180px;"
|
| 67 |
></span>
|
| 68 |
</div>
|
|
|
|
| 69 |
</div>
|
| 70 |
|
| 71 |
<div class="flex items-center justify-end">
|
| 72 |
<img
|
| 73 |
src="../assets/client-logo.png"
|
| 74 |
alt="Company logo"
|
| 75 |
+
class="h-14 w-auto object-contain"
|
| 76 |
loading="eager"
|
| 77 |
/>
|
| 78 |
</div>
|
|
|
|
| 80 |
</header>
|
| 81 |
|
| 82 |
<!-- Observations and Findings -->
|
| 83 |
+
<section class="mb-2" aria-labelledby="observations-title">
|
| 84 |
+
<h2 id="observations-title" class="text-[10px] font-semibold text-gray-800 border-b border-gray-200 pb-1 mb-1">
|
| 85 |
Observations and Findings
|
| 86 |
</h2>
|
| 87 |
|
| 88 |
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
| 89 |
<div class="md:col-span-2">
|
| 90 |
+
<div class="grid grid-cols-3 gap-2">
|
| 91 |
<div class="space-y-0.5">
|
| 92 |
+
<div class="text-[9px] font-medium text-gray-500">Ref</div>
|
| 93 |
+
<div class="template-field text-[10px] font-semibold text-gray-900" contenteditable="true" data-placeholder="Ref"></div>
|
| 94 |
</div>
|
| 95 |
|
| 96 |
<div class="space-y-0.5">
|
| 97 |
+
<div class="text-[9px] font-medium text-gray-500">Area</div>
|
| 98 |
+
<div class="template-field text-[10px] font-semibold text-gray-900" contenteditable="true" data-placeholder="Area"></div>
|
| 99 |
</div>
|
| 100 |
|
| 101 |
<div class="space-y-0.5">
|
| 102 |
+
<div class="text-[9px] font-medium text-gray-500">Location</div>
|
| 103 |
+
<div class="template-field text-[10px] font-semibold text-gray-900" contenteditable="true" data-placeholder="Location"></div>
|
| 104 |
</div>
|
| 105 |
</div>
|
| 106 |
</div>
|
| 107 |
|
| 108 |
<!-- Centered Category + Priority (must be direct child of the grid) -->
|
| 109 |
<div class="md:col-span-2 flex justify-center">
|
| 110 |
+
<div class="inline-flex items-center gap-4">
|
| 111 |
<div class="text-center space-y-1">
|
| 112 |
+
<div class="text-[9px] font-medium text-gray-500">Category</div>
|
| 113 |
<span
|
| 114 |
+
class="template-field inline-flex items-center justify-center rounded-md border px-3 py-1 text-[10px] font-semibold min-w-[120px] bg-yellow-200 text-yellow-800 border-yellow-200"
|
| 115 |
contenteditable="true"
|
| 116 |
data-placeholder="3 - Poor"
|
| 117 |
>3 - Poor</span>
|
| 118 |
</div>
|
| 119 |
|
| 120 |
<div class="text-center space-y-1">
|
| 121 |
+
<div class="text-[9px] font-medium text-gray-500">Priority</div>
|
| 122 |
<span
|
| 123 |
+
class="template-field inline-flex items-center justify-center rounded-md border px-3 py-1 text-[10px] font-semibold min-w-[120px] bg-green-200 text-green-800 border-green-200"
|
| 124 |
contenteditable="true"
|
| 125 |
data-placeholder="3 - 3 Years"
|
| 126 |
>3 - 3 Years</span>
|
|
|
|
| 131 |
|
| 132 |
<!-- Full-width: Condition Description -->
|
| 133 |
<div class="md:col-span-2 space-y-1">
|
| 134 |
+
<div class="text-[9px] font-medium text-gray-500">Condition Description</div>
|
| 135 |
+
<div class="bg-gray-50 border-l-4 border-gray-300 p-1.5 rounded-sm">
|
| 136 |
+
<p class="template-field template-field-multiline text-gray-700 text-[9px] font-medium leading-snug" contenteditable="true" data-placeholder="Item description"></p>
|
| 137 |
</div>
|
| 138 |
</div>
|
| 139 |
|
| 140 |
<!-- Full-width: Action Required -->
|
| 141 |
<div class="md:col-span-2 space-y-1">
|
| 142 |
+
<div class="text-[9px] font-medium text-gray-500">Action Required</div>
|
| 143 |
+
<div class="bg-gray-50 border-l-4 border-gray-300 p-1.5 rounded-sm">
|
| 144 |
+
<p class="template-field template-field-multiline text-gray-700 text-[9px] font-medium leading-snug" contenteditable="true" data-placeholder="Required action"></p>
|
| 145 |
</div>
|
| 146 |
</div>
|
| 147 |
</div>
|
|
|
|
| 154 |
</h2>
|
| 155 |
|
| 156 |
<div class="columns-2" style="column-gap:0.75rem;">
|
| 157 |
+
<figure class="border border-gray-200 rounded-lg p-2 pb-3 bg-gray-50 break-inside-avoid mb-3 flex flex-col gap-1 overflow-hidden">
|
| 158 |
<div class="h-40 w-full flex items-center justify-center text-xs text-gray-400 border border-dashed border-gray-300 rounded-md">
|
| 159 |
Photo slot
|
| 160 |
</div>
|
| 161 |
+
<figcaption class="text-[10px] text-gray-600 text-center break-all leading-tight">
|
| 162 |
Figure 1
|
| 163 |
</figcaption>
|
| 164 |
</figure>
|
| 165 |
|
| 166 |
+
<figure class="border border-gray-200 rounded-lg p-2 pb-3 bg-gray-50 break-inside-avoid mb-3 flex flex-col gap-1 overflow-hidden">
|
| 167 |
<div class="h-40 w-full flex items-center justify-center text-xs text-gray-400 border border-dashed border-gray-300 rounded-md">
|
| 168 |
Photo slot
|
| 169 |
</div>
|
| 170 |
+
<figcaption class="text-[10px] text-gray-600 text-center break-all leading-tight">
|
| 171 |
Figure 2
|
| 172 |
</figcaption>
|
| 173 |
</figure>
|
| 174 |
|
| 175 |
+
<figure class="border border-gray-200 rounded-lg p-2 pb-3 bg-gray-50 break-inside-avoid mb-3 flex flex-col gap-1 overflow-hidden">
|
| 176 |
<div class="h-40 w-full flex items-center justify-center text-xs text-gray-400 border border-dashed border-gray-300 rounded-md">
|
| 177 |
Photo slot
|
| 178 |
</div>
|
| 179 |
+
<figcaption class="text-[10px] text-gray-600 text-center break-all leading-tight">
|
| 180 |
Figure 3
|
| 181 |
</figcaption>
|
| 182 |
</figure>
|
| 183 |
|
| 184 |
+
<figure class="border border-gray-200 rounded-lg p-2 pb-3 bg-gray-50 break-inside-avoid mb-3 flex flex-col gap-1 overflow-hidden">
|
| 185 |
<div class="h-40 w-full flex items-center justify-center text-xs text-gray-400 border border-dashed border-gray-300 rounded-md">
|
| 186 |
Photo slot
|
| 187 |
</div>
|
| 188 |
+
<figcaption class="text-[10px] text-gray-600 text-center break-all leading-tight">
|
| 189 |
Figure 4
|
| 190 |
</figcaption>
|
| 191 |
</figure>
|
| 192 |
|
| 193 |
+
<figure class="border border-gray-200 rounded-lg p-2 pb-3 bg-gray-50 break-inside-avoid mb-3 flex flex-col gap-1 overflow-hidden">
|
| 194 |
<div class="h-40 w-full flex items-center justify-center text-xs text-gray-400 border border-dashed border-gray-300 rounded-md">
|
| 195 |
Photo slot
|
| 196 |
</div>
|
| 197 |
+
<figcaption class="text-[10px] text-gray-600 text-center break-all leading-tight">
|
| 198 |
Figure 5
|
| 199 |
</figcaption>
|
| 200 |
</figure>
|
| 201 |
|
| 202 |
+
<figure class="border border-gray-200 rounded-lg p-2 pb-3 bg-gray-50 break-inside-avoid mb-3 flex flex-col gap-1 overflow-hidden">
|
| 203 |
<div class="h-40 w-full flex items-center justify-center text-xs text-gray-400 border border-dashed border-gray-300 rounded-md">
|
| 204 |
Photo slot
|
| 205 |
</div>
|
| 206 |
+
<figcaption class="text-[10px] text-gray-600 text-center break-all leading-tight">
|
| 207 |
Figure 6
|
| 208 |
</figcaption>
|
| 209 |
</figure>
|
frontend/src/components/JobSheetTemplate.tsx
CHANGED
|
@@ -238,18 +238,22 @@ function PhotoSlot({ url, label, className = "", imageClassName = "" }: PhotoSlo
|
|
| 238 |
return (
|
| 239 |
<figure
|
| 240 |
className={[
|
| 241 |
-
"rounded-lg border border-gray-200 bg-gray-50 p-2 break-inside-avoid",
|
| 242 |
className,
|
| 243 |
].join(" ")}
|
| 244 |
style={{ breakInside: "avoid", pageBreakInside: "avoid" }}
|
| 245 |
>
|
| 246 |
-
<
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
{label}
|
| 254 |
</figcaption>
|
| 255 |
</figure>
|
|
@@ -352,7 +356,7 @@ export function JobSheetTemplate({
|
|
| 352 |
<img
|
| 353 |
src="/assets/prosento-logo.png"
|
| 354 |
alt="Prosento logo"
|
| 355 |
-
className="h-
|
| 356 |
/>
|
| 357 |
<div className="text-center leading-tight">
|
| 358 |
<div className="text-base font-semibold text-gray-900">
|
|
@@ -362,38 +366,38 @@ export function JobSheetTemplate({
|
|
| 362 |
<img
|
| 363 |
src={logoUrl}
|
| 364 |
alt="Company logo"
|
| 365 |
-
className="h-
|
| 366 |
/>
|
| 367 |
</div>
|
| 368 |
</header>
|
| 369 |
|
| 370 |
{variant === "full" ? (
|
| 371 |
-
<section className="mb-
|
| 372 |
<h2
|
| 373 |
id="observations-title"
|
| 374 |
-
className="text-[
|
| 375 |
>
|
| 376 |
Observations and Findings
|
| 377 |
</h2>
|
| 378 |
|
| 379 |
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-
|
| 380 |
<div className="md:col-span-2">
|
| 381 |
-
<div className="grid grid-cols-3 gap-
|
| 382 |
<div className="space-y-0.5">
|
| 383 |
-
<div className="text-[
|
| 384 |
-
<div className="template-field text-[
|
| 385 |
{reference}
|
| 386 |
</div>
|
| 387 |
</div>
|
| 388 |
<div className="space-y-0.5">
|
| 389 |
-
<div className="text-[
|
| 390 |
-
<div className="template-field text-[
|
| 391 |
{area}
|
| 392 |
</div>
|
| 393 |
</div>
|
| 394 |
<div className="space-y-0.5">
|
| 395 |
-
<div className="text-[
|
| 396 |
-
<div className="template-field text-[
|
| 397 |
{functionalLocation}
|
| 398 |
</div>
|
| 399 |
</div>
|
|
@@ -401,14 +405,14 @@ export function JobSheetTemplate({
|
|
| 401 |
</div>
|
| 402 |
|
| 403 |
<div className="md:col-span-2 flex justify-center">
|
| 404 |
-
<div className="inline-flex items-center gap-
|
| 405 |
<div className="text-center space-y-1">
|
| 406 |
-
<div className="text-[
|
| 407 |
Category
|
| 408 |
</div>
|
| 409 |
<span
|
| 410 |
className={[
|
| 411 |
-
"template-field inline-flex items-center justify-center rounded-md border px-4 py-1 text-[
|
| 412 |
categoryBadge.className,
|
| 413 |
].join(" ")}
|
| 414 |
>
|
|
@@ -417,12 +421,12 @@ export function JobSheetTemplate({
|
|
| 417 |
</div>
|
| 418 |
|
| 419 |
<div className="text-center space-y-1">
|
| 420 |
-
<div className="text-[
|
| 421 |
Priority
|
| 422 |
</div>
|
| 423 |
<span
|
| 424 |
className={[
|
| 425 |
-
"template-field inline-flex items-center justify-center rounded-md border px-4 py-1 text-[
|
| 426 |
priorityBadge.className,
|
| 427 |
].join(" ")}
|
| 428 |
>
|
|
@@ -434,22 +438,22 @@ export function JobSheetTemplate({
|
|
| 434 |
</div>
|
| 435 |
|
| 436 |
<div className="md:col-span-2 space-y-1">
|
| 437 |
-
<div className="text-[
|
| 438 |
Condition Description
|
| 439 |
</div>
|
| 440 |
-
<div className="bg-
|
| 441 |
-
<p className="template-field template-field-multiline text-
|
| 442 |
{conditionText}
|
| 443 |
</p>
|
| 444 |
</div>
|
| 445 |
</div>
|
| 446 |
|
| 447 |
<div className="md:col-span-2 space-y-1">
|
| 448 |
-
<div className="text-[
|
| 449 |
Action Required
|
| 450 |
</div>
|
| 451 |
-
<div className="bg-
|
| 452 |
-
<p className="template-field template-field-multiline text-
|
| 453 |
{actionText}
|
| 454 |
</p>
|
| 455 |
</div>
|
|
@@ -459,7 +463,7 @@ export function JobSheetTemplate({
|
|
| 459 |
) : null}
|
| 460 |
|
| 461 |
<section className="mb-3 avoid-break flex-1 min-h-0 flex flex-col">
|
| 462 |
-
<div className="text-[
|
| 463 |
{variant === "photos" ? "Photo Documentation (continued)" : "Photo Documentation"}
|
| 464 |
</div>
|
| 465 |
<div className={`${photoGridClass} flex-1 items-stretch`}>
|
|
@@ -472,7 +476,6 @@ export function JobSheetTemplate({
|
|
| 472 |
url={photo?.url}
|
| 473 |
label={figureCaption || photo?.name || `Figure ${index + 1}`}
|
| 474 |
className="h-full"
|
| 475 |
-
imageClassName="h-full"
|
| 476 |
/>
|
| 477 |
))
|
| 478 |
)}
|
|
|
|
| 238 |
return (
|
| 239 |
<figure
|
| 240 |
className={[
|
| 241 |
+
"rounded-lg border border-gray-200 bg-gray-50 p-2 pb-3 break-inside-avoid flex flex-col gap-1 overflow-hidden",
|
| 242 |
className,
|
| 243 |
].join(" ")}
|
| 244 |
style={{ breakInside: "avoid", pageBreakInside: "avoid" }}
|
| 245 |
>
|
| 246 |
+
<div className="w-full flex-1 flex items-center justify-center">
|
| 247 |
+
<img
|
| 248 |
+
src={url}
|
| 249 |
+
alt={label}
|
| 250 |
+
className={["w-full object-contain max-h-[240px]", imageClassName].join(
|
| 251 |
+
" ",
|
| 252 |
+
)}
|
| 253 |
+
loading="eager"
|
| 254 |
+
/>
|
| 255 |
+
</div>
|
| 256 |
+
<figcaption className="text-[10px] text-gray-600 text-center break-all leading-tight">
|
| 257 |
{label}
|
| 258 |
</figcaption>
|
| 259 |
</figure>
|
|
|
|
| 356 |
<img
|
| 357 |
src="/assets/prosento-logo.png"
|
| 358 |
alt="Prosento logo"
|
| 359 |
+
className="h-14 w-auto object-contain"
|
| 360 |
/>
|
| 361 |
<div className="text-center leading-tight">
|
| 362 |
<div className="text-base font-semibold text-gray-900">
|
|
|
|
| 366 |
<img
|
| 367 |
src={logoUrl}
|
| 368 |
alt="Company logo"
|
| 369 |
+
className="h-14 w-auto object-contain"
|
| 370 |
/>
|
| 371 |
</div>
|
| 372 |
</header>
|
| 373 |
|
| 374 |
{variant === "full" ? (
|
| 375 |
+
<section className="mb-2" aria-labelledby="observations-title">
|
| 376 |
<h2
|
| 377 |
id="observations-title"
|
| 378 |
+
className="text-[10px] font-semibold text-gray-800 border-b border-gray-200 pb-1 mb-1"
|
| 379 |
>
|
| 380 |
Observations and Findings
|
| 381 |
</h2>
|
| 382 |
|
| 383 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
| 384 |
<div className="md:col-span-2">
|
| 385 |
+
<div className="grid grid-cols-3 gap-2">
|
| 386 |
<div className="space-y-0.5">
|
| 387 |
+
<div className="text-[9px] font-medium text-gray-500">Ref</div>
|
| 388 |
+
<div className="template-field text-[10px] font-semibold text-gray-900">
|
| 389 |
{reference}
|
| 390 |
</div>
|
| 391 |
</div>
|
| 392 |
<div className="space-y-0.5">
|
| 393 |
+
<div className="text-[9px] font-medium text-gray-500">Area</div>
|
| 394 |
+
<div className="template-field text-[10px] font-semibold text-gray-900">
|
| 395 |
{area}
|
| 396 |
</div>
|
| 397 |
</div>
|
| 398 |
<div className="space-y-0.5">
|
| 399 |
+
<div className="text-[9px] font-medium text-gray-500">Location</div>
|
| 400 |
+
<div className="template-field text-[10px] font-semibold text-gray-900">
|
| 401 |
{functionalLocation}
|
| 402 |
</div>
|
| 403 |
</div>
|
|
|
|
| 405 |
</div>
|
| 406 |
|
| 407 |
<div className="md:col-span-2 flex justify-center">
|
| 408 |
+
<div className="inline-flex items-center gap-4">
|
| 409 |
<div className="text-center space-y-1">
|
| 410 |
+
<div className="text-[9px] font-medium text-gray-500">
|
| 411 |
Category
|
| 412 |
</div>
|
| 413 |
<span
|
| 414 |
className={[
|
| 415 |
+
"template-field inline-flex items-center justify-center rounded-md border px-4 py-1 text-[10px] font-semibold min-w-[120px]",
|
| 416 |
categoryBadge.className,
|
| 417 |
].join(" ")}
|
| 418 |
>
|
|
|
|
| 421 |
</div>
|
| 422 |
|
| 423 |
<div className="text-center space-y-1">
|
| 424 |
+
<div className="text-[9px] font-medium text-gray-500">
|
| 425 |
Priority
|
| 426 |
</div>
|
| 427 |
<span
|
| 428 |
className={[
|
| 429 |
+
"template-field inline-flex items-center justify-center rounded-md border px-4 py-1 text-[10px] font-semibold min-w-[120px]",
|
| 430 |
priorityBadge.className,
|
| 431 |
].join(" ")}
|
| 432 |
>
|
|
|
|
| 438 |
</div>
|
| 439 |
|
| 440 |
<div className="md:col-span-2 space-y-1">
|
| 441 |
+
<div className="text-[9px] font-medium text-gray-500">
|
| 442 |
Condition Description
|
| 443 |
</div>
|
| 444 |
+
<div className="bg-gray-50 border-l-4 border-gray-300 p-1.5 rounded-sm">
|
| 445 |
+
<p className="template-field template-field-multiline text-gray-700 text-[9px] font-medium leading-snug">
|
| 446 |
{conditionText}
|
| 447 |
</p>
|
| 448 |
</div>
|
| 449 |
</div>
|
| 450 |
|
| 451 |
<div className="md:col-span-2 space-y-1">
|
| 452 |
+
<div className="text-[9px] font-medium text-gray-500">
|
| 453 |
Action Required
|
| 454 |
</div>
|
| 455 |
+
<div className="bg-gray-50 border-l-4 border-gray-300 p-1.5 rounded-sm">
|
| 456 |
+
<p className="template-field template-field-multiline text-gray-700 text-[9px] font-medium leading-snug">
|
| 457 |
{actionText}
|
| 458 |
</p>
|
| 459 |
</div>
|
|
|
|
| 463 |
) : null}
|
| 464 |
|
| 465 |
<section className="mb-3 avoid-break flex-1 min-h-0 flex flex-col">
|
| 466 |
+
<div className="text-[10px] font-semibold text-gray-800 border-b border-gray-200 pb-1 mb-2">
|
| 467 |
{variant === "photos" ? "Photo Documentation (continued)" : "Photo Documentation"}
|
| 468 |
</div>
|
| 469 |
<div className={`${photoGridClass} flex-1 items-stretch`}>
|
|
|
|
| 476 |
url={photo?.url}
|
| 477 |
label={figureCaption || photo?.name || `Figure ${index + 1}`}
|
| 478 |
className="h-full"
|
|
|
|
| 479 |
/>
|
| 480 |
))
|
| 481 |
)}
|
frontend/src/components/report-editor.js
CHANGED
|
@@ -861,14 +861,16 @@ class ReportEditor extends HTMLElement {
|
|
| 861 |
"figure_caption",
|
| 862 |
fallbackLabel || photo.name || "",
|
| 863 |
"Figure caption",
|
| 864 |
-
"text-[10px] text-gray-600 text-center w-full",
|
|
|
|
| 865 |
false,
|
| 866 |
-
true,
|
| 867 |
);
|
| 868 |
return `
|
| 869 |
-
<figure class="rounded-lg border border-gray-200 bg-gray-50 p-2 break-inside-avoid mb-3">
|
| 870 |
-
<
|
| 871 |
-
|
|
|
|
|
|
|
| 872 |
</figure>
|
| 873 |
`;
|
| 874 |
}
|
|
@@ -983,48 +985,48 @@ class ReportEditor extends HTMLElement {
|
|
| 983 |
const observationsHtml =
|
| 984 |
variant === "full"
|
| 985 |
? `
|
| 986 |
-
<section class="mb-
|
| 987 |
-
<h2 id="observations-title" class="text-
|
| 988 |
Observations and Findings
|
| 989 |
</h2>
|
| 990 |
|
| 991 |
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-
|
| 992 |
<div class="md:col-span-2">
|
| 993 |
-
<div class="grid grid-cols-3 gap-
|
| 994 |
<div class="space-y-0.5">
|
| 995 |
-
<div class="text-
|
| 996 |
-
${this._tplField("reference", reference, "Ref", "text-
|
| 997 |
</div>
|
| 998 |
<div class="space-y-0.5">
|
| 999 |
-
<div class="text-
|
| 1000 |
-
${this._tplField("area", area, "Area", "text-
|
| 1001 |
</div>
|
| 1002 |
<div class="space-y-0.5">
|
| 1003 |
-
<div class="text-
|
| 1004 |
-
${this._tplField("functional_location", functionalLocation, "Location", "text-
|
| 1005 |
</div>
|
| 1006 |
</div>
|
| 1007 |
</div>
|
| 1008 |
|
| 1009 |
<div class="md:col-span-2 flex justify-center">
|
| 1010 |
-
<div class="inline-flex items-center gap-
|
| 1011 |
<div class="text-center space-y-1">
|
| 1012 |
-
<div class="text-
|
| 1013 |
${this._tplSelectField(
|
| 1014 |
"category",
|
| 1015 |
category,
|
| 1016 |
categoryOptions,
|
| 1017 |
-
`min-w-[140px] rounded-md border px-3 py-1 text-
|
| 1018 |
)}
|
| 1019 |
</div>
|
| 1020 |
|
| 1021 |
<div class="text-center space-y-1">
|
| 1022 |
-
<div class="text-
|
| 1023 |
${this._tplSelectField(
|
| 1024 |
"priority",
|
| 1025 |
priority,
|
| 1026 |
priorityOptions,
|
| 1027 |
-
`min-w-[140px] rounded-md border px-3 py-1 text-
|
| 1028 |
)}
|
| 1029 |
</div>
|
| 1030 |
|
|
@@ -1032,19 +1034,19 @@ class ReportEditor extends HTMLElement {
|
|
| 1032 |
</div>
|
| 1033 |
|
| 1034 |
<div class="md:col-span-2 space-y-1">
|
| 1035 |
-
<div class="text-
|
| 1036 |
-
<div class="bg-
|
| 1037 |
-
<div class="text-
|
| 1038 |
-
${this._tplField("item_description", itemDescription, "Item description", "template-field-multiline text-
|
| 1039 |
</div>
|
| 1040 |
</div>
|
| 1041 |
</div>
|
| 1042 |
|
| 1043 |
<div class="md:col-span-2 space-y-1">
|
| 1044 |
-
<div class="text-
|
| 1045 |
-
<div class="bg-
|
| 1046 |
-
<div class="text-
|
| 1047 |
-
${this._tplField("required_action", requiredAction, "Required action", "template-field-multiline text-
|
| 1048 |
</div>
|
| 1049 |
</div>
|
| 1050 |
</div>
|
|
@@ -1060,7 +1062,7 @@ class ReportEditor extends HTMLElement {
|
|
| 1060 |
<header class="mb-3 border-b border-gray-200 pb-2">
|
| 1061 |
<div class="grid grid-cols-[auto,1fr,auto] items-center gap-3">
|
| 1062 |
<div class="flex items-center">
|
| 1063 |
-
<img src="/assets/prosento-logo.png" alt="Prosento logo" class="h-
|
| 1064 |
</div>
|
| 1065 |
|
| 1066 |
<div class="text-center leading-tight">
|
|
@@ -1073,7 +1075,7 @@ class ReportEditor extends HTMLElement {
|
|
| 1073 |
</div>
|
| 1074 |
|
| 1075 |
<div class="flex items-center justify-end">
|
| 1076 |
-
<img src="${this._escape(this._resolveLogoUrl(session, companyLogo))}" alt="Company logo" class="h-
|
| 1077 |
</div>
|
| 1078 |
</div>
|
| 1079 |
</header>
|
|
|
|
| 861 |
"figure_caption",
|
| 862 |
fallbackLabel || photo.name || "",
|
| 863 |
"Figure caption",
|
| 864 |
+
"text-[10px] text-gray-600 text-center w-full break-all leading-tight",
|
| 865 |
+
false,
|
| 866 |
false,
|
|
|
|
| 867 |
);
|
| 868 |
return `
|
| 869 |
+
<figure class="rounded-lg border border-gray-200 bg-gray-50 p-2 pb-3 break-inside-avoid mb-3 flex flex-col gap-1 overflow-hidden">
|
| 870 |
+
<div class="w-full flex-1 flex items-center justify-center">
|
| 871 |
+
<img src="${safeUrl}" alt="${label}" class="w-full object-contain max-h-[240px]" />
|
| 872 |
+
</div>
|
| 873 |
+
<figcaption class="text-[10px] text-gray-600 text-center break-all leading-tight">${caption}</figcaption>
|
| 874 |
</figure>
|
| 875 |
`;
|
| 876 |
}
|
|
|
|
| 985 |
const observationsHtml =
|
| 986 |
variant === "full"
|
| 987 |
? `
|
| 988 |
+
<section class="mb-2" aria-labelledby="observations-title">
|
| 989 |
+
<h2 id="observations-title" class="text-[10px] font-semibold text-gray-800 border-b border-gray-200 pb-1 mb-1">
|
| 990 |
Observations and Findings
|
| 991 |
</h2>
|
| 992 |
|
| 993 |
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
| 994 |
<div class="md:col-span-2">
|
| 995 |
+
<div class="grid grid-cols-3 gap-2">
|
| 996 |
<div class="space-y-0.5">
|
| 997 |
+
<div class="text-[9px] font-medium text-gray-500">Ref</div>
|
| 998 |
+
${this._tplField("reference", reference, "Ref", "text-[10px] font-semibold text-gray-900")}
|
| 999 |
</div>
|
| 1000 |
<div class="space-y-0.5">
|
| 1001 |
+
<div class="text-[9px] font-medium text-gray-500">Area</div>
|
| 1002 |
+
${this._tplField("area", area, "Area", "text-[10px] font-semibold text-gray-900")}
|
| 1003 |
</div>
|
| 1004 |
<div class="space-y-0.5">
|
| 1005 |
+
<div class="text-[9px] font-medium text-gray-500">Location</div>
|
| 1006 |
+
${this._tplField("functional_location", functionalLocation, "Location", "text-[10px] font-semibold text-gray-900")}
|
| 1007 |
</div>
|
| 1008 |
</div>
|
| 1009 |
</div>
|
| 1010 |
|
| 1011 |
<div class="md:col-span-2 flex justify-center">
|
| 1012 |
+
<div class="inline-flex items-center gap-4">
|
| 1013 |
<div class="text-center space-y-1">
|
| 1014 |
+
<div class="text-[9px] font-medium text-gray-500">Category</div>
|
| 1015 |
${this._tplSelectField(
|
| 1016 |
"category",
|
| 1017 |
category,
|
| 1018 |
categoryOptions,
|
| 1019 |
+
`min-w-[140px] rounded-md border px-3 py-1 text-[10px] font-semibold text-center ${categoryBadge.className}`,
|
| 1020 |
)}
|
| 1021 |
</div>
|
| 1022 |
|
| 1023 |
<div class="text-center space-y-1">
|
| 1024 |
+
<div class="text-[9px] font-medium text-gray-500">Priority</div>
|
| 1025 |
${this._tplSelectField(
|
| 1026 |
"priority",
|
| 1027 |
priority,
|
| 1028 |
priorityOptions,
|
| 1029 |
+
`min-w-[140px] rounded-md border px-3 py-1 text-[10px] font-semibold text-center ${priorityBadge.className}`,
|
| 1030 |
)}
|
| 1031 |
</div>
|
| 1032 |
|
|
|
|
| 1034 |
</div>
|
| 1035 |
|
| 1036 |
<div class="md:col-span-2 space-y-1">
|
| 1037 |
+
<div class="text-[9px] font-medium text-gray-500">Condition Description</div>
|
| 1038 |
+
<div class="bg-gray-50 border-l-4 border-gray-300 p-1.5 rounded-sm">
|
| 1039 |
+
<div class="text-gray-700 text-[9px] font-medium leading-snug">
|
| 1040 |
+
${this._tplField("item_description", itemDescription, "Item description", "template-field-multiline text-gray-700 text-[9px] font-medium leading-snug", true)}
|
| 1041 |
</div>
|
| 1042 |
</div>
|
| 1043 |
</div>
|
| 1044 |
|
| 1045 |
<div class="md:col-span-2 space-y-1">
|
| 1046 |
+
<div class="text-[9px] font-medium text-gray-500">Action Required</div>
|
| 1047 |
+
<div class="bg-gray-50 border-l-4 border-gray-300 p-1.5 rounded-sm">
|
| 1048 |
+
<div class="text-gray-700 text-[9px] font-medium leading-snug">
|
| 1049 |
+
${this._tplField("required_action", requiredAction, "Required action", "template-field-multiline text-gray-700 text-[9px] font-medium leading-snug", true)}
|
| 1050 |
</div>
|
| 1051 |
</div>
|
| 1052 |
</div>
|
|
|
|
| 1062 |
<header class="mb-3 border-b border-gray-200 pb-2">
|
| 1063 |
<div class="grid grid-cols-[auto,1fr,auto] items-center gap-3">
|
| 1064 |
<div class="flex items-center">
|
| 1065 |
+
<img src="/assets/prosento-logo.png" alt="Prosento logo" class="h-14 w-auto object-contain" />
|
| 1066 |
</div>
|
| 1067 |
|
| 1068 |
<div class="text-center leading-tight">
|
|
|
|
| 1075 |
</div>
|
| 1076 |
|
| 1077 |
<div class="flex items-center justify-end">
|
| 1078 |
+
<img src="${this._escape(this._resolveLogoUrl(session, companyLogo))}" alt="Company logo" class="h-14 w-auto object-contain" />
|
| 1079 |
</div>
|
| 1080 |
</div>
|
| 1081 |
</header>
|
frontend/src/index.css
CHANGED
|
@@ -23,7 +23,7 @@ body {
|
|
| 23 |
}
|
| 24 |
|
| 25 |
.template-field-multiline {
|
| 26 |
-
min-height:
|
| 27 |
white-space: pre-wrap;
|
| 28 |
}
|
| 29 |
|
|
|
|
| 23 |
}
|
| 24 |
|
| 25 |
.template-field-multiline {
|
| 26 |
+
min-height: 1.8em;
|
| 27 |
white-space: pre-wrap;
|
| 28 |
}
|
| 29 |
|
server/app/services/pdf_reportlab.py
CHANGED
|
@@ -51,6 +51,22 @@ def _wrap_lines(
|
|
| 51 |
lines: List[str] = []
|
| 52 |
current: List[str] = []
|
| 53 |
for word in words:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
test = " ".join(current + [word])
|
| 55 |
if pdf.stringWidth(test, font, size) <= width or not current:
|
| 56 |
current.append(word)
|
|
@@ -130,6 +146,37 @@ def _draw_label_value(
|
|
| 130 |
return y
|
| 131 |
|
| 132 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
def _badge_style(value: str, scale: dict) -> tuple[str, colors.Color, colors.Color]:
|
| 134 |
raw = (value or "").strip()
|
| 135 |
key = raw.upper()
|
|
@@ -220,7 +267,7 @@ def render_report_pdf(
|
|
| 220 |
) -> Path:
|
| 221 |
width, height = A4
|
| 222 |
margin = 10 * mm
|
| 223 |
-
header_h =
|
| 224 |
footer_h = 12 * mm
|
| 225 |
gap = 4 * mm
|
| 226 |
photo_col_gap = 6 * mm
|
|
@@ -378,10 +425,10 @@ def render_report_pdf(
|
|
| 378 |
content_height = content_top - content_bottom
|
| 379 |
|
| 380 |
# Header
|
|
|
|
|
|
|
| 381 |
logo_x = margin
|
| 382 |
-
logo_y = header_y -
|
| 383 |
-
logo_w = 28 * mm
|
| 384 |
-
logo_h = 15 * mm
|
| 385 |
logo_drawn = False
|
| 386 |
if default_logo:
|
| 387 |
logo_drawn = _draw_image_fit(pdf, default_logo, logo_x, logo_y, logo_w, logo_h)
|
|
@@ -398,30 +445,32 @@ def render_report_pdf(
|
|
| 398 |
pdf.rect(logo_x, logo_y, logo_w, logo_h, stroke=1, fill=0)
|
| 399 |
client_logo = _resolve_logo_path(store, session, template.get("company_logo", ""))
|
| 400 |
if client_logo:
|
|
|
|
|
|
|
| 401 |
_draw_image_fit(
|
| 402 |
pdf,
|
| 403 |
client_logo,
|
| 404 |
-
width - margin -
|
| 405 |
-
header_y -
|
| 406 |
-
|
| 407 |
-
|
| 408 |
)
|
| 409 |
pdf.setFillColor(gray_900)
|
| 410 |
pdf.setFont("Helvetica-Bold", 13)
|
| 411 |
pdf.drawCentredString(
|
| 412 |
width / 2,
|
| 413 |
-
header_y -
|
| 414 |
doc_number or "Document No",
|
| 415 |
)
|
| 416 |
pdf.setStrokeColor(gray_200)
|
| 417 |
-
pdf.line(margin, header_y -
|
| 418 |
|
| 419 |
y = content_top
|
| 420 |
|
| 421 |
if variant == "full":
|
| 422 |
# Observations and Findings
|
| 423 |
pdf.setFillColor(gray_800)
|
| 424 |
-
pdf.setFont("Helvetica-Bold",
|
| 425 |
pdf.drawString(margin, y, "Observations and Findings")
|
| 426 |
pdf.setStrokeColor(gray_200)
|
| 427 |
pdf.line(margin, y - 2, width - margin, y - 2)
|
|
@@ -433,70 +482,78 @@ def render_report_pdf(
|
|
| 433 |
item_desc = _safe_text(template.get("item_description"))
|
| 434 |
required_action = _safe_text(template.get("required_action"))
|
| 435 |
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
|
|
|
|
|
|
|
|
|
| 440 |
|
| 441 |
-
label_size =
|
| 442 |
-
value_size =
|
| 443 |
-
value_gap =
|
| 444 |
-
row_gap =
|
| 445 |
-
leading =
|
| 446 |
|
| 447 |
row_y = y
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
row_y,
|
| 467 |
-
"Helvetica",
|
| 468 |
-
"Helvetica-Bold",
|
| 469 |
-
label_size,
|
| 470 |
-
value_size,
|
| 471 |
-
gray_500,
|
| 472 |
-
gray_900,
|
| 473 |
)
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 486 |
)
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 497 |
)
|
| 498 |
|
| 499 |
-
y = row_y - (label_size + value_gap + leading * max(1,
|
| 500 |
|
| 501 |
category = _safe_text(template.get("category"))
|
| 502 |
priority = _safe_text(template.get("priority"))
|
|
@@ -520,54 +577,58 @@ def render_report_pdf(
|
|
| 520 |
|
| 521 |
badge_w = 40 * mm
|
| 522 |
badge_h = 10 * mm
|
| 523 |
-
y -=
|
| 524 |
pdf.setFillColor(gray_500)
|
| 525 |
-
pdf.setFont("Helvetica",
|
| 526 |
-
|
| 527 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 528 |
label_y = y
|
| 529 |
pdf.drawCentredString(cat_label_x, label_y, "Category")
|
| 530 |
pdf.drawCentredString(pr_label_x, label_y, "Priority")
|
| 531 |
-
y -=
|
| 532 |
pdf.setFillColor(cat_bg)
|
| 533 |
pdf.setStrokeColor(gray_200)
|
| 534 |
-
pdf.roundRect(
|
| 535 |
pdf.setFillColor(cat_text_color)
|
| 536 |
-
pdf.setFont("Helvetica-Bold",
|
| 537 |
-
pdf.drawCentredString(cat_label_x, y - 2 + badge_h / 2 -
|
| 538 |
pdf.setFillColor(pr_bg)
|
| 539 |
-
pdf.roundRect(
|
| 540 |
pdf.setFillColor(pr_text_color)
|
| 541 |
-
pdf.drawCentredString(pr_label_x, y - 2 + badge_h / 2 -
|
| 542 |
-
y -=
|
| 543 |
|
| 544 |
condition = item_desc
|
| 545 |
action = required_action
|
| 546 |
|
| 547 |
pdf.setFillColor(gray_500)
|
| 548 |
-
pdf.setFont("Helvetica",
|
| 549 |
pdf.drawCentredString(
|
| 550 |
margin + (width - 2 * margin) / 2, y, "Condition Description"
|
| 551 |
)
|
| 552 |
-
y -=
|
| 553 |
-
pdf.setFillColor(
|
| 554 |
-
pdf.setStrokeColor(
|
| 555 |
cond_lines = _wrap_lines(
|
| 556 |
pdf,
|
| 557 |
condition or "-",
|
| 558 |
width - 2 * margin - 4 * mm,
|
| 559 |
4,
|
| 560 |
-
"Helvetica
|
| 561 |
-
|
| 562 |
)
|
| 563 |
-
cond_h = max(
|
| 564 |
cond_bottom = y - cond_h
|
| 565 |
pdf.rect(margin, cond_bottom, width - 2 * margin, cond_h, stroke=1, fill=1)
|
| 566 |
-
pdf.setLineWidth(
|
| 567 |
pdf.line(margin, cond_bottom, margin, y)
|
| 568 |
pdf.setLineWidth(1)
|
| 569 |
-
pdf.setFillColor(
|
| 570 |
-
pdf.setFont("Helvetica-Bold", 11)
|
| 571 |
text_center_x = margin + (width - 2 * margin) / 2
|
| 572 |
_draw_centered_block(
|
| 573 |
pdf,
|
|
@@ -577,34 +638,33 @@ def render_report_pdf(
|
|
| 577 |
cond_h,
|
| 578 |
leading,
|
| 579 |
"Helvetica-Bold",
|
| 580 |
-
|
| 581 |
)
|
| 582 |
-
y = cond_bottom -
|
| 583 |
|
| 584 |
pdf.setFillColor(gray_500)
|
| 585 |
-
pdf.setFont("Helvetica",
|
| 586 |
pdf.drawCentredString(
|
| 587 |
margin + (width - 2 * margin) / 2, y, "Required Action"
|
| 588 |
)
|
| 589 |
-
y -=
|
| 590 |
-
pdf.setFillColor(
|
| 591 |
-
pdf.setStrokeColor(
|
| 592 |
action_lines = _wrap_lines(
|
| 593 |
pdf,
|
| 594 |
action or "-",
|
| 595 |
width - 2 * margin - 4 * mm,
|
| 596 |
4,
|
| 597 |
-
"Helvetica
|
| 598 |
-
|
| 599 |
)
|
| 600 |
-
action_h = max(
|
| 601 |
action_bottom = y - action_h
|
| 602 |
pdf.rect(margin, action_bottom, width - 2 * margin, action_h, stroke=1, fill=1)
|
| 603 |
-
pdf.setLineWidth(
|
| 604 |
pdf.line(margin, action_bottom, margin, y)
|
| 605 |
pdf.setLineWidth(1)
|
| 606 |
-
pdf.setFillColor(
|
| 607 |
-
pdf.setFont("Helvetica-Bold", 11)
|
| 608 |
text_center_x = margin + (width - 2 * margin) / 2
|
| 609 |
_draw_centered_block(
|
| 610 |
pdf,
|
|
@@ -614,12 +674,12 @@ def render_report_pdf(
|
|
| 614 |
action_h,
|
| 615 |
leading,
|
| 616 |
"Helvetica-Bold",
|
| 617 |
-
|
| 618 |
)
|
| 619 |
-
y = action_bottom -
|
| 620 |
else:
|
| 621 |
pdf.setFillColor(gray_800)
|
| 622 |
-
pdf.setFont("Helvetica-Bold",
|
| 623 |
pdf.drawString(margin, y, "Photo Documentation (continued)")
|
| 624 |
pdf.setStrokeColor(gray_200)
|
| 625 |
pdf.line(margin, y - 2, width - margin, y - 2)
|
|
@@ -628,7 +688,7 @@ def render_report_pdf(
|
|
| 628 |
if variant == "full":
|
| 629 |
y -= 2 * mm
|
| 630 |
pdf.setFillColor(gray_800)
|
| 631 |
-
pdf.setFont("Helvetica-Bold",
|
| 632 |
pdf.drawString(margin, y, "Photo Documentation")
|
| 633 |
pdf.setStrokeColor(gray_200)
|
| 634 |
pdf.line(margin, y - 2, width - margin, y - 2)
|
|
@@ -653,14 +713,43 @@ def render_report_pdf(
|
|
| 653 |
pdf.setStrokeColor(gray_200)
|
| 654 |
pdf.setFillColor(gray_50)
|
| 655 |
pdf.roundRect(x, y, cell_w, cell_h, 3 * mm, stroke=1, fill=1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 656 |
_draw_image_fit(
|
| 657 |
-
pdf,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 658 |
)
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 664 |
)
|
| 665 |
else:
|
| 666 |
pdf.setFont("Helvetica", 11)
|
|
@@ -675,7 +764,7 @@ def render_report_pdf(
|
|
| 675 |
pdf.drawCentredString(
|
| 676 |
width / 2,
|
| 677 |
footer_y + 4 * mm,
|
| 678 |
-
"Prosento - (c) 2026 All Rights Reserved
|
| 679 |
)
|
| 680 |
page_line = f"Page {output_index + 1} of {total_pages}"
|
| 681 |
if section_label:
|
|
|
|
| 51 |
lines: List[str] = []
|
| 52 |
current: List[str] = []
|
| 53 |
for word in words:
|
| 54 |
+
if pdf.stringWidth(word, font, size) > width:
|
| 55 |
+
if current:
|
| 56 |
+
lines.append(" ".join(current))
|
| 57 |
+
current = []
|
| 58 |
+
if len(lines) >= max_lines:
|
| 59 |
+
break
|
| 60 |
+
remaining = word
|
| 61 |
+
while remaining and len(lines) < max_lines:
|
| 62 |
+
cut = len(remaining)
|
| 63 |
+
while cut > 1 and pdf.stringWidth(remaining[:cut], font, size) > width:
|
| 64 |
+
cut -= 1
|
| 65 |
+
if cut <= 1:
|
| 66 |
+
cut = 1
|
| 67 |
+
lines.append(remaining[:cut])
|
| 68 |
+
remaining = remaining[cut:]
|
| 69 |
+
continue
|
| 70 |
test = " ".join(current + [word])
|
| 71 |
if pdf.stringWidth(test, font, size) <= width or not current:
|
| 72 |
current.append(word)
|
|
|
|
| 146 |
return y
|
| 147 |
|
| 148 |
|
| 149 |
+
def _draw_label_value_centered(
|
| 150 |
+
pdf: canvas.Canvas,
|
| 151 |
+
label: str,
|
| 152 |
+
value: str,
|
| 153 |
+
x_center: float,
|
| 154 |
+
y: float,
|
| 155 |
+
label_font: str,
|
| 156 |
+
value_font: str,
|
| 157 |
+
label_size: int,
|
| 158 |
+
value_size: int,
|
| 159 |
+
label_color: colors.Color,
|
| 160 |
+
value_color: colors.Color,
|
| 161 |
+
max_width: float,
|
| 162 |
+
max_lines: int,
|
| 163 |
+
leading: float,
|
| 164 |
+
) -> int:
|
| 165 |
+
pdf.setFillColor(label_color)
|
| 166 |
+
pdf.setFont(label_font, label_size)
|
| 167 |
+
pdf.drawCentredString(x_center, y, label)
|
| 168 |
+
y -= label_size + 1
|
| 169 |
+
pdf.setFillColor(value_color)
|
| 170 |
+
pdf.setFont(value_font, value_size)
|
| 171 |
+
lines = _wrap_lines(pdf, value or "-", max_width, max_lines, value_font, value_size)
|
| 172 |
+
if not lines:
|
| 173 |
+
lines = ["-"]
|
| 174 |
+
for line in lines:
|
| 175 |
+
pdf.drawCentredString(x_center, y, line)
|
| 176 |
+
y -= leading
|
| 177 |
+
return len(lines)
|
| 178 |
+
|
| 179 |
+
|
| 180 |
def _badge_style(value: str, scale: dict) -> tuple[str, colors.Color, colors.Color]:
|
| 181 |
raw = (value or "").strip()
|
| 182 |
key = raw.upper()
|
|
|
|
| 267 |
) -> Path:
|
| 268 |
width, height = A4
|
| 269 |
margin = 10 * mm
|
| 270 |
+
header_h = 26 * mm
|
| 271 |
footer_h = 12 * mm
|
| 272 |
gap = 4 * mm
|
| 273 |
photo_col_gap = 6 * mm
|
|
|
|
| 425 |
content_height = content_top - content_bottom
|
| 426 |
|
| 427 |
# Header
|
| 428 |
+
logo_w = 40 * mm
|
| 429 |
+
logo_h = 20 * mm
|
| 430 |
logo_x = margin
|
| 431 |
+
logo_y = header_y - (header_h + logo_h) / 2
|
|
|
|
|
|
|
| 432 |
logo_drawn = False
|
| 433 |
if default_logo:
|
| 434 |
logo_drawn = _draw_image_fit(pdf, default_logo, logo_x, logo_y, logo_w, logo_h)
|
|
|
|
| 445 |
pdf.rect(logo_x, logo_y, logo_w, logo_h, stroke=1, fill=0)
|
| 446 |
client_logo = _resolve_logo_path(store, session, template.get("company_logo", ""))
|
| 447 |
if client_logo:
|
| 448 |
+
client_logo_w = 40 * mm
|
| 449 |
+
client_logo_h = 20 * mm
|
| 450 |
_draw_image_fit(
|
| 451 |
pdf,
|
| 452 |
client_logo,
|
| 453 |
+
width - margin - client_logo_w,
|
| 454 |
+
header_y - (header_h + client_logo_h) / 2,
|
| 455 |
+
client_logo_w,
|
| 456 |
+
client_logo_h,
|
| 457 |
)
|
| 458 |
pdf.setFillColor(gray_900)
|
| 459 |
pdf.setFont("Helvetica-Bold", 13)
|
| 460 |
pdf.drawCentredString(
|
| 461 |
width / 2,
|
| 462 |
+
header_y - header_h / 2 + 2 * mm,
|
| 463 |
doc_number or "Document No",
|
| 464 |
)
|
| 465 |
pdf.setStrokeColor(gray_200)
|
| 466 |
+
pdf.line(margin, header_y - header_h + 3 * mm, width - margin, header_y - header_h + 3 * mm)
|
| 467 |
|
| 468 |
y = content_top
|
| 469 |
|
| 470 |
if variant == "full":
|
| 471 |
# Observations and Findings
|
| 472 |
pdf.setFillColor(gray_800)
|
| 473 |
+
pdf.setFont("Helvetica-Bold", 12)
|
| 474 |
pdf.drawString(margin, y, "Observations and Findings")
|
| 475 |
pdf.setStrokeColor(gray_200)
|
| 476 |
pdf.line(margin, y - 2, width - margin, y - 2)
|
|
|
|
| 482 |
item_desc = _safe_text(template.get("item_description"))
|
| 483 |
required_action = _safe_text(template.get("required_action"))
|
| 484 |
|
| 485 |
+
total_w = width - 2 * margin
|
| 486 |
+
col_w = total_w / 3
|
| 487 |
+
col_centers = [
|
| 488 |
+
margin + col_w / 2,
|
| 489 |
+
margin + col_w * 1.5,
|
| 490 |
+
margin + col_w * 2.5,
|
| 491 |
+
]
|
| 492 |
|
| 493 |
+
label_size = 9
|
| 494 |
+
value_size = 10
|
| 495 |
+
value_gap = 1.5 * mm
|
| 496 |
+
row_gap = 3 * mm
|
| 497 |
+
leading = 11
|
| 498 |
|
| 499 |
row_y = y
|
| 500 |
+
line_counts = []
|
| 501 |
+
line_counts.append(
|
| 502 |
+
_draw_label_value_centered(
|
| 503 |
+
pdf,
|
| 504 |
+
"Ref",
|
| 505 |
+
ref,
|
| 506 |
+
col_centers[0],
|
| 507 |
+
row_y,
|
| 508 |
+
"Helvetica",
|
| 509 |
+
"Helvetica-Bold",
|
| 510 |
+
label_size,
|
| 511 |
+
value_size,
|
| 512 |
+
gray_500,
|
| 513 |
+
gray_900,
|
| 514 |
+
col_w - 4 * mm,
|
| 515 |
+
2,
|
| 516 |
+
leading,
|
| 517 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 518 |
)
|
| 519 |
+
line_counts.append(
|
| 520 |
+
_draw_label_value_centered(
|
| 521 |
+
pdf,
|
| 522 |
+
"Area",
|
| 523 |
+
area,
|
| 524 |
+
col_centers[1],
|
| 525 |
+
row_y,
|
| 526 |
+
"Helvetica",
|
| 527 |
+
"Helvetica-Bold",
|
| 528 |
+
label_size,
|
| 529 |
+
value_size,
|
| 530 |
+
gray_500,
|
| 531 |
+
gray_900,
|
| 532 |
+
col_w - 4 * mm,
|
| 533 |
+
2,
|
| 534 |
+
leading,
|
| 535 |
+
)
|
| 536 |
)
|
| 537 |
+
line_counts.append(
|
| 538 |
+
_draw_label_value_centered(
|
| 539 |
+
pdf,
|
| 540 |
+
"Location",
|
| 541 |
+
location,
|
| 542 |
+
col_centers[2],
|
| 543 |
+
row_y,
|
| 544 |
+
"Helvetica",
|
| 545 |
+
"Helvetica-Bold",
|
| 546 |
+
label_size,
|
| 547 |
+
value_size,
|
| 548 |
+
gray_500,
|
| 549 |
+
gray_900,
|
| 550 |
+
col_w - 4 * mm,
|
| 551 |
+
2,
|
| 552 |
+
leading,
|
| 553 |
+
)
|
| 554 |
)
|
| 555 |
|
| 556 |
+
y = row_y - (label_size + value_gap + leading * max(1, max(line_counts))) - row_gap
|
| 557 |
|
| 558 |
category = _safe_text(template.get("category"))
|
| 559 |
priority = _safe_text(template.get("priority"))
|
|
|
|
| 577 |
|
| 578 |
badge_w = 40 * mm
|
| 579 |
badge_h = 10 * mm
|
| 580 |
+
y -= 1 * mm
|
| 581 |
pdf.setFillColor(gray_500)
|
| 582 |
+
pdf.setFont("Helvetica", 9)
|
| 583 |
+
badge_gap = 20 * mm
|
| 584 |
+
total_badge_w = badge_w * 2 + badge_gap
|
| 585 |
+
start_x = (width - total_badge_w) / 2
|
| 586 |
+
cat_x = start_x
|
| 587 |
+
pr_x = start_x + badge_w + badge_gap
|
| 588 |
+
cat_label_x = cat_x + badge_w / 2
|
| 589 |
+
pr_label_x = pr_x + badge_w / 2
|
| 590 |
label_y = y
|
| 591 |
pdf.drawCentredString(cat_label_x, label_y, "Category")
|
| 592 |
pdf.drawCentredString(pr_label_x, label_y, "Priority")
|
| 593 |
+
y -= 10 * mm
|
| 594 |
pdf.setFillColor(cat_bg)
|
| 595 |
pdf.setStrokeColor(gray_200)
|
| 596 |
+
pdf.roundRect(cat_x, y - 2, badge_w, badge_h, 2 * mm, stroke=1, fill=1)
|
| 597 |
pdf.setFillColor(cat_text_color)
|
| 598 |
+
pdf.setFont("Helvetica-Bold", 10)
|
| 599 |
+
pdf.drawCentredString(cat_label_x, y - 2 + badge_h / 2 - 3, cat_text)
|
| 600 |
pdf.setFillColor(pr_bg)
|
| 601 |
+
pdf.roundRect(pr_x, y - 2, badge_w, badge_h, 2 * mm, stroke=1, fill=1)
|
| 602 |
pdf.setFillColor(pr_text_color)
|
| 603 |
+
pdf.drawCentredString(pr_label_x, y - 2 + badge_h / 2 - 3, pr_text)
|
| 604 |
+
y -= 8 * mm
|
| 605 |
|
| 606 |
condition = item_desc
|
| 607 |
action = required_action
|
| 608 |
|
| 609 |
pdf.setFillColor(gray_500)
|
| 610 |
+
pdf.setFont("Helvetica", 9)
|
| 611 |
pdf.drawCentredString(
|
| 612 |
margin + (width - 2 * margin) / 2, y, "Condition Description"
|
| 613 |
)
|
| 614 |
+
y -= 3 * mm
|
| 615 |
+
pdf.setFillColor(gray_50)
|
| 616 |
+
pdf.setStrokeColor(gray_200)
|
| 617 |
cond_lines = _wrap_lines(
|
| 618 |
pdf,
|
| 619 |
condition or "-",
|
| 620 |
width - 2 * margin - 4 * mm,
|
| 621 |
4,
|
| 622 |
+
"Helvetica",
|
| 623 |
+
9,
|
| 624 |
)
|
| 625 |
+
cond_h = max(10 * mm, (len(cond_lines) or 1) * leading + 4 * mm)
|
| 626 |
cond_bottom = y - cond_h
|
| 627 |
pdf.rect(margin, cond_bottom, width - 2 * margin, cond_h, stroke=1, fill=1)
|
| 628 |
+
pdf.setLineWidth(2)
|
| 629 |
pdf.line(margin, cond_bottom, margin, y)
|
| 630 |
pdf.setLineWidth(1)
|
| 631 |
+
pdf.setFillColor(gray_700)
|
|
|
|
| 632 |
text_center_x = margin + (width - 2 * margin) / 2
|
| 633 |
_draw_centered_block(
|
| 634 |
pdf,
|
|
|
|
| 638 |
cond_h,
|
| 639 |
leading,
|
| 640 |
"Helvetica-Bold",
|
| 641 |
+
10,
|
| 642 |
)
|
| 643 |
+
y = cond_bottom - 4 * mm
|
| 644 |
|
| 645 |
pdf.setFillColor(gray_500)
|
| 646 |
+
pdf.setFont("Helvetica", 9)
|
| 647 |
pdf.drawCentredString(
|
| 648 |
margin + (width - 2 * margin) / 2, y, "Required Action"
|
| 649 |
)
|
| 650 |
+
y -= 3 * mm
|
| 651 |
+
pdf.setFillColor(gray_50)
|
| 652 |
+
pdf.setStrokeColor(gray_200)
|
| 653 |
action_lines = _wrap_lines(
|
| 654 |
pdf,
|
| 655 |
action or "-",
|
| 656 |
width - 2 * margin - 4 * mm,
|
| 657 |
4,
|
| 658 |
+
"Helvetica",
|
| 659 |
+
9,
|
| 660 |
)
|
| 661 |
+
action_h = max(10 * mm, (len(action_lines) or 1) * leading + 4 * mm)
|
| 662 |
action_bottom = y - action_h
|
| 663 |
pdf.rect(margin, action_bottom, width - 2 * margin, action_h, stroke=1, fill=1)
|
| 664 |
+
pdf.setLineWidth(2)
|
| 665 |
pdf.line(margin, action_bottom, margin, y)
|
| 666 |
pdf.setLineWidth(1)
|
| 667 |
+
pdf.setFillColor(gray_700)
|
|
|
|
| 668 |
text_center_x = margin + (width - 2 * margin) / 2
|
| 669 |
_draw_centered_block(
|
| 670 |
pdf,
|
|
|
|
| 674 |
action_h,
|
| 675 |
leading,
|
| 676 |
"Helvetica-Bold",
|
| 677 |
+
10,
|
| 678 |
)
|
| 679 |
+
y = action_bottom - 4 * mm
|
| 680 |
else:
|
| 681 |
pdf.setFillColor(gray_800)
|
| 682 |
+
pdf.setFont("Helvetica-Bold", 12)
|
| 683 |
pdf.drawString(margin, y, "Photo Documentation (continued)")
|
| 684 |
pdf.setStrokeColor(gray_200)
|
| 685 |
pdf.line(margin, y - 2, width - margin, y - 2)
|
|
|
|
| 688 |
if variant == "full":
|
| 689 |
y -= 2 * mm
|
| 690 |
pdf.setFillColor(gray_800)
|
| 691 |
+
pdf.setFont("Helvetica-Bold", 12)
|
| 692 |
pdf.drawString(margin, y, "Photo Documentation")
|
| 693 |
pdf.setStrokeColor(gray_200)
|
| 694 |
pdf.line(margin, y - 2, width - margin, y - 2)
|
|
|
|
| 713 |
pdf.setStrokeColor(gray_200)
|
| 714 |
pdf.setFillColor(gray_50)
|
| 715 |
pdf.roundRect(x, y, cell_w, cell_h, 3 * mm, stroke=1, fill=1)
|
| 716 |
+
|
| 717 |
+
caption_text = f"Fig {idx + 1}: {label}" if label else ""
|
| 718 |
+
caption_font = "Helvetica"
|
| 719 |
+
caption_size = 9
|
| 720 |
+
caption_leading = 10
|
| 721 |
+
caption_lines = _wrap_lines(
|
| 722 |
+
pdf,
|
| 723 |
+
caption_text,
|
| 724 |
+
cell_w - 6 * mm,
|
| 725 |
+
2,
|
| 726 |
+
caption_font,
|
| 727 |
+
caption_size,
|
| 728 |
+
)
|
| 729 |
+
caption_h = max(6 * mm, max(1, len(caption_lines)) * caption_leading)
|
| 730 |
+
image_y = y + caption_h + 2 * mm
|
| 731 |
+
image_h = cell_h - caption_h - 6 * mm
|
| 732 |
+
if image_h < 10 * mm:
|
| 733 |
+
image_h = 10 * mm
|
| 734 |
_draw_image_fit(
|
| 735 |
+
pdf,
|
| 736 |
+
photo_path,
|
| 737 |
+
x + 2 * mm,
|
| 738 |
+
image_y,
|
| 739 |
+
cell_w - 4 * mm,
|
| 740 |
+
image_h,
|
| 741 |
)
|
| 742 |
+
if caption_lines:
|
| 743 |
+
pdf.setFillColor(gray_500)
|
| 744 |
+
_draw_centered_block(
|
| 745 |
+
pdf,
|
| 746 |
+
caption_lines,
|
| 747 |
+
x + cell_w / 2,
|
| 748 |
+
y + 2 * mm,
|
| 749 |
+
caption_h,
|
| 750 |
+
caption_leading,
|
| 751 |
+
caption_font,
|
| 752 |
+
caption_size,
|
| 753 |
)
|
| 754 |
else:
|
| 755 |
pdf.setFont("Helvetica", 11)
|
|
|
|
| 764 |
pdf.drawCentredString(
|
| 765 |
width / 2,
|
| 766 |
footer_y + 4 * mm,
|
| 767 |
+
"Prosento - (c) 2026 All Rights Reserved",
|
| 768 |
)
|
| 769 |
page_line = f"Page {output_index + 1} of {total_pages}"
|
| 770 |
if section_label:
|