ChristopherJKoen commited on
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: 2.4em;
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-4 border-b border-gray-200 pb-3">
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-10 w-auto object-contain"
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-10 w-auto object-contain"
77
  loading="eager"
78
  />
79
  </div>
@@ -81,47 +80,47 @@
81
  </header>
82
 
83
  <!-- Observations and Findings -->
84
- <section class="mb-4" aria-labelledby="observations-title">
85
- <h2 id="observations-title" class="text-base font-semibold text-gray-800 border-b border-gray-200 pb-1 mb-2">
86
  Observations and Findings
87
  </h2>
88
 
89
- <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
90
  <div class="md:col-span-2">
91
- <div class="grid grid-cols-3 gap-3">
92
  <div class="space-y-0.5">
93
- <div class="text-xs font-medium text-gray-500">Ref</div>
94
- <div class="template-field text-sm font-semibold text-gray-900" contenteditable="true" data-placeholder="Ref"></div>
95
  </div>
96
 
97
  <div class="space-y-0.5">
98
- <div class="text-xs font-medium text-gray-500">Area</div>
99
- <div class="template-field text-sm font-semibold text-gray-900" contenteditable="true" data-placeholder="Area"></div>
100
  </div>
101
 
102
  <div class="space-y-0.5">
103
- <div class="text-xs font-medium text-gray-500">Location</div>
104
- <div class="template-field text-sm font-semibold text-gray-900" contenteditable="true" data-placeholder="Location"></div>
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-6">
112
  <div class="text-center space-y-1">
113
- <div class="text-xs font-medium text-gray-500">Category</div>
114
  <span
115
- class="template-field inline-flex items-center justify-center rounded-md border px-3 py-1 text-sm font-semibold min-w-[120px] bg-yellow-200 text-yellow-800 border-yellow-200"
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-xs font-medium text-gray-500">Priority</div>
123
  <span
124
- class="template-field inline-flex items-center justify-center rounded-md border px-3 py-1 text-sm font-semibold min-w-[120px] bg-green-200 text-green-800 border-green-200"
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-xs font-medium text-gray-500">Condition Description</div>
136
- <div class="bg-amber-50 border-l-4 border-amber-300 p-2 rounded-sm">
137
- <p class="template-field template-field-multiline text-amber-800 text-sm font-semibold leading-snug" contenteditable="true" data-placeholder="Item description"></p>
138
  </div>
139
  </div>
140
 
141
  <!-- Full-width: Action Required -->
142
  <div class="md:col-span-2 space-y-1">
143
- <div class="text-xs font-medium text-gray-500">Action Required</div>
144
- <div class="bg-blue-50 border-l-4 border-blue-300 p-2 rounded-sm">
145
- <p class="template-field template-field-multiline text-blue-800 text-sm font-semibold leading-snug" contenteditable="true" data-placeholder="Required action"></p>
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 overflow-hidden break-inside-avoid mb-3">
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-[11px] text-gray-600 mt-1 text-center leading-tight">
163
  Figure 1
164
  </figcaption>
165
  </figure>
166
 
167
- <figure class="border border-gray-200 rounded-lg p-2 bg-gray-50 overflow-hidden break-inside-avoid mb-3">
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-[11px] text-gray-600 mt-1 text-center leading-tight">
172
  Figure 2
173
  </figcaption>
174
  </figure>
175
 
176
- <figure class="border border-gray-200 rounded-lg p-2 bg-gray-50 overflow-hidden break-inside-avoid mb-3">
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-[11px] text-gray-600 mt-1 text-center leading-tight">
181
  Figure 3
182
  </figcaption>
183
  </figure>
184
 
185
- <figure class="border border-gray-200 rounded-lg p-2 bg-gray-50 overflow-hidden break-inside-avoid mb-3">
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-[11px] text-gray-600 mt-1 text-center leading-tight">
190
  Figure 4
191
  </figcaption>
192
  </figure>
193
 
194
- <figure class="border border-gray-200 rounded-lg p-2 bg-gray-50 overflow-hidden break-inside-avoid mb-3">
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-[11px] text-gray-600 mt-1 text-center leading-tight">
199
  Figure 5
200
  </figcaption>
201
  </figure>
202
 
203
- <figure class="border border-gray-200 rounded-lg p-2 bg-gray-50 overflow-hidden break-inside-avoid mb-3">
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-[11px] text-gray-600 mt-1 text-center leading-tight">
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
- <img
247
- src={url}
248
- alt={label}
249
- className={["w-full h-full object-contain", imageClassName].join(" ")}
250
- loading="eager"
251
- />
252
- <figcaption className="mt-1 text-[10px] text-gray-600 text-center">
 
 
 
 
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-9 w-auto object-contain"
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-9 w-auto object-contain"
366
  />
367
  </div>
368
  </header>
369
 
370
  {variant === "full" ? (
371
- <section className="mb-3" aria-labelledby="observations-title">
372
  <h2
373
  id="observations-title"
374
- className="text-[11px] font-semibold text-gray-800 border-b border-gray-200 pb-1 mb-2"
375
  >
376
  Observations and Findings
377
  </h2>
378
 
379
- <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
380
  <div className="md:col-span-2">
381
- <div className="grid grid-cols-3 gap-3">
382
  <div className="space-y-0.5">
383
- <div className="text-[10px] font-medium text-gray-500">Ref</div>
384
- <div className="template-field text-[11px] font-semibold text-gray-900">
385
  {reference}
386
  </div>
387
  </div>
388
  <div className="space-y-0.5">
389
- <div className="text-[10px] font-medium text-gray-500">Area</div>
390
- <div className="template-field text-[11px] font-semibold text-gray-900">
391
  {area}
392
  </div>
393
  </div>
394
  <div className="space-y-0.5">
395
- <div className="text-[10px] font-medium text-gray-500">Location</div>
396
- <div className="template-field text-[11px] font-semibold text-gray-900">
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-6">
405
  <div className="text-center space-y-1">
406
- <div className="text-[10px] font-medium text-gray-500">
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-[11px] font-semibold min-w-[120px]",
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-[10px] font-medium text-gray-500">
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-[11px] font-semibold min-w-[120px]",
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-[10px] font-medium text-gray-500">
438
  Condition Description
439
  </div>
440
- <div className="bg-amber-50 border-l-4 border-amber-300 p-2 rounded-sm">
441
- <p className="template-field template-field-multiline text-amber-800 text-[11px] font-semibold leading-snug">
442
  {conditionText}
443
  </p>
444
  </div>
445
  </div>
446
 
447
  <div className="md:col-span-2 space-y-1">
448
- <div className="text-[10px] font-medium text-gray-500">
449
  Action Required
450
  </div>
451
- <div className="bg-blue-50 border-l-4 border-blue-300 p-2 rounded-sm">
452
- <p className="template-field template-field-multiline text-blue-800 text-[11px] font-semibold leading-snug">
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-[11px] font-semibold text-gray-800 border-b border-gray-200 pb-1 mb-2">
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
- <img src="${safeUrl}" alt="${label}" class="w-full h-auto object-contain" />
871
- <figcaption class="mt-1 text-[10px] text-gray-600 text-center">${caption}</figcaption>
 
 
872
  </figure>
873
  `;
874
  }
@@ -983,48 +985,48 @@ class ReportEditor extends HTMLElement {
983
  const observationsHtml =
984
  variant === "full"
985
  ? `
986
- <section class="mb-4" aria-labelledby="observations-title">
987
- <h2 id="observations-title" class="text-base font-semibold text-gray-800 border-b border-gray-200 pb-1 mb-2">
988
  Observations and Findings
989
  </h2>
990
 
991
- <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
992
  <div class="md:col-span-2">
993
- <div class="grid grid-cols-3 gap-3">
994
  <div class="space-y-0.5">
995
- <div class="text-xs font-medium text-gray-500">Ref</div>
996
- ${this._tplField("reference", reference, "Ref", "text-sm font-semibold text-gray-900")}
997
  </div>
998
  <div class="space-y-0.5">
999
- <div class="text-xs font-medium text-gray-500">Area</div>
1000
- ${this._tplField("area", area, "Area", "text-sm font-semibold text-gray-900")}
1001
  </div>
1002
  <div class="space-y-0.5">
1003
- <div class="text-xs font-medium text-gray-500">Location</div>
1004
- ${this._tplField("functional_location", functionalLocation, "Location", "text-sm font-semibold text-gray-900")}
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-6">
1011
  <div class="text-center space-y-1">
1012
- <div class="text-xs font-medium text-gray-500">Category</div>
1013
  ${this._tplSelectField(
1014
  "category",
1015
  category,
1016
  categoryOptions,
1017
- `min-w-[140px] rounded-md border px-3 py-1 text-sm font-semibold text-center ${categoryBadge.className}`,
1018
  )}
1019
  </div>
1020
 
1021
  <div class="text-center space-y-1">
1022
- <div class="text-xs font-medium text-gray-500">Priority</div>
1023
  ${this._tplSelectField(
1024
  "priority",
1025
  priority,
1026
  priorityOptions,
1027
- `min-w-[140px] rounded-md border px-3 py-1 text-sm font-semibold text-center ${priorityBadge.className}`,
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-xs font-medium text-gray-500">Condition Description</div>
1036
- <div class="bg-amber-50 border-l-4 border-amber-300 p-2 rounded-sm">
1037
- <div class="text-amber-800 text-sm font-semibold leading-snug">
1038
- ${this._tplField("item_description", itemDescription, "Item description", "template-field-multiline text-amber-800 text-sm font-semibold leading-snug", true)}
1039
  </div>
1040
  </div>
1041
  </div>
1042
 
1043
  <div class="md:col-span-2 space-y-1">
1044
- <div class="text-xs font-medium text-gray-500">Action Required</div>
1045
- <div class="bg-blue-50 border-l-4 border-blue-300 p-2 rounded-sm">
1046
- <div class="text-blue-800 text-sm font-semibold leading-snug">
1047
- ${this._tplField("required_action", requiredAction, "Required action", "template-field-multiline text-blue-800 text-sm font-semibold leading-snug", true)}
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-10 w-auto object-contain" />
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-10 w-auto object-contain" />
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: 2.4em;
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 = 20 * mm
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 - 15 * mm
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 - 32 * mm,
405
- header_y - 15 * mm,
406
- 32 * mm,
407
- 15 * mm,
408
  )
409
  pdf.setFillColor(gray_900)
410
  pdf.setFont("Helvetica-Bold", 13)
411
  pdf.drawCentredString(
412
  width / 2,
413
- header_y - 7 * mm,
414
  doc_number or "Document No",
415
  )
416
  pdf.setStrokeColor(gray_200)
417
- pdf.line(margin, header_y - 17 * mm, width - margin, header_y - 17 * mm)
418
 
419
  y = content_top
420
 
421
  if variant == "full":
422
  # Observations and Findings
423
  pdf.setFillColor(gray_800)
424
- pdf.setFont("Helvetica-Bold", 14)
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
- left_w = (width - 2 * margin) * 0.6
437
- right_w = (width - 2 * margin) * 0.4
438
- left_x = margin
439
- right_x = margin + left_w + 4 * mm
 
 
 
440
 
441
- label_size = 11
442
- value_size = 12
443
- value_gap = 2 * mm
444
- row_gap = 4 * mm
445
- leading = 14
446
 
447
  row_y = y
448
- _draw_label_value(
449
- pdf,
450
- "Ref",
451
- ref,
452
- left_x,
453
- row_y,
454
- "Helvetica",
455
- "Helvetica-Bold",
456
- label_size,
457
- value_size,
458
- gray_500,
459
- gray_900,
460
- )
461
- _draw_label_value(
462
- pdf,
463
- "Area",
464
- area,
465
- left_x + left_w / 2,
466
- row_y,
467
- "Helvetica",
468
- "Helvetica-Bold",
469
- label_size,
470
- value_size,
471
- gray_500,
472
- gray_900,
473
  )
474
-
475
- pdf.setFillColor(gray_500)
476
- pdf.setFont("Helvetica", label_size)
477
- pdf.drawString(right_x, row_y, "Location")
478
- pdf.setFillColor(gray_900)
479
- loc_lines = _wrap_lines(
480
- pdf,
481
- location or "-",
482
- right_w - 2 * mm,
483
- 2,
484
- "Helvetica-Bold",
485
- value_size,
 
 
 
 
 
486
  )
487
- _draw_wrapped(
488
- pdf,
489
- location or "-",
490
- right_x,
491
- row_y - label_size - value_gap,
492
- right_w - 2 * mm,
493
- leading,
494
- 2,
495
- "Helvetica-Bold",
496
- value_size,
 
 
 
 
 
 
 
497
  )
498
 
499
- y = row_y - (label_size + value_gap + leading * max(1, len(loc_lines))) - row_gap
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 -= 2 * mm
524
  pdf.setFillColor(gray_500)
525
- pdf.setFont("Helvetica", 11)
526
- cat_label_x = margin + 20 * mm + badge_w / 2
527
- pr_label_x = margin + 100 * mm + badge_w / 2
 
 
 
 
 
528
  label_y = y
529
  pdf.drawCentredString(cat_label_x, label_y, "Category")
530
  pdf.drawCentredString(pr_label_x, label_y, "Priority")
531
- y -= 12 * mm
532
  pdf.setFillColor(cat_bg)
533
  pdf.setStrokeColor(gray_200)
534
- pdf.roundRect(margin + 20 * mm, y - 2, badge_w, badge_h, 2 * mm, stroke=1, fill=1)
535
  pdf.setFillColor(cat_text_color)
536
- pdf.setFont("Helvetica-Bold", 11)
537
- pdf.drawCentredString(cat_label_x, y - 2 + badge_h / 2 - 4, cat_text)
538
  pdf.setFillColor(pr_bg)
539
- pdf.roundRect(margin + 100 * mm, y - 2, badge_w, badge_h, 2 * mm, stroke=1, fill=1)
540
  pdf.setFillColor(pr_text_color)
541
- pdf.drawCentredString(pr_label_x, y - 2 + badge_h / 2 - 4, pr_text)
542
- y -= 10 * mm
543
 
544
  condition = item_desc
545
  action = required_action
546
 
547
  pdf.setFillColor(gray_500)
548
- pdf.setFont("Helvetica", 11)
549
  pdf.drawCentredString(
550
  margin + (width - 2 * margin) / 2, y, "Condition Description"
551
  )
552
- y -= 4 * mm
553
- pdf.setFillColor(amber_50)
554
- pdf.setStrokeColor(amber_300)
555
  cond_lines = _wrap_lines(
556
  pdf,
557
  condition or "-",
558
  width - 2 * margin - 4 * mm,
559
  4,
560
- "Helvetica-Bold",
561
- 11,
562
  )
563
- cond_h = max(18 * mm, (len(cond_lines) or 1) * leading + 6 * mm)
564
  cond_bottom = y - cond_h
565
  pdf.rect(margin, cond_bottom, width - 2 * margin, cond_h, stroke=1, fill=1)
566
- pdf.setLineWidth(3)
567
  pdf.line(margin, cond_bottom, margin, y)
568
  pdf.setLineWidth(1)
569
- pdf.setFillColor(amber_800)
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
- 11,
581
  )
582
- y = cond_bottom - 6 * mm
583
 
584
  pdf.setFillColor(gray_500)
585
- pdf.setFont("Helvetica", 11)
586
  pdf.drawCentredString(
587
  margin + (width - 2 * margin) / 2, y, "Required Action"
588
  )
589
- y -= 4 * mm
590
- pdf.setFillColor(blue_50)
591
- pdf.setStrokeColor(blue_300)
592
  action_lines = _wrap_lines(
593
  pdf,
594
  action or "-",
595
  width - 2 * margin - 4 * mm,
596
  4,
597
- "Helvetica-Bold",
598
- 11,
599
  )
600
- action_h = max(18 * mm, (len(action_lines) or 1) * leading + 6 * mm)
601
  action_bottom = y - action_h
602
  pdf.rect(margin, action_bottom, width - 2 * margin, action_h, stroke=1, fill=1)
603
- pdf.setLineWidth(3)
604
  pdf.line(margin, action_bottom, margin, y)
605
  pdf.setLineWidth(1)
606
- pdf.setFillColor(blue_800)
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
- 11,
618
  )
619
- y = action_bottom - 6 * mm
620
  else:
621
  pdf.setFillColor(gray_800)
622
- pdf.setFont("Helvetica-Bold", 11)
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", 14)
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, photo_path, x + 2 * mm, y + 8 * mm, cell_w - 4 * mm, cell_h - 14 * mm
 
 
 
 
 
658
  )
659
- pdf.setFillColor(gray_500)
660
- pdf.setFont("Helvetica", 11)
661
- if label:
662
- pdf.drawCentredString(
663
- x + cell_w / 2, y + 3 * mm, f"Fig {idx + 1}: {label}"
 
 
 
 
 
 
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 - Automatically generated job sheet",
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: