ChristopherJKoen commited on
Commit
4297738
·
1 Parent(s): f48f5c4

Root HTML files added

Browse files
README.md CHANGED
@@ -1,26 +1,36 @@
1
- # Starter: FastAPI + Vite (Minimal)
2
 
3
- A minimal, industry-standard starter with:
4
- - `server/` - FastAPI API (`/api/health`).
5
- - `frontend/` - Vite + React app that renders a full-screen black page.
6
 
7
  ## Project Layout
8
 
9
  ```
10
- server/
11
- app/
12
- api/
13
- routes/
14
- health.py
15
- router.py
16
- core/
17
- config.py
18
- main.py
19
- frontend/
20
- src/
21
- App.tsx
22
- main.tsx
23
- index.css
 
 
 
 
 
 
 
 
 
 
 
 
24
  ```
25
 
26
  ## Quick Start
@@ -34,13 +44,11 @@ uvicorn server.app.main:app --reload --port 8000
34
  ```
35
 
36
  ### Web
 
37
  ```powershell
38
- cd frontend
39
- npm install
40
- npm run dev
41
  ```
42
-
43
- Open `http://localhost:5173`.
44
 
45
  ## Configuration
46
 
 
1
+ # RepEx Web Starter
2
 
3
+ Static HTML pages at the project root plus a minimal FastAPI backend.
 
 
4
 
5
  ## Project Layout
6
 
7
  ```
8
+ /
9
+ index.html
10
+ processing.html
11
+ review-setup.html
12
+ report-viewer.html
13
+ edit-layouts.html
14
+ export.html
15
+ style.css
16
+ script.js
17
+ components/
18
+ report-editor.js
19
+ templates/
20
+ job-sheet-template.html
21
+ assets/
22
+ prosento-logo.png
23
+ client-logo.png
24
+ server/
25
+ app/
26
+ api/
27
+ routes/
28
+ health.py
29
+ router.py
30
+ core/
31
+ config.py
32
+ main.py
33
+ frontend/ (optional Vite starter)
34
  ```
35
 
36
  ## Quick Start
 
44
  ```
45
 
46
  ### Web
47
+ Open `index.html` directly in your browser, or serve the root with a static server:
48
  ```powershell
49
+ python -m http.server 5173
 
 
50
  ```
51
+ Then open `http://localhost:5173`.
 
52
 
53
  ## Configuration
54
 
assets/client-logo.png ADDED
assets/prosento-logo.png ADDED
components/report-editor.js ADDED
@@ -0,0 +1,1113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class ReportEditor extends HTMLElement {
2
+ constructor() {
3
+ super();
4
+ this._mounted = false;
5
+
6
+ this.BASE_W = 595; // A4 points-ish (screen independent model)
7
+ this.BASE_H = 842;
8
+
9
+ this.state = {
10
+ isOpen: false,
11
+ zoom: 1,
12
+ activePage: 0,
13
+ pages: [], // [{ items: [...] }]
14
+ selectedId: null,
15
+ tool: "select", // select | text | rect
16
+ dragging: null, // { id, startX, startY, origX, origY }
17
+ resizing: null, // { id, handle, startX, startY, orig }
18
+ undo: [], // stack of serialized states (current page)
19
+ redo: [],
20
+ payload: null,
21
+ };
22
+ }
23
+
24
+ connectedCallback() {
25
+ if (this._mounted) return;
26
+ this._mounted = true;
27
+ this.render();
28
+ this.bind();
29
+ this.hide();
30
+ }
31
+
32
+ // Public API
33
+ open({ payload, pageIndex = 0, totalPages = 6 } = {}) {
34
+ this.state.payload = payload ?? null;
35
+ this.state.isOpen = true;
36
+
37
+ // Load existing editor pages from storage, else initialize
38
+ const stored = this._loadPages();
39
+ if (stored && Array.isArray(stored.pages) && stored.pages.length) {
40
+ this.state.pages = stored.pages;
41
+ } else {
42
+ this.state.pages = Array.from({ length: totalPages }, () => ({ items: [] }));
43
+ this._savePages();
44
+ }
45
+
46
+ this.state.activePage = Math.min(Math.max(0, pageIndex), this.state.pages.length - 1);
47
+ this.state.selectedId = null;
48
+ this.state.tool = "select";
49
+ this.state.undo = [];
50
+ this.state.redo = [];
51
+
52
+ this.show();
53
+ this.updateAll();
54
+ }
55
+
56
+ close() {
57
+ this.state.isOpen = false;
58
+ this.hide();
59
+ this.dispatchEvent(new CustomEvent("editor-closed", { bubbles: true }));
60
+ }
61
+
62
+ // ---------- Rendering ----------
63
+ render() {
64
+ this.innerHTML = `
65
+ <div class="fixed inset-0 z-50 hidden" data-overlay>
66
+ <div class="absolute inset-0 bg-black/30"></div>
67
+
68
+ <div class="relative h-full w-full flex items-center justify-center p-4">
69
+ <div class="w-full max-w-6xl bg-white rounded-xl shadow-sm ring-1 ring-gray-200 overflow-hidden">
70
+ <!-- Header -->
71
+ <div class="flex items-center justify-between px-4 py-3 border-b border-gray-200">
72
+ <div class="flex items-center gap-2">
73
+ <div class="inline-flex h-9 w-9 items-center justify-center rounded-lg bg-gray-50 border border-gray-200">
74
+ <span class="text-xs font-bold text-gray-700">A4</span>
75
+ </div>
76
+ <div>
77
+ <div class="text-sm font-semibold text-gray-900">Edit Report</div>
78
+ <div class="text-xs text-gray-500">Drag, resize, format and arrange elements</div>
79
+ </div>
80
+ </div>
81
+
82
+ <div class="flex items-center gap-2">
83
+ <button data-btn="undo"
84
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm font-semibold text-gray-800 hover:bg-gray-50 transition disabled:opacity-50 disabled:cursor-not-allowed">
85
+ <i data-feather="rotate-ccw" class="h-4 w-4"></i> Undo
86
+ </button>
87
+ <button data-btn="redo"
88
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm font-semibold text-gray-800 hover:bg-gray-50 transition disabled:opacity-50 disabled:cursor-not-allowed">
89
+ <i data-feather="rotate-cw" class="h-4 w-4"></i> Redo
90
+ </button>
91
+
92
+ <button data-btn="save"
93
+ class="inline-flex items-center gap-2 rounded-lg bg-emerald-600 px-3 py-2 text-sm font-semibold text-white hover:bg-emerald-700 transition">
94
+ <i data-feather="save" class="h-4 w-4"></i> Save
95
+ </button>
96
+
97
+ <button data-btn="close"
98
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm font-semibold text-gray-800 hover:bg-gray-50 transition">
99
+ <i data-feather="x" class="h-4 w-4"></i> Done
100
+ </button>
101
+ </div>
102
+ </div>
103
+
104
+ <!-- Body -->
105
+ <div class="grid grid-cols-1 lg:grid-cols-[240px,1fr,280px] gap-0 min-h-[70vh]">
106
+ <!-- Pages sidebar -->
107
+ <aside class="border-r border-gray-200 bg-white p-3">
108
+ <div class="flex items-center justify-between mb-3">
109
+ <div class="text-sm font-semibold text-gray-900">Pages</div>
110
+ <button data-btn="add-page"
111
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-2.5 py-1.5 text-xs font-semibold text-gray-800 hover:bg-gray-50 transition">
112
+ <i data-feather="plus" class="h-4 w-4"></i> Add
113
+ </button>
114
+ </div>
115
+
116
+ <div class="space-y-2 max-h-[60vh] overflow-auto pr-1" data-page-list></div>
117
+
118
+ <div class="mt-3 text-xs text-gray-500">
119
+ Tip: Click a page to edit. Your edits are saved to your browser storage.
120
+ </div>
121
+ </aside>
122
+
123
+ <!-- Canvas + toolbar -->
124
+ <section class="bg-gray-50 p-3">
125
+ <!-- Toolbar -->
126
+ <div class="flex flex-wrap items-center justify-between gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 mb-3">
127
+ <div class="flex flex-wrap items-center gap-2">
128
+ <button data-tool="select"
129
+ class="toolBtn inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm font-semibold text-gray-800 hover:bg-gray-50 transition">
130
+ <i data-feather="mouse-pointer" class="h-4 w-4"></i> Select
131
+ </button>
132
+
133
+ <button data-tool="text"
134
+ class="toolBtn inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm font-semibold text-gray-800 hover:bg-gray-50 transition">
135
+ <i data-feather="type" class="h-4 w-4"></i> Text
136
+ </button>
137
+
138
+ <button data-tool="rect"
139
+ class="toolBtn inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm font-semibold text-gray-800 hover:bg-gray-50 transition">
140
+ <i data-feather="square" class="h-4 w-4"></i> Shape
141
+ </button>
142
+
143
+ <button data-btn="add-image"
144
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm font-semibold text-gray-800 hover:bg-gray-50 transition">
145
+ <i data-feather="image" class="h-4 w-4"></i> Image
146
+ </button>
147
+
148
+ <input data-file="image" type="file" accept="image/*" class="hidden" />
149
+ </div>
150
+
151
+ <div class="flex items-center gap-2">
152
+ <div class="text-xs font-semibold text-gray-600">Zoom</div>
153
+ <button data-btn="zoom-out"
154
+ class="inline-flex items-center justify-center rounded-lg border border-gray-200 bg-white p-2 hover:bg-gray-50 transition">
155
+ <i data-feather="minus" class="h-4 w-4"></i>
156
+ </button>
157
+ <div class="text-xs font-semibold text-gray-700 w-14 text-center" data-zoom-label>100%</div>
158
+ <button data-btn="zoom-in"
159
+ class="inline-flex items-center justify-center rounded-lg border border-gray-200 bg-white p-2 hover:bg-gray-50 transition">
160
+ <i data-feather="plus" class="h-4 w-4"></i>
161
+ </button>
162
+ </div>
163
+ </div>
164
+
165
+ <!-- Canvas area -->
166
+ <div class="flex justify-center">
167
+ <div class="relative" data-canvas-wrap>
168
+ <div
169
+ data-canvas
170
+ class="relative bg-white border border-gray-200 rounded-xl shadow-sm overflow-hidden select-none"
171
+ style="width: min(100%, 700px); aspect-ratio: 210/297;"
172
+ aria-label="Editable A4 canvas"
173
+ >
174
+ <!-- items injected here -->
175
+ </div>
176
+ </div>
177
+ </div>
178
+
179
+ <div class="mt-3 text-xs text-gray-500">
180
+ Drag elements to move. Drag corner handles to resize. Double-click text to edit.
181
+ </div>
182
+ </section>
183
+
184
+ <!-- Properties panel -->
185
+ <aside class="border-l border-gray-200 bg-white p-3">
186
+ <div class="text-sm font-semibold text-gray-900 mb-2">Properties</div>
187
+
188
+ <div data-empty-props class="text-sm text-gray-600 rounded-lg border border-gray-200 bg-gray-50 p-3">
189
+ Select an element to edit formatting and layout options.
190
+ </div>
191
+
192
+ <div data-props class="hidden space-y-4">
193
+ <!-- Arrange -->
194
+ <div class="rounded-lg border border-gray-200 p-3">
195
+ <div class="text-xs font-semibold text-gray-600 mb-2">Arrange</div>
196
+ <div class="flex flex-wrap gap-2">
197
+ <button data-btn="bring-front"
198
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-semibold text-gray-800 hover:bg-gray-50 transition">
199
+ <i data-feather="chevrons-up" class="h-4 w-4"></i> Front
200
+ </button>
201
+ <button data-btn="send-back"
202
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-semibold text-gray-800 hover:bg-gray-50 transition">
203
+ <i data-feather="chevrons-down" class="h-4 w-4"></i> Back
204
+ </button>
205
+ <button data-btn="duplicate"
206
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-semibold text-gray-800 hover:bg-gray-50 transition">
207
+ <i data-feather="copy" class="h-4 w-4"></i> Duplicate
208
+ </button>
209
+ <button data-btn="delete"
210
+ class="inline-flex items-center gap-2 rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-xs font-semibold text-red-700 hover:bg-red-100 transition">
211
+ <i data-feather="trash-2" class="h-4 w-4"></i> Delete
212
+ </button>
213
+ </div>
214
+ </div>
215
+
216
+ <!-- Text controls -->
217
+ <div data-props-text class="rounded-lg border border-gray-200 p-3 hidden">
218
+ <div class="text-xs font-semibold text-gray-600 mb-2">Text</div>
219
+
220
+ <div class="grid grid-cols-2 gap-2">
221
+ <label class="text-xs text-gray-600">
222
+ Font size
223
+ <input data-prop="fontSize" type="number" min="8" max="72"
224
+ class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-200 focus:border-blue-500" />
225
+ </label>
226
+
227
+ <label class="text-xs text-gray-600">
228
+ Color
229
+ <input data-prop="color" type="color"
230
+ class="mt-1 w-full h-[40px] rounded-lg border border-gray-300 px-2 py-1" />
231
+ </label>
232
+ </div>
233
+
234
+ <div class="flex flex-wrap gap-2 mt-2">
235
+ <button data-btn="bold"
236
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-semibold text-gray-800 hover:bg-gray-50 transition">
237
+ <i data-feather="bold" class="h-4 w-4"></i>
238
+ </button>
239
+ <button data-btn="italic"
240
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-semibold text-gray-800 hover:bg-gray-50 transition">
241
+ <i data-feather="italic" class="h-4 w-4"></i>
242
+ </button>
243
+ <button data-btn="underline"
244
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-semibold text-gray-800 hover:bg-gray-50 transition">
245
+ <i data-feather="underline" class="h-4 w-4"></i>
246
+ </button>
247
+
248
+ <button data-btn="align-left"
249
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-semibold text-gray-800 hover:bg-gray-50 transition">
250
+ <i data-feather="align-left" class="h-4 w-4"></i>
251
+ </button>
252
+ <button data-btn="align-center"
253
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-semibold text-gray-800 hover:bg-gray-50 transition">
254
+ <i data-feather="align-center" class="h-4 w-4"></i>
255
+ </button>
256
+ <button data-btn="align-right"
257
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-semibold text-gray-800 hover:bg-gray-50 transition">
258
+ <i data-feather="align-right" class="h-4 w-4"></i>
259
+ </button>
260
+ </div>
261
+ </div>
262
+
263
+ <!-- Shape controls -->
264
+ <div data-props-rect class="rounded-lg border border-gray-200 p-3 hidden">
265
+ <div class="text-xs font-semibold text-gray-600 mb-2">Shape</div>
266
+
267
+ <div class="grid grid-cols-2 gap-2">
268
+ <label class="text-xs text-gray-600">
269
+ Fill
270
+ <input data-prop="fill" type="color"
271
+ class="mt-1 w-full h-[40px] rounded-lg border border-gray-300 px-2 py-1" />
272
+ </label>
273
+
274
+ <label class="text-xs text-gray-600">
275
+ Border
276
+ <input data-prop="stroke" type="color"
277
+ class="mt-1 w-full h-[40px] rounded-lg border border-gray-300 px-2 py-1" />
278
+ </label>
279
+ </div>
280
+
281
+ <label class="text-xs text-gray-600 block mt-2">
282
+ Border width
283
+ <input data-prop="strokeWidth" type="number" min="0" max="12"
284
+ class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-200 focus:border-blue-500" />
285
+ </label>
286
+ </div>
287
+
288
+ <!-- Image controls -->
289
+ <div data-props-image class="rounded-lg border border-gray-200 p-3 hidden">
290
+ <div class="text-xs font-semibold text-gray-600 mb-2">Image</div>
291
+ <button data-btn="replace-image"
292
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-semibold text-gray-800 hover:bg-gray-50 transition">
293
+ <i data-feather="refresh-cw" class="h-4 w-4"></i> Replace image
294
+ </button>
295
+ <input data-file="replace" type="file" accept="image/*" class="hidden" />
296
+ </div>
297
+ </div>
298
+
299
+ <div class="mt-4 rounded-lg border border-gray-200 bg-gray-50 p-3 text-xs text-gray-600">
300
+ <div class="font-semibold text-gray-800 mb-1">Keyboard shortcuts</div>
301
+ <ul class="list-disc pl-4 space-y-1">
302
+ <li><span class="font-semibold">Delete</span>: remove selected</li>
303
+ <li><span class="font-semibold">Ctrl/Cmd+Z</span>: undo</li>
304
+ <li><span class="font-semibold">Ctrl/Cmd+Y</span>: redo</li>
305
+ <li><span class="font-semibold">Esc</span>: close editor</li>
306
+ </ul>
307
+ </div>
308
+ </aside>
309
+ </div>
310
+ </div>
311
+ </div>
312
+ </div>
313
+ `;
314
+ }
315
+
316
+ bind() {
317
+ this.$overlay = this.querySelector("[data-overlay]");
318
+ this.$pageList = this.querySelector("[data-page-list]");
319
+ this.$canvas = this.querySelector("[data-canvas]");
320
+ this.$zoomLabel = this.querySelector("[data-zoom-label]");
321
+
322
+ this.$emptyProps = this.querySelector("[data-empty-props]");
323
+ this.$props = this.querySelector("[data-props]");
324
+ this.$propsText = this.querySelector("[data-props-text]");
325
+ this.$propsRect = this.querySelector("[data-props-rect]");
326
+ this.$propsImage = this.querySelector("[data-props-image]");
327
+
328
+ this.$imgFile = this.querySelector('[data-file="image"]');
329
+ this.$replaceFile = this.querySelector('[data-file="replace"]');
330
+
331
+ // header buttons
332
+ this.querySelector('[data-btn="close"]').addEventListener("click", () => this.close());
333
+ this.querySelector('[data-btn="save"]').addEventListener("click", () => this._savePages(true));
334
+ this.querySelector('[data-btn="undo"]').addEventListener("click", () => this.undo());
335
+ this.querySelector('[data-btn="redo"]').addEventListener("click", () => this.redo());
336
+
337
+ // tools
338
+ this.querySelectorAll(".toolBtn").forEach(btn => {
339
+ btn.addEventListener("click", () => {
340
+ this.state.tool = btn.dataset.tool;
341
+ this.updateToolbar();
342
+ });
343
+ });
344
+
345
+ // toolbar buttons
346
+ this.querySelector('[data-btn="add-image"]').addEventListener("click", () => this.$imgFile.click());
347
+ this.$imgFile.addEventListener("change", (e) => this._handleImageUpload(e, "add"));
348
+
349
+ this.querySelector('[data-btn="zoom-in"]').addEventListener("click", () => this.setZoom(this.state.zoom + 0.1));
350
+ this.querySelector('[data-btn="zoom-out"]').addEventListener("click", () => this.setZoom(this.state.zoom - 0.1));
351
+
352
+ // pages
353
+ this.querySelector('[data-btn="add-page"]').addEventListener("click", () => this.addPage());
354
+
355
+ // properties buttons
356
+ this.querySelector('[data-btn="delete"]').addEventListener("click", () => this.deleteSelected());
357
+ this.querySelector('[data-btn="duplicate"]').addEventListener("click", () => this.duplicateSelected());
358
+ this.querySelector('[data-btn="bring-front"]').addEventListener("click", () => this.bringFront());
359
+ this.querySelector('[data-btn="send-back"]').addEventListener("click", () => this.sendBack());
360
+
361
+ // text props
362
+ this.querySelector('[data-btn="bold"]').addEventListener("click", () => this.toggleTextStyle("bold"));
363
+ this.querySelector('[data-btn="italic"]').addEventListener("click", () => this.toggleTextStyle("italic"));
364
+ this.querySelector('[data-btn="underline"]').addEventListener("click", () => this.toggleTextStyle("underline"));
365
+ this.querySelector('[data-btn="align-left"]').addEventListener("click", () => this.setTextAlign("left"));
366
+ this.querySelector('[data-btn="align-center"]').addEventListener("click", () => this.setTextAlign("center"));
367
+ this.querySelector('[data-btn="align-right"]').addEventListener("click", () => this.setTextAlign("right"));
368
+
369
+ this.querySelector('[data-prop="fontSize"]').addEventListener("input", (e) => this.setProp("fontSize", Number(e.target.value || 12)));
370
+ this.querySelector('[data-prop="color"]').addEventListener("input", (e) => this.setProp("color", e.target.value));
371
+
372
+ // rect props
373
+ this.querySelector('[data-prop="fill"]').addEventListener("input", (e) => this.setProp("fill", e.target.value));
374
+ this.querySelector('[data-prop="stroke"]').addEventListener("input", (e) => this.setProp("stroke", e.target.value));
375
+ this.querySelector('[data-prop="strokeWidth"]').addEventListener("input", (e) => this.setProp("strokeWidth", Number(e.target.value || 0)));
376
+
377
+ // image replace
378
+ this.querySelector('[data-btn="replace-image"]').addEventListener("click", () => this.$replaceFile.click());
379
+ this.$replaceFile.addEventListener("change", (e) => this._handleImageUpload(e, "replace"));
380
+
381
+ // canvas interactions
382
+ this.$canvas.addEventListener("pointerdown", (e) => this.onCanvasPointerDown(e));
383
+ window.addEventListener("pointermove", (e) => this.onPointerMove(e));
384
+ window.addEventListener("pointerup", () => this.onPointerUp());
385
+
386
+ // keyboard shortcuts
387
+ window.addEventListener("keydown", (e) => {
388
+ if (!this.state.isOpen) return;
389
+
390
+ if (e.key === "Escape") {
391
+ e.preventDefault();
392
+ this.close();
393
+ }
394
+
395
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "z") {
396
+ e.preventDefault();
397
+ this.undo();
398
+ }
399
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "y") {
400
+ e.preventDefault();
401
+ this.redo();
402
+ }
403
+
404
+ if (e.key === "Delete" || e.key === "Backspace") {
405
+ // avoid deleting while typing in contenteditable
406
+ const active = document.activeElement;
407
+ const isEditingText = active && active.getAttribute && active.getAttribute("contenteditable") === "true";
408
+ if (!isEditingText) this.deleteSelected();
409
+ }
410
+ });
411
+ }
412
+
413
+ // ---------- Core helpers ----------
414
+ show() {
415
+ this.$overlay.classList.remove("hidden");
416
+ this.state.isOpen = true;
417
+ this.updateAll();
418
+ }
419
+
420
+ hide() {
421
+ this.$overlay.classList.add("hidden");
422
+ this.state.isOpen = false;
423
+ }
424
+
425
+ setZoom(z) {
426
+ const clamped = Math.max(0.6, Math.min(1.4, Number(z.toFixed(2))));
427
+ this.state.zoom = clamped;
428
+ this.updateCanvasScale();
429
+ }
430
+
431
+ get activePage() {
432
+ return this.state.pages[this.state.activePage];
433
+ }
434
+
435
+ updateAll() {
436
+ this.updateToolbar();
437
+ this.renderPageList();
438
+ this.renderCanvas();
439
+ this.updateCanvasScale();
440
+ this.updatePropsPanel();
441
+ this.updateUndoRedoButtons();
442
+ this._refreshIcons();
443
+ }
444
+
445
+ updateToolbar() {
446
+ this.querySelectorAll(".toolBtn").forEach(btn => {
447
+ const active = btn.dataset.tool === this.state.tool;
448
+ btn.classList.toggle("bg-gray-900", active);
449
+ btn.classList.toggle("text-white", active);
450
+ btn.classList.toggle("border-gray-900", active);
451
+
452
+ if (!active) {
453
+ btn.classList.add("bg-white", "text-gray-800", "border-gray-200");
454
+ btn.classList.remove("bg-gray-900", "text-white", "border-gray-900");
455
+ } else {
456
+ btn.classList.remove("bg-white", "text-gray-800", "border-gray-200");
457
+ }
458
+ });
459
+ }
460
+
461
+ updateCanvasScale() {
462
+ if (!this.$canvas) return;
463
+ this.$canvas.style.transformOrigin = "top center";
464
+ this.$canvas.style.transform = `scale(${this.state.zoom})`;
465
+ this.$zoomLabel.textContent = `${Math.round(this.state.zoom * 100)}%`;
466
+ }
467
+
468
+ _refreshIcons() {
469
+ if (window.feather && typeof window.feather.replace === "function") {
470
+ window.feather.replace();
471
+ }
472
+ }
473
+
474
+ // ---------- Storage ----------
475
+ _storageKey() {
476
+ return "repex_report_pages_v1";
477
+ }
478
+
479
+ _loadPages() {
480
+ try {
481
+ const raw = localStorage.getItem(this._storageKey());
482
+ return raw ? JSON.parse(raw) : null;
483
+ } catch {
484
+ return null;
485
+ }
486
+ }
487
+
488
+ _savePages(showToast = false) {
489
+ try {
490
+ localStorage.setItem(this._storageKey(), JSON.stringify({ pages: this.state.pages }));
491
+ if (showToast) this._toast("Saved");
492
+ } catch {
493
+ if (showToast) this._toast("Save failed");
494
+ }
495
+ }
496
+
497
+ _toast(text) {
498
+ const el = document.createElement("div");
499
+ el.className = "fixed z-[60] bottom-5 left-1/2 -translate-x-1/2 rounded-lg bg-gray-900 text-white text-sm font-semibold px-4 py-2 shadow";
500
+ el.textContent = text;
501
+ document.body.appendChild(el);
502
+ setTimeout(() => el.remove(), 1200);
503
+ }
504
+
505
+ // ---------- Page list ----------
506
+ renderPageList() {
507
+ this.$pageList.innerHTML = "";
508
+
509
+ this.state.pages.forEach((_, idx) => {
510
+ const active = idx === this.state.activePage;
511
+
512
+ const btn = document.createElement("button");
513
+ btn.type = "button";
514
+ btn.className =
515
+ "w-full text-left rounded-lg border px-3 py-2 transition " +
516
+ (active
517
+ ? "border-gray-900 bg-gray-900 text-white"
518
+ : "border-gray-200 bg-white text-gray-800 hover:bg-gray-50");
519
+ btn.innerHTML = `
520
+ <div class="flex items-center justify-between">
521
+ <div class="text-sm font-semibold">Page ${idx + 1}</div>
522
+ <div class="text-xs ${active ? "text-white/80" : "text-gray-500"}">${this.state.pages[idx].items.length} items</div>
523
+ </div>
524
+ `;
525
+ btn.addEventListener("click", () => {
526
+ this.state.activePage = idx;
527
+ this.state.selectedId = null;
528
+ this.state.undo = [];
529
+ this.state.redo = [];
530
+ this.updateAll();
531
+ });
532
+
533
+ this.$pageList.appendChild(btn);
534
+ });
535
+ }
536
+
537
+ addPage() {
538
+ this._pushUndoSnapshot();
539
+ this.state.pages.push({ items: [] });
540
+ this.state.activePage = this.state.pages.length - 1;
541
+ this.state.selectedId = null;
542
+ this._savePages();
543
+ this.updateAll();
544
+ }
545
+
546
+ // ---------- Canvas rendering ----------
547
+ renderCanvas() {
548
+ this.$canvas.innerHTML = "";
549
+
550
+ // Click-away surface
551
+ const surface = document.createElement("div");
552
+ surface.className = "absolute inset-0";
553
+ surface.addEventListener("pointerdown", (e) => {
554
+ // only clear selection if clicking empty space
555
+ if (e.target === surface) {
556
+ this.state.selectedId = null;
557
+ this.updatePropsPanel();
558
+ this.renderCanvas();
559
+ }
560
+ });
561
+ this.$canvas.appendChild(surface);
562
+
563
+ const items = this.activePage.items;
564
+ const selectedId = this.state.selectedId;
565
+
566
+ items
567
+ .slice()
568
+ .sort((a, b) => (a.z ?? 0) - (b.z ?? 0))
569
+ .forEach(item => {
570
+ const wrapper = document.createElement("div");
571
+ wrapper.dataset.itemId = item.id;
572
+ wrapper.className = "absolute";
573
+
574
+ // scaled px placement based on model units
575
+ const scale = this._canvasScale();
576
+ wrapper.style.left = `${item.x * scale}px`;
577
+ wrapper.style.top = `${item.y * scale}px`;
578
+ wrapper.style.width = `${item.w * scale}px`;
579
+ wrapper.style.height = `${item.h * scale}px`;
580
+ wrapper.style.zIndex = String(item.z ?? 0);
581
+
582
+ const isSelected = selectedId === item.id;
583
+ if (isSelected) wrapper.classList.add("ring-2", "ring-blue-300");
584
+
585
+ // content
586
+ if (item.type === "text") {
587
+ const content = document.createElement("div");
588
+ content.className = "w-full h-full p-2 overflow-hidden";
589
+ content.setAttribute("contenteditable", "true");
590
+ content.style.fontSize = `${(item.style?.fontSize ?? 14) * scale}px`;
591
+ content.style.fontWeight = item.style?.bold ? "700" : "400";
592
+ content.style.fontStyle = item.style?.italic ? "italic" : "normal";
593
+ content.style.textDecoration = item.style?.underline ? "underline" : "none";
594
+ content.style.color = item.style?.color ?? "#111827";
595
+ content.style.textAlign = item.style?.align ?? "left";
596
+ content.style.whiteSpace = "pre-wrap";
597
+ content.style.outline = "none";
598
+ content.innerText = item.content ?? "Double-click to edit";
599
+
600
+ // update model when typing (debounced)
601
+ let t = null;
602
+ content.addEventListener("input", () => {
603
+ clearTimeout(t);
604
+ t = setTimeout(() => {
605
+ const it = this._findItem(item.id);
606
+ if (!it) return;
607
+ it.content = content.innerText;
608
+ this._savePages();
609
+ }, 250);
610
+ });
611
+
612
+ // selecting the item (click wrapper)
613
+ content.addEventListener("pointerdown", (e) => {
614
+ e.stopPropagation();
615
+ this.selectItem(item.id);
616
+ });
617
+
618
+ wrapper.appendChild(content);
619
+ }
620
+
621
+ if (item.type === "image") {
622
+ const img = document.createElement("img");
623
+ img.className = "w-full h-full object-contain bg-white";
624
+ img.src = item.src;
625
+ img.alt = item.name ?? "Image";
626
+ img.draggable = false;
627
+ img.addEventListener("pointerdown", (e) => {
628
+ e.stopPropagation();
629
+ this.selectItem(item.id);
630
+ });
631
+ wrapper.appendChild(img);
632
+ }
633
+
634
+ if (item.type === "rect") {
635
+ const box = document.createElement("div");
636
+ box.className = "w-full h-full";
637
+ box.style.background = item.style?.fill ?? "#ffffff";
638
+ box.style.borderColor = item.style?.stroke ?? "#111827";
639
+ box.style.borderWidth = `${(item.style?.strokeWidth ?? 1) * scale}px`;
640
+ box.style.borderStyle = "solid";
641
+ box.addEventListener("pointerdown", (e) => {
642
+ e.stopPropagation();
643
+ this.selectItem(item.id);
644
+ });
645
+ wrapper.appendChild(box);
646
+ }
647
+
648
+ // wrapper drag handler
649
+ wrapper.addEventListener("pointerdown", (e) => this.onItemPointerDown(e, item.id));
650
+
651
+ // resize handles (selected only)
652
+ if (isSelected) {
653
+ ["nw", "ne", "sw", "se"].forEach(handle => {
654
+ const h = document.createElement("div");
655
+ h.dataset.handle = handle;
656
+ h.className =
657
+ "absolute w-3 h-3 bg-white border border-blue-300 rounded-sm";
658
+ if (handle === "nw") { h.style.left = "-6px"; h.style.top = "-6px"; }
659
+ if (handle === "ne") { h.style.right = "-6px"; h.style.top = "-6px"; }
660
+ if (handle === "sw") { h.style.left = "-6px"; h.style.bottom = "-6px"; }
661
+ if (handle === "se") { h.style.right = "-6px"; h.style.bottom = "-6px"; }
662
+
663
+ h.style.cursor = `${handle}-resize`;
664
+ h.addEventListener("pointerdown", (e) => {
665
+ e.stopPropagation();
666
+ this.startResize(e, item.id, handle);
667
+ });
668
+ wrapper.appendChild(h);
669
+ });
670
+ }
671
+
672
+ this.$canvas.appendChild(wrapper);
673
+ });
674
+ }
675
+
676
+ _canvasScale() {
677
+ // actual displayed width divided by model width
678
+ const rect = this.$canvas.getBoundingClientRect();
679
+ const w = rect.width; // already pre-zoom; we apply zoom with CSS transform
680
+ return w / this.BASE_W;
681
+ }
682
+
683
+ // ---------- Item creation ----------
684
+ onCanvasPointerDown(e) {
685
+ // prevent adding when clicking existing item
686
+ const hit = e.target.closest("[data-item-id]");
687
+ if (hit) return;
688
+
689
+ const { x, y } = this._eventToModelPoint(e);
690
+
691
+ if (this.state.tool === "text") {
692
+ this._pushUndoSnapshot();
693
+ const id = this._id();
694
+ this.activePage.items.push({
695
+ id,
696
+ type: "text",
697
+ x: this._clamp(x, 0, this.BASE_W - 200),
698
+ y: this._clamp(y, 0, this.BASE_H - 80),
699
+ w: 220,
700
+ h: 80,
701
+ z: this._maxZ() + 1,
702
+ content: "New text",
703
+ style: { fontSize: 14, bold: false, italic: false, underline: false, color: "#111827", align: "left" }
704
+ });
705
+ this.selectItem(id);
706
+ this._savePages();
707
+ this.renderCanvas();
708
+ this.updatePropsPanel();
709
+ return;
710
+ }
711
+
712
+ if (this.state.tool === "rect") {
713
+ this._pushUndoSnapshot();
714
+ const id = this._id();
715
+ this.activePage.items.push({
716
+ id,
717
+ type: "rect",
718
+ x: this._clamp(x, 0, this.BASE_W - 200),
719
+ y: this._clamp(y, 0, this.BASE_H - 120),
720
+ w: 220,
721
+ h: 120,
722
+ z: this._maxZ() + 1,
723
+ style: { fill: "#ffffff", stroke: "#111827", strokeWidth: 1 }
724
+ });
725
+ this.selectItem(id);
726
+ this._savePages();
727
+ this.renderCanvas();
728
+ this.updatePropsPanel();
729
+ return;
730
+ }
731
+
732
+ // select tool clicking empty space clears selection
733
+ if (this.state.tool === "select") {
734
+ this.state.selectedId = null;
735
+ this.updatePropsPanel();
736
+ this.renderCanvas();
737
+ }
738
+ }
739
+
740
+ _eventToModelPoint(e) {
741
+ const canvasRect = this.$canvas.getBoundingClientRect();
742
+ // account for zoom (transform), use client coords mapping
743
+ const zoom = this.state.zoom;
744
+ const xPx = (e.clientX - canvasRect.left) / zoom;
745
+ const yPx = (e.clientY - canvasRect.top) / zoom;
746
+
747
+ const scale = canvasRect.width / this.BASE_W; // pre-zoom scale
748
+ return { x: xPx / scale, y: yPx / scale };
749
+ }
750
+
751
+ // ---------- Selection / Drag / Resize ----------
752
+ selectItem(id) {
753
+ this.state.selectedId = id;
754
+ this.updatePropsPanel();
755
+ this.renderCanvas();
756
+ }
757
+
758
+ onItemPointerDown(e, id) {
759
+ // ignore if resizing handle
760
+ if (e.target && e.target.dataset && e.target.dataset.handle) return;
761
+
762
+ // select
763
+ this.selectItem(id);
764
+
765
+ // start drag only when using select tool
766
+ if (this.state.tool !== "select") return;
767
+
768
+ this._pushUndoSnapshot();
769
+
770
+ const it = this._findItem(id);
771
+ if (!it) return;
772
+
773
+ const { x, y } = this._eventToModelPoint(e);
774
+ this.state.dragging = {
775
+ id,
776
+ startX: x,
777
+ startY: y,
778
+ origX: it.x,
779
+ origY: it.y
780
+ };
781
+
782
+ e.preventDefault();
783
+ }
784
+
785
+ startResize(e, id, handle) {
786
+ this._pushUndoSnapshot();
787
+
788
+ const it = this._findItem(id);
789
+ if (!it) return;
790
+
791
+ const { x, y } = this._eventToModelPoint(e);
792
+ this.state.resizing = {
793
+ id,
794
+ handle,
795
+ startX: x,
796
+ startY: y,
797
+ orig: { x: it.x, y: it.y, w: it.w, h: it.h }
798
+ };
799
+ e.preventDefault();
800
+ }
801
+
802
+ onPointerMove(e) {
803
+ if (!this.state.isOpen) return;
804
+
805
+ if (this.state.dragging) {
806
+ const d = this.state.dragging;
807
+ const it = this._findItem(d.id);
808
+ if (!it) return;
809
+
810
+ const { x, y } = this._eventToModelPoint(e);
811
+ const dx = x - d.startX;
812
+ const dy = y - d.startY;
813
+
814
+ it.x = this._clamp(d.origX + dx, 0, this.BASE_W - it.w);
815
+ it.y = this._clamp(d.origY + dy, 0, this.BASE_H - it.h);
816
+
817
+ this._savePages();
818
+ this.renderCanvas();
819
+ return;
820
+ }
821
+
822
+ if (this.state.resizing) {
823
+ const r = this.state.resizing;
824
+ const it = this._findItem(r.id);
825
+ if (!it) return;
826
+
827
+ const { x, y } = this._eventToModelPoint(e);
828
+ const dx = x - r.startX;
829
+ const dy = y - r.startY;
830
+
831
+ const o = r.orig;
832
+ const minW = 40, minH = 30;
833
+
834
+ let nx = o.x, ny = o.y, nw = o.w, nh = o.h;
835
+
836
+ if (r.handle.includes("e")) nw = this._clamp(o.w + dx, minW, this.BASE_W - o.x);
837
+ if (r.handle.includes("s")) nh = this._clamp(o.h + dy, minH, this.BASE_H - o.y);
838
+ if (r.handle.includes("w")) {
839
+ nw = this._clamp(o.w - dx, minW, o.w + o.x);
840
+ nx = this._clamp(o.x + dx, 0, o.x + o.w - minW);
841
+ }
842
+ if (r.handle.includes("n")) {
843
+ nh = this._clamp(o.h - dy, minH, o.h + o.y);
844
+ ny = this._clamp(o.y + dy, 0, o.y + o.h - minH);
845
+ }
846
+
847
+ it.x = nx; it.y = ny; it.w = nw; it.h = nh;
848
+
849
+ this._savePages();
850
+ this.renderCanvas();
851
+ }
852
+ }
853
+
854
+ onPointerUp() {
855
+ if (!this.state.isOpen) return;
856
+
857
+ if (this.state.dragging) {
858
+ this.state.dragging = null;
859
+ this.updateUndoRedoButtons();
860
+ }
861
+ if (this.state.resizing) {
862
+ this.state.resizing = null;
863
+ this.updateUndoRedoButtons();
864
+ }
865
+ }
866
+
867
+ // ---------- Properties panel ----------
868
+ updatePropsPanel() {
869
+ const it = this._findItem(this.state.selectedId);
870
+ const has = !!it;
871
+
872
+ this.$emptyProps.classList.toggle("hidden", has);
873
+ this.$props.classList.toggle("hidden", !has);
874
+
875
+ // hide all groups first
876
+ this.$propsText.classList.add("hidden");
877
+ this.$propsRect.classList.add("hidden");
878
+ this.$propsImage.classList.add("hidden");
879
+
880
+ if (!it) return;
881
+
882
+ if (it.type === "text") {
883
+ this.$propsText.classList.remove("hidden");
884
+ this.querySelector('[data-prop="fontSize"]').value = it.style?.fontSize ?? 14;
885
+ this.querySelector('[data-prop="color"]').value = it.style?.color ?? "#111827";
886
+ }
887
+
888
+ if (it.type === "rect") {
889
+ this.$propsRect.classList.remove("hidden");
890
+ this.querySelector('[data-prop="fill"]').value = it.style?.fill ?? "#ffffff";
891
+ this.querySelector('[data-prop="stroke"]').value = it.style?.stroke ?? "#111827";
892
+ this.querySelector('[data-prop="strokeWidth"]').value = it.style?.strokeWidth ?? 1;
893
+ }
894
+
895
+ if (it.type === "image") {
896
+ this.$propsImage.classList.remove("hidden");
897
+ }
898
+
899
+ this._refreshIcons();
900
+ }
901
+
902
+ setProp(key, value) {
903
+ const it = this._findItem(this.state.selectedId);
904
+ if (!it) return;
905
+
906
+ this._pushUndoSnapshot();
907
+
908
+ it.style = it.style || {};
909
+ it.style[key] = value;
910
+
911
+ this._savePages();
912
+ this.renderCanvas();
913
+ this.updatePropsPanel();
914
+ this.updateUndoRedoButtons();
915
+ }
916
+
917
+ toggleTextStyle(which) {
918
+ const it = this._findItem(this.state.selectedId);
919
+ if (!it || it.type !== "text") return;
920
+
921
+ this._pushUndoSnapshot();
922
+
923
+ it.style = it.style || {};
924
+ if (which === "bold") it.style.bold = !it.style.bold;
925
+ if (which === "italic") it.style.italic = !it.style.italic;
926
+ if (which === "underline") it.style.underline = !it.style.underline;
927
+
928
+ this._savePages();
929
+ this.renderCanvas();
930
+ this.updateUndoRedoButtons();
931
+ }
932
+
933
+ setTextAlign(align) {
934
+ const it = this._findItem(this.state.selectedId);
935
+ if (!it || it.type !== "text") return;
936
+ this.setProp("align", align);
937
+ }
938
+
939
+ // ---------- Arrange ----------
940
+ bringFront() {
941
+ const it = this._findItem(this.state.selectedId);
942
+ if (!it) return;
943
+ this._pushUndoSnapshot();
944
+ it.z = this._maxZ() + 1;
945
+ this._savePages();
946
+ this.renderCanvas();
947
+ this.updateUndoRedoButtons();
948
+ }
949
+
950
+ sendBack() {
951
+ const it = this._findItem(this.state.selectedId);
952
+ if (!it) return;
953
+ this._pushUndoSnapshot();
954
+ it.z = this._minZ() - 1;
955
+ this._savePages();
956
+ this.renderCanvas();
957
+ this.updateUndoRedoButtons();
958
+ }
959
+
960
+ duplicateSelected() {
961
+ const it = this._findItem(this.state.selectedId);
962
+ if (!it) return;
963
+ this._pushUndoSnapshot();
964
+
965
+ const copy = JSON.parse(JSON.stringify(it));
966
+ copy.id = this._id();
967
+ copy.x = this._clamp(copy.x + 12, 0, this.BASE_W - copy.w);
968
+ copy.y = this._clamp(copy.y + 12, 0, this.BASE_H - copy.h);
969
+ copy.z = this._maxZ() + 1;
970
+
971
+ this.activePage.items.push(copy);
972
+ this.state.selectedId = copy.id;
973
+
974
+ this._savePages();
975
+ this.updateAll();
976
+ this.updateUndoRedoButtons();
977
+ }
978
+
979
+ deleteSelected() {
980
+ const id = this.state.selectedId;
981
+ if (!id) return;
982
+
983
+ this._pushUndoSnapshot();
984
+
985
+ this.activePage.items = this.activePage.items.filter(x => x.id !== id);
986
+ this.state.selectedId = null;
987
+
988
+ this._savePages();
989
+ this.updateAll();
990
+ this.updateUndoRedoButtons();
991
+ }
992
+
993
+ // ---------- Images ----------
994
+ _handleImageUpload(e, mode) {
995
+ const file = e.target.files && e.target.files[0];
996
+ if (!file) return;
997
+
998
+ const reader = new FileReader();
999
+ reader.onload = () => {
1000
+ if (mode === "add") {
1001
+ this._pushUndoSnapshot();
1002
+ const id = this._id();
1003
+ const w = 260, h = 180;
1004
+ this.activePage.items.push({
1005
+ id,
1006
+ type: "image",
1007
+ x: (this.BASE_W - w) / 2,
1008
+ y: (this.BASE_H - h) / 2,
1009
+ w, h,
1010
+ z: this._maxZ() + 1,
1011
+ src: reader.result,
1012
+ name: file.name
1013
+ });
1014
+ this.selectItem(id);
1015
+ this._savePages();
1016
+ this.updateAll();
1017
+ this.updateUndoRedoButtons();
1018
+ }
1019
+
1020
+ if (mode === "replace") {
1021
+ const it = this._findItem(this.state.selectedId);
1022
+ if (!it || it.type !== "image") return;
1023
+ this._pushUndoSnapshot();
1024
+ it.src = reader.result;
1025
+ it.name = file.name;
1026
+ this._savePages();
1027
+ this.updateAll();
1028
+ this.updateUndoRedoButtons();
1029
+ }
1030
+ };
1031
+ reader.readAsDataURL(file);
1032
+
1033
+ // reset input
1034
+ e.target.value = "";
1035
+ }
1036
+
1037
+ // ---------- Undo / redo ----------
1038
+ _pushUndoSnapshot() {
1039
+ // store snapshot of active page items
1040
+ const snap = JSON.stringify(this.activePage.items);
1041
+ const last = this.state.undo[this.state.undo.length - 1];
1042
+ if (last !== snap) this.state.undo.push(snap);
1043
+ // clear redo on new change
1044
+ this.state.redo = [];
1045
+ this.updateUndoRedoButtons();
1046
+ }
1047
+
1048
+ undo() {
1049
+ if (!this.state.undo.length) return;
1050
+
1051
+ const current = JSON.stringify(this.activePage.items);
1052
+ const prev = this.state.undo.pop();
1053
+ this.state.redo.push(current);
1054
+
1055
+ // restore prev
1056
+ try {
1057
+ this.activePage.items = JSON.parse(prev);
1058
+ } catch {}
1059
+ this.state.selectedId = null;
1060
+
1061
+ this._savePages();
1062
+ this.updateAll();
1063
+ }
1064
+
1065
+ redo() {
1066
+ if (!this.state.redo.length) return;
1067
+
1068
+ const current = JSON.stringify(this.activePage.items);
1069
+ const next = this.state.redo.pop();
1070
+ this.state.undo.push(current);
1071
+
1072
+ try {
1073
+ this.activePage.items = JSON.parse(next);
1074
+ } catch {}
1075
+ this.state.selectedId = null;
1076
+
1077
+ this._savePages();
1078
+ this.updateAll();
1079
+ }
1080
+
1081
+ updateUndoRedoButtons() {
1082
+ const undoBtn = this.querySelector('[data-btn="undo"]');
1083
+ const redoBtn = this.querySelector('[data-btn="redo"]');
1084
+ if (undoBtn) undoBtn.disabled = this.state.undo.length === 0;
1085
+ if (redoBtn) redoBtn.disabled = this.state.redo.length === 0;
1086
+ }
1087
+
1088
+ // ---------- Utils ----------
1089
+ _findItem(id) {
1090
+ if (!id) return null;
1091
+ return this.activePage.items.find(x => x.id === id) || null;
1092
+ }
1093
+
1094
+ _maxZ() {
1095
+ const items = this.activePage.items;
1096
+ return items.length ? Math.max(...items.map(i => i.z ?? 0)) : 0;
1097
+ }
1098
+
1099
+ _minZ() {
1100
+ const items = this.activePage.items;
1101
+ return items.length ? Math.min(...items.map(i => i.z ?? 0)) : 0;
1102
+ }
1103
+
1104
+ _id() {
1105
+ return "it_" + Math.random().toString(16).slice(2) + "_" + Date.now().toString(16);
1106
+ }
1107
+
1108
+ _clamp(n, a, b) {
1109
+ return Math.max(a, Math.min(b, n));
1110
+ }
1111
+ }
1112
+
1113
+ customElements.define("report-editor", ReportEditor);
edit-layouts.html ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>RepEx - Report Express | Export</title>
7
+
8
+ <link rel="stylesheet" href="style.css" />
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+ <script defer src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
11
+
12
+ <style>
13
+ img { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
14
+ @media print {
15
+ body { background: #fff !important; }
16
+ .no-print { display: none !important; }
17
+ main { box-shadow: none !important; border: none !important; margin: 0 !important; padding: 0 !important; }
18
+ }
19
+ </style>
20
+ </head>
21
+
22
+ <body class="bg-gray-50 min-h-screen">
23
+ <main class="max-w-4xl mx-auto my-8 bg-white shadow-sm ring-1 ring-gray-200 rounded-xl p-6 md:p-8">
24
+ <!-- Header -->
25
+ <header class="mb-8 border-b border-gray-200 pb-4">
26
+ <div class="grid grid-cols-[auto,1fr,auto] items-center gap-4">
27
+ <div class="flex items-center">
28
+ <img
29
+ src="assets/prosento-logo.png"
30
+ alt="Company logo"
31
+ class="h-12 w-auto object-contain"
32
+ loading="eager"
33
+ />
34
+ </div>
35
+
36
+ <div class="text-center">
37
+ <h1 class="text-2xl md:text-3xl font-bold text-gray-900 whitespace-nowrap">
38
+ RepEx - Report Express
39
+ </h1>
40
+ <p class="text-gray-600 whitespace-nowrap">Export</p>
41
+ </div>
42
+
43
+ <div class="flex justify-end gap-2 no-print">
44
+ <a
45
+ href="report-viewer.html"
46
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-semibold text-gray-800 hover:bg-gray-50 transition"
47
+ >
48
+ <i data-feather="arrow-left" class="h-4 w-4"></i>
49
+ Back
50
+ </a>
51
+ </div>
52
+ </div>
53
+ </header>
54
+
55
+ <!-- Workflow navigation -->
56
+ <nav class="mb-6 no-print" aria-label="Report workflow navigation">
57
+ <div class="flex flex-wrap gap-2">
58
+ <a
59
+ href="report-viewer.html"
60
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-semibold text-gray-800 hover:bg-gray-50 transition"
61
+ >
62
+ <i data-feather="layout" class="h-4 w-4"></i>
63
+ Report Viewer
64
+ </a>
65
+
66
+ <a
67
+ href="edit-layouts.html"
68
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-semibold text-gray-800 hover:bg-gray-50 transition"
69
+ >
70
+ <i data-feather="grid" class="h-4 w-4"></i>
71
+ Edit Page Layouts
72
+ </a>
73
+
74
+ <a
75
+ href="export.html"
76
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-900 px-4 py-2 text-sm font-semibold text-white"
77
+ >
78
+ <i data-feather="download" class="h-4 w-4"></i>
79
+ Export
80
+ </a>
81
+ </div>
82
+ </nav>
83
+
84
+ <section class="grid grid-cols-1 lg:grid-cols-[1fr,360px] gap-6">
85
+ <!-- Export options -->
86
+ <div class="rounded-lg border border-gray-200 bg-white p-4">
87
+ <h2 class="text-lg font-semibold text-gray-900 mb-2">Export Options</h2>
88
+ <p class="text-sm text-gray-600 mb-4">
89
+ PDF export comes next. For now, you can export a report “package” as JSON (pages + layout settings + payload).
90
+ </p>
91
+
92
+ <div class="space-y-4">
93
+ <div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
94
+ <div class="flex items-start justify-between gap-3">
95
+ <div>
96
+ <div class="text-sm font-semibold text-gray-900">Report package (.json)</div>
97
+ <div class="text-xs text-gray-500">Includes edited pages, layout settings and upload metadata</div>
98
+ </div>
99
+ <span class="text-xs font-semibold text-emerald-700 bg-emerald-50 border border-emerald-200 rounded-md px-2 py-1">
100
+ Available
101
+ </span>
102
+ </div>
103
+
104
+ <div class="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-3">
105
+ <label class="inline-flex items-center gap-2 text-sm text-gray-700">
106
+ <input id="incPages" type="checkbox" class="rounded border-gray-300" checked />
107
+ Include pages
108
+ </label>
109
+ <label class="inline-flex items-center gap-2 text-sm text-gray-700">
110
+ <input id="incLayout" type="checkbox" class="rounded border-gray-300" checked />
111
+ Include layout settings
112
+ </label>
113
+ <label class="inline-flex items-center gap-2 text-sm text-gray-700">
114
+ <input id="incPayload" type="checkbox" class="rounded border-gray-300" checked />
115
+ Include upload payload
116
+ </label>
117
+ <label class="inline-flex items-center gap-2 text-sm text-gray-700">
118
+ <input id="incTimestamp" type="checkbox" class="rounded border-gray-300" checked />
119
+ Include timestamp
120
+ </label>
121
+ </div>
122
+
123
+ <button id="downloadJson" type="button"
124
+ class="mt-4 inline-flex items-center justify-center gap-2 rounded-lg bg-emerald-600 px-4 py-2.5 text-white font-semibold hover:bg-emerald-700 transition">
125
+ <i data-feather="download" class="h-4 w-4"></i>
126
+ Download JSON package
127
+ </button>
128
+ </div>
129
+
130
+ <div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
131
+ <div class="flex items-start justify-between gap-3">
132
+ <div>
133
+ <div class="text-sm font-semibold text-gray-900">PDF export</div>
134
+ <div class="text-xs text-gray-500">Will export as print-ready A4 PDF</div>
135
+ </div>
136
+ <span class="text-xs font-semibold text-gray-600 bg-white border border-gray-200 rounded-md px-2 py-1">
137
+ Coming soon
138
+ </span>
139
+ </div>
140
+
141
+ <button type="button" disabled
142
+ class="mt-4 inline-flex items-center justify-center gap-2 rounded-lg bg-gray-200 px-4 py-2.5 text-gray-500 font-semibold cursor-not-allowed">
143
+ <i data-feather="file-text" class="h-4 w-4"></i>
144
+ Export PDF (disabled)
145
+ </button>
146
+ </div>
147
+ </div>
148
+ </div>
149
+
150
+ <!-- Summary -->
151
+ <aside class="rounded-lg border border-gray-200 bg-white p-4">
152
+ <h2 class="text-lg font-semibold text-gray-900 mb-2">Export Summary</h2>
153
+ <p class="text-sm text-gray-600 mb-4">This is what will be included based on your current session.</p>
154
+
155
+ <div class="space-y-3">
156
+ <div class="rounded-lg border border-gray-200 bg-gray-50 p-3">
157
+ <div class="text-xs font-semibold text-gray-600">Pages</div>
158
+ <div id="sumPages" class="text-sm font-semibold text-gray-900">—</div>
159
+ </div>
160
+
161
+ <div class="rounded-lg border border-gray-200 bg-gray-50 p-3">
162
+ <div class="text-xs font-semibold text-gray-600">Selected example photos</div>
163
+ <div id="sumPhotos" class="text-sm font-semibold text-gray-900">—</div>
164
+ </div>
165
+
166
+ <div class="rounded-lg border border-gray-200 bg-gray-50 p-3">
167
+ <div class="text-xs font-semibold text-gray-600">Documents</div>
168
+ <div id="sumDocs" class="text-sm font-semibold text-gray-900">—</div>
169
+ </div>
170
+
171
+ <div class="rounded-lg border border-gray-200 bg-gray-50 p-3">
172
+ <div class="text-xs font-semibold text-gray-600">Data files</div>
173
+ <div id="sumData" class="text-sm font-semibold text-gray-900">—</div>
174
+ </div>
175
+
176
+ <div class="text-xs text-gray-500">
177
+ Uses storage keys:
178
+ <div class="mt-1 font-mono text-[11px] text-gray-700">
179
+ repex_report_pages_v1<br>
180
+ repex_layout_settings_v1<br>
181
+ repex_report_payload
182
+ </div>
183
+ </div>
184
+ </div>
185
+ </aside>
186
+ </section>
187
+
188
+ <footer class="mt-12 text-center text-xs text-gray-500 no-print">
189
+ <p>Prosento - © 2026 All Rights Reserved</p>
190
+ <p class="mt-1">Export: JSON package now; PDF export will be added next.</p>
191
+ </footer>
192
+ </main>
193
+
194
+ <script src="script.js"></script>
195
+ <script>
196
+ document.addEventListener('DOMContentLoaded', () => {
197
+ if (window.feather && typeof window.feather.replace === 'function') feather.replace();
198
+
199
+ const PAGES_KEY = 'repex_report_pages_v1';
200
+ const LAYOUT_KEY = 'repex_layout_settings_v1';
201
+ const PAYLOAD_KEY = 'repex_report_payload';
202
+
203
+ const sumPages = document.getElementById('sumPages');
204
+ const sumPhotos = document.getElementById('sumPhotos');
205
+ const sumDocs = document.getElementById('sumDocs');
206
+ const sumData = document.getElementById('sumData');
207
+
208
+ const incPages = document.getElementById('incPages');
209
+ const incLayout = document.getElementById('incLayout');
210
+ const incPayload = document.getElementById('incPayload');
211
+ const incTimestamp = document.getElementById('incTimestamp');
212
+ const downloadJson = document.getElementById('downloadJson');
213
+
214
+ function loadJson(key) {
215
+ try {
216
+ const raw = localStorage.getItem(key);
217
+ return raw ? JSON.parse(raw) : null;
218
+ } catch {
219
+ return null;
220
+ }
221
+ }
222
+
223
+ const pagesObj = loadJson(PAGES_KEY);
224
+ const pages = pagesObj?.pages ?? [];
225
+ const layout = loadJson(LAYOUT_KEY);
226
+ const payload = loadJson(PAYLOAD_KEY);
227
+
228
+ const selectedPhotos = payload?.selectedPhotoIndices?.length ?? 0;
229
+ const docs = payload?.uploads?.documents?.length ?? 0;
230
+ const dataFiles = payload?.uploads?.dataFiles?.length ?? 0;
231
+
232
+ // Count total items
233
+ const totalItems = pages.reduce((acc, p) => acc + ((p?.items?.length) || 0), 0);
234
+
235
+ sumPages.textContent = pages.length ? `${pages.length} pages • ${totalItems} total items` : 'No saved pages yet';
236
+ sumPhotos.textContent = `${selectedPhotos}`;
237
+ sumDocs.textContent = `${docs}`;
238
+ sumData.textContent = `${dataFiles}`;
239
+
240
+ function downloadBlob(filename, text) {
241
+ const blob = new Blob([text], { type: 'application/json' });
242
+ const url = URL.createObjectURL(blob);
243
+ const a = document.createElement('a');
244
+ a.href = url;
245
+ a.download = filename;
246
+ document.body.appendChild(a);
247
+ a.click();
248
+ a.remove();
249
+ URL.revokeObjectURL(url);
250
+ }
251
+
252
+ downloadJson.addEventListener('click', () => {
253
+ const pack = {};
254
+ if (incPages.checked) pack.pages = pagesObj ?? null;
255
+ if (incLayout.checked) pack.layout = layout ?? null;
256
+ if (incPayload.checked) pack.payload = payload ?? null;
257
+ if (incTimestamp.checked) pack.exportedAt = new Date().toISOString();
258
+
259
+ const filename = `repex_report_package_${new Date().toISOString().replace(/[:.]/g,'-')}.json`;
260
+ downloadBlob(filename, JSON.stringify(pack, null, 2));
261
+ });
262
+ });
263
+ </script>
264
+ </body>
265
+ </html>
export.html ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>RepEx - Report Express | Export</title>
7
+
8
+ <link rel="stylesheet" href="style.css" />
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+ <script defer src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
11
+
12
+ <style>
13
+ img { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
14
+ @media print {
15
+ body { background: #fff !important; }
16
+ .no-print { display: none !important; }
17
+ main { box-shadow: none !important; border: none !important; margin: 0 !important; padding: 0 !important; }
18
+ }
19
+ </style>
20
+ </head>
21
+
22
+ <body class="bg-gray-50 min-h-screen">
23
+ <main class="max-w-4xl mx-auto my-8 bg-white shadow-sm ring-1 ring-gray-200 rounded-xl p-6 md:p-8">
24
+ <!-- Header -->
25
+ <header class="mb-8 border-b border-gray-200 pb-4">
26
+ <div class="grid grid-cols-[auto,1fr,auto] items-center gap-4">
27
+ <div class="flex items-center">
28
+ <img
29
+ src="assets/prosento-logo.png"
30
+ alt="Company logo"
31
+ class="h-12 w-auto object-contain"
32
+ loading="eager"
33
+ />
34
+ </div>
35
+
36
+ <div class="text-center">
37
+ <h1 class="text-2xl md:text-3xl font-bold text-gray-900 whitespace-nowrap">
38
+ RepEx - Report Express
39
+ </h1>
40
+ <p class="text-gray-600 whitespace-nowrap">Export</p>
41
+ </div>
42
+
43
+ <div class="flex justify-end gap-2 no-print">
44
+ <a
45
+ href="report-viewer.html"
46
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-semibold text-gray-800 hover:bg-gray-50 transition"
47
+ >
48
+ <i data-feather="arrow-left" class="h-4 w-4"></i>
49
+ Back
50
+ </a>
51
+ </div>
52
+ </div>
53
+ </header>
54
+
55
+ <!-- Workflow navigation -->
56
+ <nav class="mb-6 no-print" aria-label="Report workflow navigation">
57
+ <div class="flex flex-wrap gap-2">
58
+ <a
59
+ href="report-viewer.html"
60
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-semibold text-gray-800 hover:bg-gray-50 transition"
61
+ >
62
+ <i data-feather="layout" class="h-4 w-4"></i>
63
+ Report Viewer
64
+ </a>
65
+
66
+ <a
67
+ href="edit-layouts.html"
68
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-semibold text-gray-800 hover:bg-gray-50 transition"
69
+ >
70
+ <i data-feather="grid" class="h-4 w-4"></i>
71
+ Edit Page Layouts
72
+ </a>
73
+
74
+ <a
75
+ href="export.html"
76
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-900 px-4 py-2 text-sm font-semibold text-white"
77
+ >
78
+ <i data-feather="download" class="h-4 w-4"></i>
79
+ Export
80
+ </a>
81
+ </div>
82
+ </nav>
83
+
84
+ <section class="grid grid-cols-1 lg:grid-cols-[1fr,360px] gap-6">
85
+ <!-- Export options -->
86
+ <div class="rounded-lg border border-gray-200 bg-white p-4">
87
+ <h2 class="text-lg font-semibold text-gray-900 mb-2">Export Options</h2>
88
+ <p class="text-sm text-gray-600 mb-4">
89
+ PDF export comes next. For now, you can export a report “package” as JSON (pages + layout settings + payload).
90
+ </p>
91
+
92
+ <div class="space-y-4">
93
+ <div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
94
+ <div class="flex items-start justify-between gap-3">
95
+ <div>
96
+ <div class="text-sm font-semibold text-gray-900">Report package (.json)</div>
97
+ <div class="text-xs text-gray-500">Includes edited pages, layout settings and upload metadata</div>
98
+ </div>
99
+ <span class="text-xs font-semibold text-emerald-700 bg-emerald-50 border border-emerald-200 rounded-md px-2 py-1">
100
+ Available
101
+ </span>
102
+ </div>
103
+
104
+ <div class="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-3">
105
+ <label class="inline-flex items-center gap-2 text-sm text-gray-700">
106
+ <input id="incPages" type="checkbox" class="rounded border-gray-300" checked />
107
+ Include pages
108
+ </label>
109
+ <label class="inline-flex items-center gap-2 text-sm text-gray-700">
110
+ <input id="incLayout" type="checkbox" class="rounded border-gray-300" checked />
111
+ Include layout settings
112
+ </label>
113
+ <label class="inline-flex items-center gap-2 text-sm text-gray-700">
114
+ <input id="incPayload" type="checkbox" class="rounded border-gray-300" checked />
115
+ Include upload payload
116
+ </label>
117
+ <label class="inline-flex items-center gap-2 text-sm text-gray-700">
118
+ <input id="incTimestamp" type="checkbox" class="rounded border-gray-300" checked />
119
+ Include timestamp
120
+ </label>
121
+ </div>
122
+
123
+ <button id="downloadJson" type="button"
124
+ class="mt-4 inline-flex items-center justify-center gap-2 rounded-lg bg-emerald-600 px-4 py-2.5 text-white font-semibold hover:bg-emerald-700 transition">
125
+ <i data-feather="download" class="h-4 w-4"></i>
126
+ Download JSON package
127
+ </button>
128
+ </div>
129
+
130
+ <div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
131
+ <div class="flex items-start justify-between gap-3">
132
+ <div>
133
+ <div class="text-sm font-semibold text-gray-900">PDF export</div>
134
+ <div class="text-xs text-gray-500">Will export as print-ready A4 PDF</div>
135
+ </div>
136
+ <span class="text-xs font-semibold text-gray-600 bg-white border border-gray-200 rounded-md px-2 py-1">
137
+ Coming soon
138
+ </span>
139
+ </div>
140
+
141
+ <button type="button" disabled
142
+ class="mt-4 inline-flex items-center justify-center gap-2 rounded-lg bg-gray-200 px-4 py-2.5 text-gray-500 font-semibold cursor-not-allowed">
143
+ <i data-feather="file-text" class="h-4 w-4"></i>
144
+ Export PDF (disabled)
145
+ </button>
146
+ </div>
147
+ </div>
148
+ </div>
149
+
150
+ <!-- Summary -->
151
+ <aside class="rounded-lg border border-gray-200 bg-white p-4">
152
+ <h2 class="text-lg font-semibold text-gray-900 mb-2">Export Summary</h2>
153
+ <p class="text-sm text-gray-600 mb-4">This is what will be included based on your current session.</p>
154
+
155
+ <div class="space-y-3">
156
+ <div class="rounded-lg border border-gray-200 bg-gray-50 p-3">
157
+ <div class="text-xs font-semibold text-gray-600">Pages</div>
158
+ <div id="sumPages" class="text-sm font-semibold text-gray-900">—</div>
159
+ </div>
160
+
161
+ <div class="rounded-lg border border-gray-200 bg-gray-50 p-3">
162
+ <div class="text-xs font-semibold text-gray-600">Selected example photos</div>
163
+ <div id="sumPhotos" class="text-sm font-semibold text-gray-900">—</div>
164
+ </div>
165
+
166
+ <div class="rounded-lg border border-gray-200 bg-gray-50 p-3">
167
+ <div class="text-xs font-semibold text-gray-600">Documents</div>
168
+ <div id="sumDocs" class="text-sm font-semibold text-gray-900">—</div>
169
+ </div>
170
+
171
+ <div class="rounded-lg border border-gray-200 bg-gray-50 p-3">
172
+ <div class="text-xs font-semibold text-gray-600">Data files</div>
173
+ <div id="sumData" class="text-sm font-semibold text-gray-900">—</div>
174
+ </div>
175
+
176
+ <div class="text-xs text-gray-500">
177
+ Uses storage keys:
178
+ <div class="mt-1 font-mono text-[11px] text-gray-700">
179
+ repex_report_pages_v1<br>
180
+ repex_layout_settings_v1<br>
181
+ repex_report_payload
182
+ </div>
183
+ </div>
184
+ </div>
185
+ </aside>
186
+ </section>
187
+
188
+ <footer class="mt-12 text-center text-xs text-gray-500 no-print">
189
+ <p>Prosento - © 2026 All Rights Reserved</p>
190
+ <p class="mt-1">Export: JSON package now; PDF export will be added next.</p>
191
+ </footer>
192
+ </main>
193
+
194
+ <script src="script.js"></script>
195
+ <script>
196
+ document.addEventListener('DOMContentLoaded', () => {
197
+ if (window.feather && typeof window.feather.replace === 'function') feather.replace();
198
+
199
+ const PAGES_KEY = 'repex_report_pages_v1';
200
+ const LAYOUT_KEY = 'repex_layout_settings_v1';
201
+ const PAYLOAD_KEY = 'repex_report_payload';
202
+
203
+ const sumPages = document.getElementById('sumPages');
204
+ const sumPhotos = document.getElementById('sumPhotos');
205
+ const sumDocs = document.getElementById('sumDocs');
206
+ const sumData = document.getElementById('sumData');
207
+
208
+ const incPages = document.getElementById('incPages');
209
+ const incLayout = document.getElementById('incLayout');
210
+ const incPayload = document.getElementById('incPayload');
211
+ const incTimestamp = document.getElementById('incTimestamp');
212
+ const downloadJson = document.getElementById('downloadJson');
213
+
214
+ function loadJson(key) {
215
+ try {
216
+ const raw = localStorage.getItem(key);
217
+ return raw ? JSON.parse(raw) : null;
218
+ } catch {
219
+ return null;
220
+ }
221
+ }
222
+
223
+ const pagesObj = loadJson(PAGES_KEY);
224
+ const pages = pagesObj?.pages ?? [];
225
+ const layout = loadJson(LAYOUT_KEY);
226
+ const payload = loadJson(PAYLOAD_KEY);
227
+
228
+ const selectedPhotos = payload?.selectedPhotoIndices?.length ?? 0;
229
+ const docs = payload?.uploads?.documents?.length ?? 0;
230
+ const dataFiles = payload?.uploads?.dataFiles?.length ?? 0;
231
+
232
+ // Count total items
233
+ const totalItems = pages.reduce((acc, p) => acc + ((p?.items?.length) || 0), 0);
234
+
235
+ sumPages.textContent = pages.length ? `${pages.length} pages • ${totalItems} total items` : 'No saved pages yet';
236
+ sumPhotos.textContent = `${selectedPhotos}`;
237
+ sumDocs.textContent = `${docs}`;
238
+ sumData.textContent = `${dataFiles}`;
239
+
240
+ function downloadBlob(filename, text) {
241
+ const blob = new Blob([text], { type: 'application/json' });
242
+ const url = URL.createObjectURL(blob);
243
+ const a = document.createElement('a');
244
+ a.href = url;
245
+ a.download = filename;
246
+ document.body.appendChild(a);
247
+ a.click();
248
+ a.remove();
249
+ URL.revokeObjectURL(url);
250
+ }
251
+
252
+ downloadJson.addEventListener('click', () => {
253
+ const pack = {};
254
+ if (incPages.checked) pack.pages = pagesObj ?? null;
255
+ if (incLayout.checked) pack.layout = layout ?? null;
256
+ if (incPayload.checked) pack.payload = payload ?? null;
257
+ if (incTimestamp.checked) pack.exportedAt = new Date().toISOString();
258
+
259
+ const filename = `repex_report_package_${new Date().toISOString().replace(/[:.]/g,'-')}.json`;
260
+ downloadBlob(filename, JSON.stringify(pack, null, 2));
261
+ });
262
+ });
263
+ </script>
264
+ <script src="https://huggingface.co/deepsite/deepsite-badge.js"></script>
265
+ </body>
266
+ </html>
index.html ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>RepEx - Report Express</title>
7
+
8
+ <!-- Project styles (optional) -->
9
+ <link rel="stylesheet" href="style.css" />
10
+
11
+ <!-- Tailwind (CDN is fine for prototypes; for production, compile Tailwind) -->
12
+ <script src="https://cdn.tailwindcss.com"></script>
13
+
14
+ <!-- Feather Icons (include once) -->
15
+ <script defer src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
16
+
17
+ <style>
18
+ img { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
19
+ </style>
20
+ </head>
21
+
22
+ <body class="bg-gray-50 min-h-screen">
23
+ <!-- Page container (matches the previous site's “white card + subtle ring” style) -->
24
+ <main class="max-w-4xl mx-auto my-8 bg-white shadow-sm ring-1 ring-gray-200 rounded-xl p-6 md:p-8">
25
+ <!-- Top Header (logo + app name) -->
26
+ <header class="mb-10 border-b border-gray-200 pb-4">
27
+ <div class="grid grid-cols-[auto,1fr,auto] items-center gap-4">
28
+ <!-- Logo -->
29
+ <div class="flex items-center">
30
+ <img
31
+ src="assets/prosento-logo.png"
32
+ alt="Company logo"
33
+ class="h-12 w-auto object-contain"
34
+ loading="eager"
35
+ />
36
+ </div>
37
+
38
+ <!-- Title -->
39
+ <div class="text-center">
40
+ <h1 class="text-2xl md:text-3xl font-bold text-gray-900 whitespace-nowrap">
41
+ Prosento RepEx
42
+ </h1>
43
+ <p class="text-gray-600 whitespace-nowrap">
44
+ Upload photos and documents to generate professional job reports instantly
45
+ </p>
46
+ </div>
47
+
48
+ <!-- Right action -->
49
+ <div class="flex justify-end">
50
+ <a
51
+ href="#upload"
52
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-semibold text-gray-800 hover:bg-gray-50 transition"
53
+ >
54
+ <i data-feather="arrow-down-circle" class="h-4 w-4"></i>
55
+ Upload
56
+ </a>
57
+ </div>
58
+ </div>
59
+ </header>
60
+
61
+ <!-- Hero CTA -->
62
+ <section class="text-center mb-12">
63
+ <h2 class="text-3xl md:text-4xl font-bold text-gray-900 mb-3">
64
+ Generate inspection-ready reports in minutes
65
+ </h2>
66
+ <p class="text-base md:text-lg text-gray-600 mb-8">
67
+ Drag-and-drop files, add quick context, and produce a clean, consistent report format every time.
68
+ </p>
69
+
70
+ <div class="flex flex-col sm:flex-row justify-center gap-3">
71
+ <a
72
+ href="#upload"
73
+ class="inline-flex items-center justify-center gap-2 rounded-lg bg-blue-600 px-6 py-3 text-white font-semibold hover:bg-blue-700 transition"
74
+ >
75
+ <i data-feather="upload" class="h-5 w-5"></i>
76
+ Start Uploading
77
+ </a>
78
+
79
+ <a
80
+ href="#how-it-works"
81
+ class="inline-flex items-center justify-center gap-2 rounded-lg border border-gray-200 bg-white px-6 py-3 text-gray-800 font-semibold hover:bg-gray-50 transition"
82
+ >
83
+ <i data-feather="info" class="h-5 w-5"></i>
84
+ Learn More
85
+ </a>
86
+ </div>
87
+ </section>
88
+
89
+ <!-- How it works -->
90
+ <section id="how-it-works" class="mb-12">
91
+ <h2 class="text-xl md:text-2xl font-semibold text-gray-800 border-b border-gray-200 pb-2 mb-6">
92
+ How It Works
93
+ </h2>
94
+
95
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
96
+ <article class="rounded-lg border border-gray-200 bg-gray-50 p-5">
97
+ <div class="mb-4 inline-flex h-10 w-10 items-center justify-center rounded-full bg-blue-50 border border-blue-100">
98
+ <i data-feather="camera" class="h-5 w-5 text-blue-700"></i>
99
+ </div>
100
+ <h3 class="text-base font-semibold text-gray-900 mb-1">Capture site photos</h3>
101
+ <p class="text-sm text-gray-600">
102
+ Take clear photos of structural elements and issues during your inspection.
103
+ </p>
104
+ </article>
105
+
106
+ <article class="rounded-lg border border-gray-200 bg-gray-50 p-5">
107
+ <div class="mb-4 inline-flex h-10 w-10 items-center justify-center rounded-full bg-emerald-50 border border-emerald-100">
108
+ <i data-feather="upload-cloud" class="h-5 w-5 text-emerald-700"></i>
109
+ </div>
110
+ <h3 class="text-base font-semibold text-gray-900 mb-1">Upload documents</h3>
111
+ <p class="text-sm text-gray-600">
112
+ Add notes, measurements, and supporting PDFs/DOCX to complete the context.
113
+ </p>
114
+ </article>
115
+
116
+ <article class="rounded-lg border border-gray-200 bg-gray-50 p-5">
117
+ <div class="mb-4 inline-flex h-10 w-10 items-center justify-center rounded-full bg-blue-50 border border-blue-100">
118
+ <i data-feather="file-text" class="h-5 w-5 text-blue-700"></i>
119
+ </div>
120
+ <h3 class="text-base font-semibold text-gray-900 mb-1">Generate report</h3>
121
+ <p class="text-sm text-gray-600">
122
+ Produce a consistent, professional report that’s ready for submission.
123
+ </p>
124
+ </article>
125
+ </div>
126
+ </section>
127
+
128
+ <!-- Upload -->
129
+ <section id="upload" class="mb-12">
130
+ <h2 class="text-xl md:text-2xl font-semibold text-gray-800 border-b border-gray-200 pb-2 mb-6">
131
+ Upload Your Files
132
+ </h2>
133
+
134
+ <div class="rounded-lg border border-gray-200 bg-white p-6">
135
+ <!-- Drop zone -->
136
+ <div
137
+ class="rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 p-8 text-center hover:border-blue-400 transition mb-6"
138
+ role="button"
139
+ tabindex="0"
140
+ aria-label="Drag and drop files here"
141
+ >
142
+ <div class="mx-auto mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full bg-blue-50 border border-blue-100">
143
+ <i data-feather="upload" class="h-5 w-5 text-blue-700"></i>
144
+ </div>
145
+
146
+ <h3 class="text-base font-semibold text-gray-900">Drag &amp; drop files here</h3>
147
+ <p class="text-sm text-gray-600 mt-1 mb-4">or browse to upload</p>
148
+
149
+ <button
150
+ type="button"
151
+ class="inline-flex items-center justify-center rounded-lg bg-blue-600 px-5 py-2.5 text-white font-semibold hover:bg-blue-700 transition"
152
+ >
153
+ Browse Files
154
+ </button>
155
+
156
+ <p class="text-xs text-gray-500 mt-4">
157
+ Supports JPG, PNG, PDF, DOCX (Max 50MB each)
158
+ </p>
159
+ </div>
160
+
161
+ <!-- Metadata -->
162
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
163
+ <div class="space-y-1">
164
+ <label for="projectName" class="block text-sm font-medium text-gray-700">Project Name</label>
165
+ <input
166
+ id="projectName"
167
+ name="projectName"
168
+ type="text"
169
+ placeholder="e.g., North Pit Conveyor Support"
170
+ class="w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-200 focus:border-blue-500"
171
+ />
172
+ </div>
173
+
174
+ <div class="space-y-1">
175
+ <label for="inspectionDate" class="block text-sm font-medium text-gray-700">Inspection Date</label>
176
+ <input
177
+ id="inspectionDate"
178
+ name="inspectionDate"
179
+ type="date"
180
+ class="w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-200 focus:border-blue-500"
181
+ />
182
+ </div>
183
+ </div>
184
+
185
+ <div class="space-y-1 mb-6">
186
+ <label for="notes" class="block text-sm font-medium text-gray-700">Additional Notes</label>
187
+ <textarea
188
+ id="notes"
189
+ name="notes"
190
+ rows="4"
191
+ placeholder="Add any context you'd like included in the report..."
192
+ class="w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-200 focus:border-blue-500"
193
+ ></textarea>
194
+ </div>
195
+
196
+ <button
197
+ type="button"
198
+ class="w-full inline-flex items-center justify-center gap-2 rounded-lg bg-emerald-600 px-6 py-3 text-white font-semibold hover:bg-emerald-700 transition"
199
+ >
200
+ <i data-feather="file-plus" class="h-5 w-5"></i>
201
+ Generate Report
202
+ </button>
203
+ </div>
204
+ </section>
205
+
206
+ <!-- Recent Reports -->
207
+ <section class="mb-6">
208
+ <h2 class="text-xl md:text-2xl font-semibold text-gray-800 border-b border-gray-200 pb-2 mb-6">
209
+ Recent Reports
210
+ </h2>
211
+
212
+ <div class="rounded-lg border border-gray-200 bg-white overflow-hidden">
213
+ <div class="overflow-x-auto">
214
+ <table class="min-w-full divide-y divide-gray-200">
215
+ <thead class="bg-gray-50">
216
+ <tr>
217
+ <th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">
218
+ Report ID
219
+ </th>
220
+ <th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">
221
+ Project
222
+ </th>
223
+ <th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">
224
+ Date
225
+ </th>
226
+ <th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">
227
+ Status
228
+ </th>
229
+ <th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">
230
+ Actions
231
+ </th>
232
+ </tr>
233
+ </thead>
234
+
235
+ <tbody class="bg-white divide-y divide-gray-200">
236
+ <tr>
237
+ <td class="px-6 py-4 whitespace-nowrap text-sm font-semibold text-gray-900">#RPT-2023-001</td>
238
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">Bridge Inspection - Main Span</td>
239
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">2023-10-15</td>
240
+ <td class="px-6 py-4 whitespace-nowrap">
241
+ <span class="inline-flex items-center rounded-md border px-2.5 py-1 text-xs font-semibold bg-emerald-50 text-emerald-700 border-emerald-200">
242
+ Completed
243
+ </span>
244
+ </td>
245
+ <td class="px-6 py-4 whitespace-nowrap text-sm">
246
+ <a href="#" class="inline-flex items-center text-blue-700 hover:text-blue-800 mr-3" aria-label="Download report">
247
+ <i data-feather="download" class="h-4 w-4"></i>
248
+ </a>
249
+ <a href="#" class="inline-flex items-center text-blue-700 hover:text-blue-800" aria-label="Edit report">
250
+ <i data-feather="edit" class="h-4 w-4"></i>
251
+ </a>
252
+ </td>
253
+ </tr>
254
+
255
+ <tr>
256
+ <td class="px-6 py-4 whitespace-nowrap text-sm font-semibold text-gray-900">#RPT-2023-002</td>
257
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">Building Facade Assessment</td>
258
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">2023-10-18</td>
259
+ <td class="px-6 py-4 whitespace-nowrap">
260
+ <span class="inline-flex items-center rounded-md border px-2.5 py-1 text-xs font-semibold bg-amber-50 text-amber-700 border-amber-200">
261
+ Processing
262
+ </span>
263
+ </td>
264
+ <td class="px-6 py-4 whitespace-nowrap text-sm">
265
+ <a href="#" class="inline-flex items-center text-gray-400 cursor-not-allowed mr-3" aria-label="Download report (disabled)">
266
+ <i data-feather="download" class="h-4 w-4"></i>
267
+ </a>
268
+ <a href="#" class="inline-flex items-center text-blue-700 hover:text-blue-800" aria-label="Edit report">
269
+ <i data-feather="edit" class="h-4 w-4"></i>
270
+ </a>
271
+ </td>
272
+ </tr>
273
+ </tbody>
274
+ </table>
275
+ </div>
276
+ </div>
277
+ </section>
278
+
279
+ <!-- Footer -->
280
+ <footer class="mt-12 text-center text-xs text-gray-500">
281
+ <p>Prosento - © 2026 All Rights Reserved</p>
282
+ <p class="mt-1">RepEx is a report automation interface. All uploads should comply with site data policies.</p>
283
+ </footer>
284
+ </main>
285
+
286
+ <!-- Your app script (optional) -->
287
+ <script src="script.js"></script>
288
+
289
+ <script>
290
+ document.addEventListener('DOMContentLoaded', () => {
291
+ if (window.feather && typeof window.feather.replace === 'function') {
292
+ window.feather.replace();
293
+ }
294
+ });
295
+ </script>
296
+ </body>
297
+ </html>
package-lock.json DELETED
@@ -1,6 +0,0 @@
1
- {
2
- "name": "RepEx",
3
- "lockfileVersion": 3,
4
- "requires": true,
5
- "packages": {}
6
- }
 
 
 
 
 
 
 
processing.html ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>Processing - RepEx</title>
7
+
8
+ <link rel="stylesheet" href="style.css" />
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+ <script defer src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
11
+ </head>
12
+
13
+ <body class="bg-gray-50 min-h-screen">
14
+ <main class="max-w-2xl mx-auto my-10 bg-white shadow-sm ring-1 ring-gray-200 rounded-xl p-6 md:p-8">
15
+ <header class="flex items-center gap-4 border-b border-gray-200 pb-4 mb-6">
16
+ <img
17
+ src="assets/prosento-logo.png"
18
+ alt="Prosento logo"
19
+ class="h-10 w-auto object-contain"
20
+ loading="eager"
21
+ />
22
+ <div>
23
+ <h1 class="text-xl font-semibold text-gray-900">Prosento RepEx</h1>
24
+ <p class="text-sm text-gray-600">Report processing</p>
25
+ </div>
26
+ </header>
27
+
28
+ <section class="text-center">
29
+ <div class="mx-auto mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full bg-blue-50 border border-blue-100">
30
+ <i data-feather="loader" class="h-5 w-5 text-blue-700"></i>
31
+ </div>
32
+ <h2 class="text-2xl font-semibold text-gray-900 mb-2">Generating your report</h2>
33
+ <p class="text-gray-600 mb-6">
34
+ We are preparing your files and building the final report. This may take a few minutes.
35
+ </p>
36
+
37
+ <div class="w-full rounded-full bg-gray-200 h-3 overflow-hidden">
38
+ <div class="h-full bg-blue-600 w-1/2 animate-pulse"></div>
39
+ </div>
40
+
41
+ <p class="text-xs text-gray-500 mt-4">
42
+ You can leave this tab open while we process your submission.
43
+ </p>
44
+ </section>
45
+ </main>
46
+
47
+ <script src="script.js"></script>
48
+ <script>
49
+ document.addEventListener('DOMContentLoaded', () => {
50
+ if (window.feather && typeof window.feather.replace === 'function') {
51
+ window.feather.replace();
52
+ }
53
+ });
54
+ </script>
55
+ </body>
56
+ </html>
report-viewer.html ADDED
@@ -0,0 +1,420 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>RepEx - Report Express | Report Viewer</title>
7
+
8
+ <link rel="stylesheet" href="style.css" />
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+ <script src="components/report-editor.js"></script>
11
+ <script defer src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
12
+
13
+ <style>
14
+ @page { size: A4; margin: 0; }
15
+
16
+ /* A4 canvas container */
17
+ .a4-page {
18
+ aspect-ratio: 210 / 297;
19
+ width: min(100%, 560px);
20
+ border-radius: 12px;
21
+ }
22
+
23
+ /* Viewer uses white page unless empty */
24
+ .page-white {
25
+ background: #ffffff;
26
+ border: 1px solid #e5e7eb; /* gray-200 */
27
+ }
28
+
29
+ /* If a page has no items, show placeholder red */
30
+ .page-empty {
31
+ background: #fee2e2; /* red-200-ish */
32
+ border: 1px solid #fca5a5; /* red-300-ish */
33
+ }
34
+
35
+ @media print {
36
+ body { background: #fff !important; }
37
+ .print-a4 {
38
+ width: 210mm !important;
39
+ height: 297mm !important;
40
+ aspect-ratio: auto !important;
41
+ border-radius: 0 !important;
42
+ box-shadow: none !important;
43
+ }
44
+ .no-print { display: none !important; }
45
+ main { box-shadow: none !important; border: none !important; margin: 0 !important; padding: 0 !important; }
46
+ }
47
+
48
+ img { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
49
+ </style>
50
+ </head>
51
+
52
+ <body class="bg-gray-50 min-h-screen">
53
+ <main class="max-w-4xl mx-auto my-8 bg-white shadow-sm ring-1 ring-gray-200 rounded-xl p-6 md:p-8 print:ring-0 print:shadow-none print:rounded-none">
54
+ <!-- Header -->
55
+ <header class="mb-8 border-b border-gray-200 pb-4">
56
+ <div class="grid grid-cols-[auto,1fr,auto] items-center gap-4">
57
+ <div class="flex items-center">
58
+ <img
59
+ src="assets/prosento-logo.png"
60
+ alt="Company logo"
61
+ class="h-12 w-auto object-contain"
62
+ loading="eager"
63
+ />
64
+ </div>
65
+
66
+ <div class="text-center">
67
+ <h1 class="text-2xl md:text-3xl font-bold text-gray-900 whitespace-nowrap">
68
+ RepEx - Report Express
69
+ </h1>
70
+ <p class="text-gray-600 whitespace-nowrap">Report Viewer</p>
71
+ </div>
72
+
73
+ <div class="flex justify-end gap-2 no-print">
74
+ <a
75
+ href="review-setup.html"
76
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-semibold text-gray-800 hover:bg-gray-50 transition"
77
+ >
78
+ <i data-feather="arrow-left" class="h-4 w-4"></i>
79
+ Back
80
+ </a>
81
+ </div>
82
+ </div>
83
+ </header>
84
+
85
+ <!-- Workflow buttons -->
86
+ <nav class="mb-6 no-print" aria-label="Report workflow navigation">
87
+ <div class="flex flex-wrap gap-2">
88
+ <button
89
+ type="button"
90
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-900 px-4 py-2 text-sm font-semibold text-white"
91
+ >
92
+ <i data-feather="layout" class="h-4 w-4"></i>
93
+ Report Viewer
94
+ </button>
95
+
96
+ <button
97
+ type="button"
98
+ id="edit-report"
99
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-semibold text-gray-800 hover:bg-gray-50 transition"
100
+ >
101
+ <i data-feather="edit-3" class="h-4 w-4"></i>
102
+ Edit Report
103
+ </button>
104
+
105
+ <button
106
+ type="button"
107
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-semibold text-gray-800 hover:bg-gray-50 transition"
108
+ >
109
+ <i data-feather="grid" class="h-4 w-4"></i>
110
+ Edit Page Layouts
111
+ </button>
112
+
113
+ <button
114
+ type="button"
115
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-semibold text-gray-800 hover:bg-gray-50 transition"
116
+ >
117
+ <i data-feather="download" class="h-4 w-4"></i>
118
+ Export
119
+ </button>
120
+ </div>
121
+ </nav>
122
+
123
+ <!-- Viewer -->
124
+ <section id="viewerSection" aria-label="Report viewer">
125
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-4">
126
+ <div>
127
+ <h2 class="text-xl font-semibold text-gray-800">Report Pages</h2>
128
+ <p id="viewerMeta" class="text-sm text-gray-600">Loading…</p>
129
+ </div>
130
+
131
+ <div class="flex items-center gap-2 no-print">
132
+ <button
133
+ id="prevPage"
134
+ type="button"
135
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-semibold text-gray-800 hover:bg-gray-50 transition disabled:opacity-50 disabled:cursor-not-allowed"
136
+ >
137
+ <i data-feather="chevron-left" class="h-4 w-4"></i>
138
+ Prev
139
+ </button>
140
+
141
+ <div class="text-sm font-semibold text-gray-700">
142
+ Page <span id="pageNumber">1</span> / <span id="pageTotal">1</span>
143
+ </div>
144
+
145
+ <button
146
+ id="nextPage"
147
+ type="button"
148
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-semibold text-gray-800 hover:bg-gray-50 transition disabled:opacity-50 disabled:cursor-not-allowed"
149
+ >
150
+ Next
151
+ <i data-feather="chevron-right" class="h-4 w-4"></i>
152
+ </button>
153
+ </div>
154
+ </div>
155
+
156
+ <!-- Page stage -->
157
+ <div class="flex justify-center">
158
+ <div
159
+ id="pageStage"
160
+ class="a4-page print-a4 relative overflow-hidden shadow-sm page-empty"
161
+ aria-label="Rendered A4 page"
162
+ ></div>
163
+ </div>
164
+
165
+ <p class="mt-4 text-xs text-gray-500 no-print">
166
+ Tip: Use keyboard arrows (← / →) to change pages.
167
+ </p>
168
+ </section>
169
+
170
+ <!-- Footer -->
171
+ <footer class="mt-12 text-center text-xs text-gray-500 no-print">
172
+ <p>Prosento - © 2026 All Rights Reserved</p>
173
+ <p class="mt-1">Viewer now renders saved edits from the editor.</p>
174
+ </footer>
175
+ </main>
176
+
177
+ <script src="script.js"></script>
178
+ <script>
179
+ document.addEventListener('DOMContentLoaded', () => {
180
+ // Feather icons
181
+ if (window.feather && typeof window.feather.replace === 'function') {
182
+ window.feather.replace();
183
+ }
184
+
185
+ // Mount editor (hidden until opened)
186
+ const editor = document.createElement('report-editor');
187
+ document.body.appendChild(editor);
188
+ editor.style.display = 'none';
189
+
190
+ const viewerSection = document.getElementById('viewerSection');
191
+ const pageStage = document.getElementById('pageStage');
192
+ const viewerMeta = document.getElementById('viewerMeta');
193
+
194
+ const prevPageBtn = document.getElementById('prevPage');
195
+ const nextPageBtn = document.getElementById('nextPage');
196
+ const pageNumber = document.getElementById('pageNumber');
197
+ const pageTotal = document.getElementById('pageTotal');
198
+ const editBtn = document.getElementById('edit-report');
199
+
200
+ // A4 model dimensions (must match editor)
201
+ const BASE_W = 595;
202
+ const BASE_H = 842;
203
+
204
+ // ---- Load payload from Review page (optional meta) ----
205
+ const payloadRaw = localStorage.getItem('repex_report_payload');
206
+ let payload = null;
207
+ try { payload = payloadRaw ? JSON.parse(payloadRaw) : null; } catch { payload = null; }
208
+
209
+ // ---- Pages storage used by editor ----
210
+ const PAGES_STORAGE_KEY = 'repex_report_pages_v1';
211
+
212
+ function loadEditorPages() {
213
+ try {
214
+ const raw = localStorage.getItem(PAGES_STORAGE_KEY);
215
+ const parsed = raw ? JSON.parse(raw) : null;
216
+ if (parsed && Array.isArray(parsed.pages)) return parsed.pages;
217
+ return null;
218
+ } catch {
219
+ return null;
220
+ }
221
+ }
222
+
223
+ // ---- State ----
224
+ const state = {
225
+ pageIndex: 0,
226
+ totalPages: 6,
227
+ editMode: false,
228
+ pages: null
229
+ };
230
+
231
+ function setMeta() {
232
+ const selected = payload?.selectedPhotoIndices?.length ?? 0;
233
+ const docs = payload?.uploads?.documents?.length ?? 0;
234
+ const dataFiles = payload?.uploads?.dataFiles?.length ?? 0;
235
+
236
+ const hasEdits = !!state.pages;
237
+ viewerMeta.textContent =
238
+ `Selected example photos: ${selected} • Documents: ${docs} • Data files: ${dataFiles}` +
239
+ (hasEdits ? ` • Edited pages loaded` : ` • No saved edits yet`);
240
+ }
241
+
242
+ function renderControls() {
243
+ pageTotal.textContent = String(state.totalPages);
244
+ pageNumber.textContent = String(state.pageIndex + 1);
245
+ prevPageBtn.disabled = state.pageIndex === 0;
246
+ nextPageBtn.disabled = state.pageIndex === state.totalPages - 1;
247
+ }
248
+
249
+ function stageScale() {
250
+ // stage width is in CSS pixels; map model coords to it
251
+ const rect = pageStage.getBoundingClientRect();
252
+ return rect.width / BASE_W;
253
+ }
254
+
255
+ function clearStage() {
256
+ pageStage.innerHTML = '';
257
+ }
258
+
259
+ function renderStageFromPage(pageObj) {
260
+ clearStage();
261
+
262
+ const items = pageObj?.items ?? [];
263
+ const hasItems = items.length > 0;
264
+
265
+ // page background style
266
+ pageStage.classList.toggle('page-empty', !hasItems);
267
+ pageStage.classList.toggle('page-white', hasItems);
268
+
269
+ // If empty, leave blank red page
270
+ if (!hasItems) return;
271
+
272
+ const scale = stageScale();
273
+
274
+ // Render items sorted by z
275
+ items
276
+ .slice()
277
+ .sort((a, b) => (a.z ?? 0) - (b.z ?? 0))
278
+ .forEach(item => {
279
+ const wrap = document.createElement('div');
280
+ wrap.className = 'absolute';
281
+ wrap.style.left = `${item.x * scale}px`;
282
+ wrap.style.top = `${item.y * scale}px`;
283
+ wrap.style.width = `${item.w * scale}px`;
284
+ wrap.style.height = `${item.h * scale}px`;
285
+ wrap.style.zIndex = String(item.z ?? 0);
286
+
287
+ if (item.type === 'text') {
288
+ const d = document.createElement('div');
289
+ d.className = 'w-full h-full p-2 overflow-hidden';
290
+ d.style.whiteSpace = 'pre-wrap';
291
+ d.style.outline = 'none';
292
+
293
+ const fontSize = (item.style?.fontSize ?? 14) * scale;
294
+ d.style.fontSize = `${fontSize}px`;
295
+ d.style.fontWeight = item.style?.bold ? '700' : '400';
296
+ d.style.fontStyle = item.style?.italic ? 'italic' : 'normal';
297
+ d.style.textDecoration = item.style?.underline ? 'underline' : 'none';
298
+ d.style.color = item.style?.color ?? '#111827';
299
+ d.style.textAlign = item.style?.align ?? 'left';
300
+ d.textContent = item.content ?? '';
301
+
302
+ wrap.appendChild(d);
303
+ }
304
+
305
+ if (item.type === 'image') {
306
+ const img = document.createElement('img');
307
+ img.className = 'w-full h-full object-contain bg-white';
308
+ img.src = item.src;
309
+ img.alt = item.name ?? 'Image';
310
+ img.loading = 'eager';
311
+ wrap.appendChild(img);
312
+ }
313
+
314
+ if (item.type === 'rect') {
315
+ const box = document.createElement('div');
316
+ box.className = 'w-full h-full';
317
+ box.style.background = item.style?.fill ?? '#ffffff';
318
+ box.style.borderStyle = 'solid';
319
+ box.style.borderColor = item.style?.stroke ?? '#111827';
320
+ box.style.borderWidth = `${(item.style?.strokeWidth ?? 1) * scale}px`;
321
+ wrap.appendChild(box);
322
+ }
323
+
324
+ pageStage.appendChild(wrap);
325
+ });
326
+ }
327
+
328
+ function renderPage() {
329
+ // Reload pages each time to reflect saved edits immediately
330
+ state.pages = loadEditorPages();
331
+ state.totalPages = state.pages?.length ?? state.totalPages;
332
+
333
+ renderControls();
334
+
335
+ const pageObj = state.pages ? state.pages[state.pageIndex] : null;
336
+ renderStageFromPage(pageObj);
337
+
338
+ setMeta();
339
+ }
340
+
341
+ function nextPage() {
342
+ if (state.pageIndex < state.totalPages - 1) {
343
+ state.pageIndex += 1;
344
+ renderPage();
345
+ }
346
+ }
347
+
348
+ function prevPage() {
349
+ if (state.pageIndex > 0) {
350
+ state.pageIndex -= 1;
351
+ renderPage();
352
+ }
353
+ }
354
+
355
+ function openEditor() {
356
+ state.editMode = true;
357
+ editor.style.display = 'block';
358
+ viewerSection.classList.add('hidden');
359
+
360
+ // Ensure the editor has the latest page count
361
+ const pages = loadEditorPages();
362
+ const totalPages = pages?.length ?? state.totalPages;
363
+
364
+ editor.open({
365
+ payload,
366
+ pageIndex: state.pageIndex,
367
+ totalPages
368
+ });
369
+
370
+ editBtn.classList.add('bg-gray-900', 'text-white', 'border-gray-900');
371
+ editBtn.classList.remove('bg-white', 'text-gray-800', 'border-gray-200');
372
+
373
+ if (window.feather && typeof window.feather.replace === 'function') {
374
+ window.feather.replace();
375
+ }
376
+ }
377
+
378
+ function closeEditor() {
379
+ state.editMode = false;
380
+ editor.style.display = 'none';
381
+ viewerSection.classList.remove('hidden');
382
+
383
+ editBtn.classList.remove('bg-gray-900', 'text-white', 'border-gray-900');
384
+ editBtn.classList.add('bg-white', 'text-gray-800', 'border-gray-200');
385
+
386
+ // Re-render to reflect latest saved edits
387
+ renderPage();
388
+ }
389
+
390
+ // Events
391
+ prevPageBtn.addEventListener('click', prevPage);
392
+ nextPageBtn.addEventListener('click', nextPage);
393
+
394
+ document.getElementById('edit-report').addEventListener('click', () => {
395
+ if (!state.editMode) openEditor();
396
+ });
397
+
398
+ editor.addEventListener('editor-closed', closeEditor);
399
+
400
+ window.addEventListener('keydown', (e) => {
401
+ if (state.editMode) return;
402
+ if (e.key === 'ArrowRight') nextPage();
403
+ if (e.key === 'ArrowLeft') prevPage();
404
+ });
405
+
406
+ // Re-render on resize so scaling stays correct
407
+ window.addEventListener('resize', () => {
408
+ if (!state.editMode) renderPage();
409
+ });
410
+
411
+ window.addEventListener('beforeprint', () => {
412
+ document.querySelectorAll('img').forEach(img => (img.loading = 'eager'));
413
+ });
414
+
415
+ // Initial render
416
+ renderPage();
417
+ });
418
+ </script>
419
+ </body>
420
+ </html>
review-setup.html ADDED
@@ -0,0 +1,385 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>RepEx - Report Express | Review & Setup</title>
7
+
8
+ <link rel="stylesheet" href="style.css" />
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+ <script defer src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
11
+
12
+ <style>
13
+ img { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
14
+ </style>
15
+ </head>
16
+
17
+ <body class="bg-gray-50 min-h-screen">
18
+ <main class="max-w-4xl mx-auto my-8 bg-white shadow-sm ring-1 ring-gray-200 rounded-xl p-6 md:p-8">
19
+ <!-- Header -->
20
+ <header class="mb-8 border-b border-gray-200 pb-4">
21
+ <div class="grid grid-cols-[auto,1fr,auto] items-center gap-4">
22
+ <div class="flex items-center">
23
+ <img
24
+ src="assets/prosento-logo.png"
25
+ alt="Company logo"
26
+ class="h-12 w-auto object-contain"
27
+ loading="eager"
28
+ />
29
+ </div>
30
+
31
+ <div class="text-center">
32
+ <h1 class="text-2xl md:text-3xl font-bold text-gray-900 whitespace-nowrap">
33
+ RepEx - Report Express
34
+ </h1>
35
+ <p class="text-gray-600 whitespace-nowrap">
36
+ Review uploads → pick examples → continue to report viewer
37
+ </p>
38
+ </div>
39
+
40
+ <div class="flex justify-end">
41
+ <span class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-xs font-semibold text-gray-700">
42
+ <i data-feather="check-circle" class="h-4 w-4"></i>
43
+ Uploads processed
44
+ </span>
45
+ </div>
46
+ </div>
47
+ </header>
48
+
49
+ <!-- Orientation -->
50
+ <section class="mb-8" aria-labelledby="what-next">
51
+ <h2 id="what-next" class="text-xl font-semibold text-gray-800 border-b border-gray-200 pb-2 mb-4">
52
+ What happens on this page
53
+ </h2>
54
+
55
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
56
+ <div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
57
+ <div class="inline-flex h-10 w-10 items-center justify-center rounded-full bg-blue-50 border border-blue-100 mb-3">
58
+ <i data-feather="image" class="h-5 w-5 text-blue-700"></i>
59
+ </div>
60
+ <h3 class="font-semibold text-gray-900 mb-1">Select example photos</h3>
61
+ <p class="text-sm text-gray-600">
62
+ Choose which uploaded images should appear as example figures in the report.
63
+ </p>
64
+ </div>
65
+
66
+ <div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
67
+ <div class="inline-flex h-10 w-10 items-center justify-center rounded-full bg-emerald-50 border border-emerald-100 mb-3">
68
+ <i data-feather="file-text" class="h-5 w-5 text-emerald-700"></i>
69
+ </div>
70
+ <h3 class="font-semibold text-gray-900 mb-1">Confirm documents</h3>
71
+ <p class="text-sm text-gray-600">
72
+ Ensure supporting PDFs/DOCX are correct (and later attach to export if needed).
73
+ </p>
74
+ </div>
75
+
76
+ <div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
77
+ <div class="inline-flex h-10 w-10 items-center justify-center rounded-full bg-amber-50 border border-amber-100 mb-3">
78
+ <i data-feather="table" class="h-5 w-5 text-amber-700"></i>
79
+ </div>
80
+ <h3 class="font-semibold text-gray-900 mb-1">Use Excel/CSV data</h3>
81
+ <p class="text-sm text-gray-600">
82
+ If Excel/CSV exists, it will populate report data areas automatically in later steps.
83
+ </p>
84
+ </div>
85
+ </div>
86
+ </section>
87
+
88
+ <!-- Review uploads -->
89
+ <section class="mb-8" aria-labelledby="review-uploads">
90
+ <h2 id="review-uploads" class="text-xl font-semibold text-gray-800 border-b border-gray-200 pb-2 mb-6">
91
+ Review uploaded files
92
+ </h2>
93
+
94
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
95
+ <!-- Photos -->
96
+ <div class="lg:col-span-2">
97
+ <div class="flex items-center justify-between mb-3">
98
+ <h3 class="text-lg font-semibold text-gray-900">Photos</h3>
99
+ <span id="photoCount" class="text-sm font-semibold text-gray-600">0 files</span>
100
+ </div>
101
+
102
+ <div class="rounded-lg border border-gray-200 bg-white p-4">
103
+ <p class="text-sm text-gray-600 mb-3">
104
+ Select images to use as example figures in the report.
105
+ <span class="font-semibold text-gray-800">Recommended:</span> 2–6 images.
106
+ </p>
107
+
108
+ <div id="photoGrid" class="grid grid-cols-2 md:grid-cols-3 gap-3">
109
+ <!-- populated by JS -->
110
+ </div>
111
+
112
+ <div class="mt-4 flex flex-wrap items-center justify-between gap-3">
113
+ <div class="text-sm text-gray-600">
114
+ Selected for report: <span id="photoSelected" class="font-semibold text-gray-900">0</span>
115
+ </div>
116
+ <div class="flex gap-2">
117
+ <button id="selectAllPhotos" type="button"
118
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm font-semibold text-gray-800 hover:bg-gray-50 transition">
119
+ <i data-feather="check-square" class="h-4 w-4"></i>
120
+ Select all
121
+ </button>
122
+ <button id="clearPhotos" type="button"
123
+ class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm font-semibold text-gray-800 hover:bg-gray-50 transition">
124
+ <i data-feather="square" class="h-4 w-4"></i>
125
+ Clear
126
+ </button>
127
+ </div>
128
+ </div>
129
+ </div>
130
+ </div>
131
+
132
+ <!-- Documents + Data files -->
133
+ <div class="space-y-6">
134
+ <!-- Documents -->
135
+ <div>
136
+ <div class="flex items-center justify-between mb-3">
137
+ <h3 class="text-lg font-semibold text-gray-900">Documents</h3>
138
+ <span id="docCount" class="text-sm font-semibold text-gray-600">0 files</span>
139
+ </div>
140
+
141
+ <div class="rounded-lg border border-gray-200 bg-white p-4">
142
+ <ul id="docList" class="space-y-2 text-sm text-gray-700">
143
+ <!-- populated by JS -->
144
+ </ul>
145
+ <p id="docHint" class="text-xs text-gray-500 mt-3">
146
+ PDFs/DOCX appear here after processing.
147
+ </p>
148
+ </div>
149
+ </div>
150
+
151
+ <!-- Data files -->
152
+ <div>
153
+ <div class="flex items-center justify-between mb-3">
154
+ <h3 class="text-lg font-semibold text-gray-900">Data files</h3>
155
+ <span id="dataCount" class="text-sm font-semibold text-gray-600">0 files</span>
156
+ </div>
157
+
158
+ <div class="rounded-lg border border-gray-200 bg-white p-4">
159
+ <div id="dataBox" class="text-sm text-gray-700">
160
+ <!-- populated by JS -->
161
+ </div>
162
+ <p class="text-xs text-gray-500 mt-3">
163
+ If present, these files will populate report tables/fields automatically.
164
+ </p>
165
+ </div>
166
+ </div>
167
+ </div>
168
+ </div>
169
+ </section>
170
+
171
+ <!-- Continue -->
172
+ <section class="mb-4" aria-label="Continue to report viewer">
173
+ <div class="rounded-lg border border-gray-200 bg-gray-50 p-4 flex flex-col sm:flex-row gap-3 sm:items-center sm:justify-between">
174
+ <div class="text-sm text-gray-600">
175
+ Next step:
176
+ <span id="readyStatus" class="font-semibold text-amber-700">Choose report example images to continue…</span>
177
+ </div>
178
+
179
+ <button
180
+ id="continueBtn"
181
+ type="button"
182
+ class="inline-flex items-center justify-center gap-2 rounded-lg bg-emerald-600 px-5 py-2.5 text-white font-semibold hover:bg-emerald-700 transition disabled:opacity-50 disabled:cursor-not-allowed"
183
+ disabled
184
+ >
185
+ <i data-feather="arrow-right" class="h-5 w-5"></i>
186
+ Continue to Report Viewer
187
+ </button>
188
+ </div>
189
+
190
+ <p class="text-xs text-gray-500 mt-3">
191
+ Note: This page assumes uploads were completed on a previous “Processing” page.
192
+ </p>
193
+ </section>
194
+
195
+ <!-- Footer -->
196
+ <footer class="mt-12 text-center text-xs text-gray-500">
197
+ <p>Prosento - © 2026 All Rights Reserved</p>
198
+ <p class="mt-1">Workflow: Processing → Review uploads → Report viewer → Edit → Export</p>
199
+ </footer>
200
+ </main>
201
+
202
+ <script src="script.js"></script>
203
+ <script>
204
+ document.addEventListener('DOMContentLoaded', () => {
205
+ // Feather icons
206
+ if (window.feather && typeof window.feather.replace === 'function') {
207
+ window.feather.replace();
208
+ }
209
+
210
+ // ---- Simulated "uploads processed" payload ----
211
+ // Replace this with the real payload passed from the progress page (e.g., localStorage, URL params, backend session).
212
+ const processedUploads = {
213
+ photos: [
214
+ { name: 'photo_01.jpg', url: 'https://huggingface.co/spaces/ChristopherJKoen/minefix-simm-inspector/resolve/main/images/Picture2.png' },
215
+ { name: 'photo_02.jpg', url: 'https://huggingface.co/spaces/ChristopherJKoen/minefix-simm-inspector/resolve/main/images/Screenshot%202026-02-02%20100102.png' },
216
+ { name: 'photo_03.jpg', url: 'https://huggingface.co/spaces/ChristopherJKoen/minefix-simm-inspector/resolve/main/images/Picture2.png' }
217
+ ],
218
+ documents: [
219
+ { name: 'inspection_notes.pdf', type: 'PDF' },
220
+ { name: 'supporting_docs.docx', type: 'DOCX' }
221
+ ],
222
+ dataFiles: [
223
+ { name: 'report_data.xlsx', type: 'XLSX' }
224
+ ]
225
+ };
226
+
227
+ // ---- State ----
228
+ const state = {
229
+ selectedPhotoIds: new Set(),
230
+ };
231
+
232
+ // ---- Elements ----
233
+ const photoGrid = document.getElementById('photoGrid');
234
+ const photoCount = document.getElementById('photoCount');
235
+ const photoSelected = document.getElementById('photoSelected');
236
+ const selectAllPhotosBtn = document.getElementById('selectAllPhotos');
237
+ const clearPhotosBtn = document.getElementById('clearPhotos');
238
+
239
+ const docList = document.getElementById('docList');
240
+ const docCount = document.getElementById('docCount');
241
+ const docHint = document.getElementById('docHint');
242
+
243
+ const dataBox = document.getElementById('dataBox');
244
+ const dataCount = document.getElementById('dataCount');
245
+
246
+ const readyStatus = document.getElementById('readyStatus');
247
+ const continueBtn = document.getElementById('continueBtn');
248
+
249
+ function renderUploads() {
250
+ // Photos
251
+ const photos = processedUploads.photos || [];
252
+ photoCount.textContent = `${photos.length} file${photos.length === 1 ? '' : 's'}`;
253
+
254
+ photoGrid.innerHTML = photos.map((p, idx) => {
255
+ const id = `photo-${idx}`;
256
+ return `
257
+ <label class="group cursor-pointer">
258
+ <input type="checkbox" class="sr-only photoCheck" data-photo-id="${id}">
259
+ <div class="rounded-lg border border-gray-200 bg-gray-50 overflow-hidden group-has-[:checked]:ring-2 group-has-[:checked]:ring-emerald-200 group-has-[:checked]:border-emerald-300 transition">
260
+ <div class="relative">
261
+ <img src="${p.url}" alt="${p.name}" class="h-28 w-full object-cover" loading="eager">
262
+ <div class="absolute top-2 right-2 inline-flex items-center justify-center rounded-full bg-white/90 border border-gray-200 p-1.5 text-gray-700 group-has-[:checked]:bg-emerald-50 group-has-[:checked]:border-emerald-200 group-has-[:checked]:text-emerald-700">
263
+ <i data-feather="check" class="h-4 w-4"></i>
264
+ </div>
265
+ </div>
266
+ <div class="p-2">
267
+ <div class="text-xs font-semibold text-gray-900 truncate">${p.name}</div>
268
+ <div class="text-xs text-gray-500">Click to select for report</div>
269
+ </div>
270
+ </div>
271
+ </label>
272
+ `;
273
+ }).join('');
274
+
275
+ // Documents
276
+ const docs = processedUploads.documents || [];
277
+ docCount.textContent = `${docs.length} file${docs.length === 1 ? '' : 's'}`;
278
+ docList.innerHTML = docs.length
279
+ ? docs.map(d => `
280
+ <li class="flex items-center justify-between gap-3 rounded-lg border border-gray-200 bg-gray-50 px-3 py-2">
281
+ <div class="flex items-center gap-2 min-w-0">
282
+ <i data-feather="file-text" class="h-4 w-4 text-gray-600"></i>
283
+ <span class="truncate text-gray-800">${d.name}</span>
284
+ </div>
285
+ <span class="text-xs font-semibold text-gray-600">${d.type}</span>
286
+ </li>
287
+ `).join('')
288
+ : `<li class="text-sm text-gray-500">No supporting documents detected.</li>`;
289
+ docHint.style.display = docs.length ? 'none' : 'block';
290
+
291
+ // Data files
292
+ const dataFiles = processedUploads.dataFiles || [];
293
+ dataCount.textContent = `${dataFiles.length} file${dataFiles.length === 1 ? '' : 's'}`;
294
+ dataBox.innerHTML = dataFiles.length
295
+ ? dataFiles.map(f => `
296
+ <div class="rounded-lg border border-gray-200 bg-gray-50 p-3 text-sm text-gray-700 mb-2 last:mb-0">
297
+ <div class="flex items-center justify-between gap-3">
298
+ <div class="flex items-center gap-2 min-w-0">
299
+ <i data-feather="table" class="h-4 w-4 text-amber-700"></i>
300
+ <span class="truncate font-semibold text-gray-900">${f.name}</span>
301
+ </div>
302
+ <span class="text-xs font-semibold text-gray-600">${f.type}</span>
303
+ </div>
304
+ <div class="text-xs text-gray-600 mt-1">
305
+ Will populate report data areas (tables/fields).
306
+ </div>
307
+ </div>
308
+ `).join('')
309
+ : `
310
+ <div class="rounded-lg border border-gray-200 bg-gray-50 p-3 text-sm text-gray-600">
311
+ <div class="font-semibold text-gray-800 mb-1">No Excel/CSV detected</div>
312
+ If you upload a CSV or Excel file, RepEx can auto-populate report data fields.
313
+ </div>
314
+ `;
315
+
316
+ // Icons for dynamic content
317
+ if (window.feather && typeof window.feather.replace === 'function') {
318
+ window.feather.replace();
319
+ }
320
+ }
321
+
322
+ function updateSelectionState() {
323
+ photoSelected.textContent = String(state.selectedPhotoIds.size);
324
+ const canContinue = state.selectedPhotoIds.size > 0;
325
+ continueBtn.disabled = !canContinue;
326
+
327
+ if (!canContinue) {
328
+ readyStatus.textContent = 'Choose report example images to continue…';
329
+ readyStatus.className = 'font-semibold text-amber-700';
330
+ } else {
331
+ readyStatus.textContent = 'Ready. Continue to report viewer.';
332
+ readyStatus.className = 'font-semibold text-emerald-700';
333
+ }
334
+ }
335
+
336
+ // Init render
337
+ renderUploads();
338
+ updateSelectionState();
339
+
340
+ // Photo selection tracking
341
+ photoGrid.addEventListener('change', (e) => {
342
+ const cb = e.target.closest('.photoCheck');
343
+ if (!cb) return;
344
+ const id = cb.dataset.photoId;
345
+ if (cb.checked) state.selectedPhotoIds.add(id);
346
+ else state.selectedPhotoIds.delete(id);
347
+ updateSelectionState();
348
+ });
349
+
350
+ selectAllPhotosBtn.addEventListener('click', () => {
351
+ photoGrid.querySelectorAll('.photoCheck').forEach(cb => {
352
+ cb.checked = true;
353
+ state.selectedPhotoIds.add(cb.dataset.photoId);
354
+ });
355
+ updateSelectionState();
356
+ });
357
+
358
+ clearPhotosBtn.addEventListener('click', () => {
359
+ photoGrid.querySelectorAll('.photoCheck').forEach(cb => (cb.checked = false));
360
+ state.selectedPhotoIds.clear();
361
+ updateSelectionState();
362
+ });
363
+
364
+ // Continue → go to report-viewer.html and pass selections
365
+ continueBtn.addEventListener('click', () => {
366
+ if (state.selectedPhotoIds.size === 0) return;
367
+
368
+ const selectedIndices = Array.from(state.selectedPhotoIds)
369
+ .map(id => Number(id.replace('photo-', '')))
370
+ .filter(n => Number.isFinite(n));
371
+
372
+ const payload = {
373
+ // what the viewer might need later
374
+ selectedPhotoIndices: selectedIndices,
375
+ uploads: processedUploads,
376
+ createdAt: new Date().toISOString(),
377
+ };
378
+
379
+ localStorage.setItem('repex_report_payload', JSON.stringify(payload));
380
+ window.location.href = 'report-viewer.html';
381
+ });
382
+ });
383
+ </script>
384
+ </body>
385
+ </html>
script.js ADDED
@@ -0,0 +1 @@
 
 
1
+ "use strict";
style.css ADDED
@@ -0,0 +1 @@
 
 
1
+ /* Global overrides for RepEx pages. */
templates/job-sheet-template.html ADDED
@@ -0,0 +1,286 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>SIMM Inspection Job Sheet</title>
7
+
8
+ <!-- Optional project stylesheet -->
9
+ <link rel="stylesheet" href="../style.css" />
10
+
11
+ <!-- Tailwind (CDN OK for prototypes; compile for production) -->
12
+ <script src="https://cdn.tailwindcss.com"></script>
13
+
14
+ <!-- Feather icons -->
15
+ <script defer src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
16
+
17
+ <style>
18
+ @page { size: A4; margin: 10mm; }
19
+
20
+ @media print {
21
+ html, body { background: #fff !important; }
22
+ .no-print { display: none !important; }
23
+ main { box-shadow: none !important; border: none !important; margin: 0 !important; padding: 0 !important; }
24
+ }
25
+
26
+ .avoid-break { break-inside: avoid; page-break-inside: avoid; }
27
+ img { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
28
+ </style>
29
+ </head>
30
+
31
+ <body class="bg-gray-50 print:bg-white">
32
+ <!-- A4-focused container -->
33
+ <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">
34
+ <!-- Header -->
35
+ <header class="mb-4 border-b border-gray-200 pb-3">
36
+ <div class="grid grid-cols-[auto,1fr,auto] items-center gap-3">
37
+ <div class="flex items-center">
38
+ <img
39
+ src="../assets/prosento-logo.png"
40
+ alt="Prosento logo"
41
+ class="h-10 w-auto object-contain"
42
+ loading="eager"
43
+ />
44
+ </div>
45
+
46
+ <div class="text-center leading-tight">
47
+ <h1 class="text-2xl font-bold text-gray-900 whitespace-nowrap">SIMM Inspection Job Sheet</h1>
48
+ <p class="text-sm text-gray-600 whitespace-nowrap">Structural Inspection and Maintenance Management</p>
49
+ </div>
50
+
51
+ <div class="flex items-center justify-end">
52
+ <img
53
+ src="../assets/client-logo.png"
54
+ alt="Client logo placeholder"
55
+ class="h-10 w-auto object-contain"
56
+ loading="eager"
57
+ />
58
+ </div>
59
+ </div>
60
+ </header>
61
+
62
+ <!-- Inspection Details -->
63
+ <section class="mb-4" aria-labelledby="inspection-details-title">
64
+ <h2 id="inspection-details-title" class="text-base font-semibold text-gray-800 border-b border-gray-200 pb-1 mb-2">
65
+ Inspection Details
66
+ </h2>
67
+
68
+ <dl class="grid grid-cols-2 md:grid-cols-4 gap-2 bg-gray-50 border border-gray-200 rounded-lg p-3">
69
+ <div class="space-y-0.5">
70
+ <dt class="text-xs font-medium text-gray-500">Inspection Date</dt>
71
+ <dd class="text-sm font-semibold text-gray-900">2023-11-15</dd>
72
+ </div>
73
+
74
+ <div class="space-y-0.5">
75
+ <dt class="text-xs font-medium text-gray-500">Inspector</dt>
76
+ <dd class="text-sm font-semibold text-gray-900">John Doe</dd>
77
+ </div>
78
+
79
+ <div class="space-y-0.5">
80
+ <dt class="text-xs font-medium text-gray-500">Accompanied By</dt>
81
+ <dd class="text-sm font-semibold text-gray-900">John Doe</dd>
82
+ </div>
83
+
84
+ <div class="space-y-0.5">
85
+ <dt class="text-xs font-medium text-gray-500">Document No</dt>
86
+ <dd class="text-sm font-mono font-semibold text-gray-900">SIMM-JS-2023-001</dd>
87
+ </div>
88
+
89
+ <div class="space-y-0.5 md:col-span-2">
90
+ <dt class="text-xs font-medium text-gray-500">Project</dt>
91
+ <dd class="text-sm font-semibold text-gray-900">North Pit – Sector 4 Conveyor Upgrade</dd>
92
+ </div>
93
+
94
+ <div class="space-y-0.5 md:col-span-2">
95
+ <dt class="text-xs font-medium text-gray-500">Client / Site</dt>
96
+ <dd class="text-sm font-semibold text-gray-900">Tronox</dd>
97
+ </div>
98
+ </dl>
99
+ </section>
100
+
101
+ <!-- Observations and Findings -->
102
+ <section class="mb-4" aria-labelledby="observations-title">
103
+ <h2 id="observations-title" class="text-base font-semibold text-gray-800 border-b border-gray-200 pb-1 mb-2">
104
+ Observations and Findings
105
+ </h2>
106
+
107
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
108
+ <!-- Left column -->
109
+ <div class="space-y-2">
110
+ <div class="grid grid-cols-2 gap-2">
111
+ <div class="space-y-0.5">
112
+ <div class="text-xs font-medium text-gray-500">Reference</div>
113
+ <div class="text-sm font-semibold text-gray-900">REF-7821</div>
114
+ </div>
115
+
116
+ <div class="space-y-0.5">
117
+ <div class="text-xs font-medium text-gray-500">Action Type</div>
118
+ <div class="text-sm font-semibold text-gray-900">Structural Repair</div>
119
+ </div>
120
+
121
+ <div class="space-y-0.5 col-span-2">
122
+ <div class="text-xs font-medium text-gray-500">Item Description</div>
123
+ <div class="text-sm font-semibold text-gray-900">Main support beam for conveyor CV-04</div>
124
+ </div>
125
+ </div>
126
+ </div>
127
+
128
+ <!-- Right column -->
129
+ <div class="space-y-2">
130
+ <div class="space-y-0.5">
131
+ <div class="text-xs font-medium text-gray-500">Functional Location</div>
132
+ <div class="text-sm font-semibold text-gray-900">Conveyor Support - CV-04-SUP-02</div>
133
+ </div>
134
+ </div>
135
+
136
+ <!-- Centered Category + Priority (must be direct child of the grid) -->
137
+ <div class="md:col-span-2 flex justify-center">
138
+ <div class="inline-flex items-center gap-10">
139
+ <div class="text-center space-y-1">
140
+ <div class="text-xs font-medium text-gray-500">Category</div>
141
+ <span
142
+ id="condition-rating"
143
+ class="badge inline-flex items-center justify-center rounded-md border px-4 py-1 text-sm font-semibold"
144
+ aria-label="Category rating"
145
+ >
146
+ <!-- populated by JS -->
147
+ </span>
148
+ </div>
149
+
150
+ <div class="text-center space-y-1">
151
+ <div class="text-xs font-medium text-gray-500">Priority</div>
152
+ <span
153
+ id="priority-rating"
154
+ class="badge inline-flex items-center justify-center rounded-md border px-4 py-1 text-sm font-semibold"
155
+ aria-label="Priority rating"
156
+ >
157
+ <!-- populated by JS -->
158
+ </span>
159
+ </div>
160
+ </div>
161
+ </div>
162
+
163
+ <!-- Full-width: Condition Description -->
164
+ <div class="md:col-span-2 space-y-1">
165
+ <div class="text-xs font-medium text-gray-500">Condition Description</div>
166
+ <div class="bg-amber-50 border-l-4 border-amber-300 p-2 rounded-sm">
167
+ <p class="text-amber-800 text-sm font-semibold leading-snug">
168
+ Visible corrosion on lower flange with ~15% material loss. Surface pitting along entire length.
169
+ </p>
170
+ </div>
171
+ </div>
172
+
173
+ <!-- Full-width: Required Action -->
174
+ <div class="md:col-span-2 space-y-1">
175
+ <div class="text-xs font-medium text-gray-500">Required Action</div>
176
+ <div class="bg-blue-50 border-l-4 border-blue-300 p-2 rounded-sm">
177
+ <p class="text-blue-800 text-sm font-semibold leading-snug">
178
+ Clean exposed rebar; apply corrosion protection; use wet-to-dry epoxy; reinstate with concrete repair.
179
+ Complete within next 12 months to limit further degradation.
180
+ </p>
181
+ </div>
182
+ </div>
183
+ </div>
184
+ </section>
185
+
186
+ <!-- Photo Documentation -->
187
+ <section class="mb-4 avoid-break" aria-labelledby="photo-doc-title">
188
+ <h2 id="photo-doc-title" class="text-base font-semibold text-gray-800 border-b border-gray-200 pb-1 mb-2">
189
+ Photo Documentation
190
+ </h2>
191
+
192
+ <div class="grid grid-cols-2 gap-3">
193
+ <figure class="border border-gray-200 rounded-lg p-2 bg-gray-50">
194
+ <img
195
+ src="https://huggingface.co/spaces/ChristopherJKoen/minefix-simm-inspector/resolve/main/images/Screenshot%202026-02-02%20100102.png"
196
+ alt="Photo 1"
197
+ class="w-full h-40 object-contain mx-auto"
198
+ loading="eager"
199
+ />
200
+ <figcaption class="text-[11px] text-gray-600 mt-1 text-center leading-tight">
201
+ Fig 1: Ref 1.1 – Concrete spalling (example)
202
+ </figcaption>
203
+ </figure>
204
+
205
+ <figure class="border border-gray-200 rounded-lg p-2 bg-gray-50 relative">
206
+ <img
207
+ src="https://huggingface.co/spaces/ChristopherJKoen/minefix-simm-inspector/resolve/main/images/Picture2.png"
208
+ alt="Photo 2"
209
+ class="w-full h-40 object-contain mx-auto"
210
+ loading="eager"
211
+ />
212
+ <div class="absolute top-2 left-2 bg-white/95 text-black text-[11px] font-bold px-2 py-1 rounded border border-gray-300">
213
+ #1.2 [C3, P3] Moderate Corrosion
214
+ </div>
215
+ <figcaption class="text-[11px] text-gray-600 mt-1 text-center leading-tight">
216
+ Fig 2: Ref 1.2 – Walkway corrosion (example)
217
+ </figcaption>
218
+ </figure>
219
+ </div>
220
+ </section>
221
+
222
+ <!-- Signatures -->
223
+ <section class="mt-6" aria-label="Signatures">
224
+ <div class="grid grid-cols-3 gap-4">
225
+ <div class="border-t pt-2">
226
+ <div class="text-xs font-medium text-gray-500">Inspected By</div>
227
+ <div class="h-10 mt-1 border-b border-gray-300"></div>
228
+ <p class="text-[11px] text-gray-500 mt-1">Name / Signature / Date</p>
229
+ </div>
230
+
231
+ <div class="border-t pt-2">
232
+ <div class="text-xs font-medium text-gray-500">Approved By</div>
233
+ <div class="h-10 mt-1 border-b border-gray-300"></div>
234
+ <p class="text-[11px] text-gray-500 mt-1">Name / Signature / Date</p>
235
+ </div>
236
+
237
+ <div class="border-t pt-2">
238
+ <div class="text-xs font-medium text-gray-500">Completed By</div>
239
+ <div class="h-10 mt-1 border-b border-gray-300"></div>
240
+ <p class="text-[11px] text-gray-500 mt-1">Name / Signature / Date</p>
241
+ </div>
242
+ </div>
243
+ </section>
244
+
245
+ <!-- Footer -->
246
+ <footer class="mt-4 text-center text-[11px] text-gray-500">
247
+ <p>Prosento - © 2026 All Rights Reserved</p>
248
+ <p class="mt-0.5">Automatically generated job sheet</p>
249
+ </footer>
250
+ </main>
251
+
252
+ <script>
253
+ document.addEventListener('DOMContentLoaded', () => {
254
+ // Badge tone system
255
+ const TONES = {
256
+ amber: ['bg-amber-50', 'text-amber-800', 'border-amber-200'],
257
+ emerald: ['bg-emerald-50', 'text-emerald-800', 'border-emerald-200'],
258
+ gray: ['bg-gray-50', 'text-gray-700', 'border-gray-200'],
259
+ };
260
+
261
+ const BASE_BADGE = 'inline-flex items-center justify-center rounded-md border px-4 py-1 text-sm font-semibold';
262
+
263
+ function setBadge(id, text, toneKey) {
264
+ const el = document.getElementById(id);
265
+ if (!el) return;
266
+ const tone = TONES[toneKey] || TONES.gray;
267
+ el.className = `${BASE_BADGE} ${tone.join(' ')}`;
268
+ el.textContent = text;
269
+ }
270
+
271
+ setBadge('condition-rating', '3 - Poor', 'amber');
272
+ setBadge('priority-rating', '3 - 3 Years', 'emerald');
273
+
274
+ // Icons
275
+ if (window.feather && typeof window.feather.replace === 'function') {
276
+ window.feather.replace();
277
+ }
278
+
279
+ // Ensure images are loaded before printing
280
+ window.addEventListener('beforeprint', () => {
281
+ document.querySelectorAll('img').forEach(img => (img.loading = 'eager'));
282
+ });
283
+ });
284
+ </script>
285
+ </body>
286
+ </html>