@primocaredentgroup/dental-digital-intake-ui 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +284 -0
- package/dist/index.js +2567 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
- package/src/styles/intake.css +819 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2567 @@
|
|
|
1
|
+
// src/components/DigitalIntakeDashboard.tsx
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
|
|
4
|
+
// src/components/StatusBadge.tsx
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
var map = {
|
|
7
|
+
draft: "#94a3b8",
|
|
8
|
+
waiting_files: "#38bdf8",
|
|
9
|
+
uploaded: "#818cf8",
|
|
10
|
+
parsing: "#fbbf24",
|
|
11
|
+
needs_review: "#fb923c",
|
|
12
|
+
classified: "#34d399",
|
|
13
|
+
linked: "#22d3ee",
|
|
14
|
+
validated: "#4ade80",
|
|
15
|
+
ready_for_next_step: "#a78bfa",
|
|
16
|
+
archived: "#64748b",
|
|
17
|
+
failed: "#f87171"
|
|
18
|
+
};
|
|
19
|
+
function StatusBadge({ status }) {
|
|
20
|
+
const c = map[status] ?? "#94a3b8";
|
|
21
|
+
return /* @__PURE__ */ jsxs(
|
|
22
|
+
"span",
|
|
23
|
+
{
|
|
24
|
+
className: "badge",
|
|
25
|
+
style: {
|
|
26
|
+
background: `${c}22`,
|
|
27
|
+
color: "#f8fafc",
|
|
28
|
+
border: `1px solid ${c}55`
|
|
29
|
+
},
|
|
30
|
+
children: [
|
|
31
|
+
/* @__PURE__ */ jsx(
|
|
32
|
+
"span",
|
|
33
|
+
{
|
|
34
|
+
style: {
|
|
35
|
+
width: 7,
|
|
36
|
+
height: 7,
|
|
37
|
+
borderRadius: "50%",
|
|
38
|
+
background: c,
|
|
39
|
+
display: "inline-block"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
),
|
|
43
|
+
status.replace(/_/g, " ")
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
function SmallBadge({
|
|
49
|
+
children,
|
|
50
|
+
variant
|
|
51
|
+
}) {
|
|
52
|
+
const cls = variant === "format" ? "badge-format" : "badge-role";
|
|
53
|
+
return /* @__PURE__ */ jsx("span", { className: `badge ${cls}`, children });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/lib/mockHostContext.ts
|
|
57
|
+
var hostAppLabel = (h) => ({
|
|
58
|
+
primoupcore: "PrimoUP Core",
|
|
59
|
+
primolabcore: "PrimoLab Core",
|
|
60
|
+
standalone: "Standalone"
|
|
61
|
+
})[h] ?? h;
|
|
62
|
+
var originLabel = (o) => ({
|
|
63
|
+
clinic_intraoral: "Clinica \xB7 intraorale",
|
|
64
|
+
lab_bench_scan: "Laboratorio \xB7 da banco",
|
|
65
|
+
unknown: "Origine non determinata"
|
|
66
|
+
})[o] ?? o;
|
|
67
|
+
var vendorLabel = (v) => ({
|
|
68
|
+
"3shape": "3Shape",
|
|
69
|
+
itero: "iTero",
|
|
70
|
+
carestream: "Carestream",
|
|
71
|
+
sirona: "Sirona / Elios",
|
|
72
|
+
exocad: "Exocad",
|
|
73
|
+
unknown: "Vendor sconosciuto"
|
|
74
|
+
})[v] ?? v;
|
|
75
|
+
|
|
76
|
+
// src/lib/validationUi.ts
|
|
77
|
+
function defaultProfileForOriginUi(origin) {
|
|
78
|
+
if (origin === "clinic_intraoral") return "clinic_intraoral_default";
|
|
79
|
+
if (origin === "lab_bench_scan") return "lab_bench_scan_default";
|
|
80
|
+
return "generic_default";
|
|
81
|
+
}
|
|
82
|
+
var validationProfileLabel = {
|
|
83
|
+
clinic_intraoral_default: "Clinica intraorale (default)",
|
|
84
|
+
lab_bench_scan_default: "Laboratorio / scansione da banco (default)",
|
|
85
|
+
generic_default: "Generico (default)"
|
|
86
|
+
};
|
|
87
|
+
var readinessLabelUi = {
|
|
88
|
+
ready_for_lab: "Ready for Lab",
|
|
89
|
+
ready_for_cad: "Ready for CAD",
|
|
90
|
+
ready_for_next_step: "Ready for Next Step"
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// src/components/DigitalIntakeDashboard.tsx
|
|
94
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
95
|
+
var defaultDashboardFilters = () => ({
|
|
96
|
+
status: "",
|
|
97
|
+
origin: "",
|
|
98
|
+
vendor: "",
|
|
99
|
+
hostApp: "",
|
|
100
|
+
validationProfile: ""
|
|
101
|
+
});
|
|
102
|
+
function DigitalIntakeDashboard({
|
|
103
|
+
acquisitions,
|
|
104
|
+
filters,
|
|
105
|
+
onFiltersChange,
|
|
106
|
+
onOpen,
|
|
107
|
+
onCreateOpen,
|
|
108
|
+
onSeedDemo
|
|
109
|
+
}) {
|
|
110
|
+
const [seedBusy, setSeedBusy] = useState(false);
|
|
111
|
+
const set = (patch) => onFiltersChange({ ...filters, ...patch });
|
|
112
|
+
const kpis = {
|
|
113
|
+
total: acquisitions?.length ?? 0,
|
|
114
|
+
review: acquisitions?.filter((a) => a.status === "needs_review").length ?? 0,
|
|
115
|
+
readyLab: acquisitions?.filter(
|
|
116
|
+
(a) => a.status === "ready_for_next_step" && a.validationResult?.readinessLabel === "ready_for_lab"
|
|
117
|
+
).length ?? 0,
|
|
118
|
+
readyCad: acquisitions?.filter(
|
|
119
|
+
(a) => a.status === "ready_for_next_step" && a.validationResult?.readinessLabel === "ready_for_cad"
|
|
120
|
+
).length ?? 0,
|
|
121
|
+
validationFailed: acquisitions?.filter(
|
|
122
|
+
(a) => a.validationResult != null && a.validationResult.isValid === false
|
|
123
|
+
).length ?? 0,
|
|
124
|
+
failed: acquisitions?.filter((a) => a.status === "failed").length ?? 0
|
|
125
|
+
};
|
|
126
|
+
return /* @__PURE__ */ jsxs2("div", { children: [
|
|
127
|
+
/* @__PURE__ */ jsxs2("div", { className: "kpi-grid", children: [
|
|
128
|
+
/* @__PURE__ */ jsxs2("div", { className: "kpi", children: [
|
|
129
|
+
/* @__PURE__ */ jsx2("div", { className: "kpi-value", children: kpis.total }),
|
|
130
|
+
/* @__PURE__ */ jsx2("div", { className: "kpi-label", children: "Acquisizioni in lista" })
|
|
131
|
+
] }),
|
|
132
|
+
/* @__PURE__ */ jsxs2("div", { className: "kpi", children: [
|
|
133
|
+
/* @__PURE__ */ jsx2("div", { className: "kpi-value", children: kpis.review }),
|
|
134
|
+
/* @__PURE__ */ jsx2("div", { className: "kpi-label", children: "Needs review" })
|
|
135
|
+
] }),
|
|
136
|
+
/* @__PURE__ */ jsxs2("div", { className: "kpi", children: [
|
|
137
|
+
/* @__PURE__ */ jsx2("div", { className: "kpi-value", children: kpis.readyLab }),
|
|
138
|
+
/* @__PURE__ */ jsx2("div", { className: "kpi-label", children: "Ready for Lab" })
|
|
139
|
+
] }),
|
|
140
|
+
/* @__PURE__ */ jsxs2("div", { className: "kpi", children: [
|
|
141
|
+
/* @__PURE__ */ jsx2("div", { className: "kpi-value", children: kpis.readyCad }),
|
|
142
|
+
/* @__PURE__ */ jsx2("div", { className: "kpi-label", children: "Ready for CAD" })
|
|
143
|
+
] }),
|
|
144
|
+
/* @__PURE__ */ jsxs2("div", { className: "kpi", children: [
|
|
145
|
+
/* @__PURE__ */ jsx2("div", { className: "kpi-value", children: kpis.validationFailed }),
|
|
146
|
+
/* @__PURE__ */ jsx2("div", { className: "kpi-label", children: "Validazione fallita" })
|
|
147
|
+
] }),
|
|
148
|
+
/* @__PURE__ */ jsxs2("div", { className: "kpi", children: [
|
|
149
|
+
/* @__PURE__ */ jsx2("div", { className: "kpi-value", children: kpis.failed }),
|
|
150
|
+
/* @__PURE__ */ jsx2("div", { className: "kpi-label", children: "Failed" })
|
|
151
|
+
] })
|
|
152
|
+
] }),
|
|
153
|
+
/* @__PURE__ */ jsxs2("div", { className: "panel", style: { marginBottom: "1.25rem" }, children: [
|
|
154
|
+
/* @__PURE__ */ jsx2("h3", { style: { marginTop: 0 }, children: "Filtri" }),
|
|
155
|
+
/* @__PURE__ */ jsxs2("div", { className: "field-grid", children: [
|
|
156
|
+
/* @__PURE__ */ jsxs2("div", { className: "field", children: [
|
|
157
|
+
/* @__PURE__ */ jsx2("label", { children: "Stato" }),
|
|
158
|
+
/* @__PURE__ */ jsxs2(
|
|
159
|
+
"select",
|
|
160
|
+
{
|
|
161
|
+
value: filters.status,
|
|
162
|
+
onChange: (e) => set({ status: e.target.value }),
|
|
163
|
+
children: [
|
|
164
|
+
/* @__PURE__ */ jsx2("option", { value: "", children: "Tutti" }),
|
|
165
|
+
/* @__PURE__ */ jsx2("option", { value: "draft", children: "draft" }),
|
|
166
|
+
/* @__PURE__ */ jsx2("option", { value: "waiting_files", children: "waiting_files" }),
|
|
167
|
+
/* @__PURE__ */ jsx2("option", { value: "uploaded", children: "uploaded" }),
|
|
168
|
+
/* @__PURE__ */ jsx2("option", { value: "parsing", children: "parsing" }),
|
|
169
|
+
/* @__PURE__ */ jsx2("option", { value: "needs_review", children: "needs_review" }),
|
|
170
|
+
/* @__PURE__ */ jsx2("option", { value: "classified", children: "classified" }),
|
|
171
|
+
/* @__PURE__ */ jsx2("option", { value: "linked", children: "linked" }),
|
|
172
|
+
/* @__PURE__ */ jsx2("option", { value: "validated", children: "validated" }),
|
|
173
|
+
/* @__PURE__ */ jsx2("option", { value: "ready_for_next_step", children: "ready_for_next_step" }),
|
|
174
|
+
/* @__PURE__ */ jsx2("option", { value: "archived", children: "archived" }),
|
|
175
|
+
/* @__PURE__ */ jsx2("option", { value: "failed", children: "failed" })
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
)
|
|
179
|
+
] }),
|
|
180
|
+
/* @__PURE__ */ jsxs2("div", { className: "field", children: [
|
|
181
|
+
/* @__PURE__ */ jsx2("label", { children: "Origine" }),
|
|
182
|
+
/* @__PURE__ */ jsxs2(
|
|
183
|
+
"select",
|
|
184
|
+
{
|
|
185
|
+
value: filters.origin,
|
|
186
|
+
onChange: (e) => set({ origin: e.target.value }),
|
|
187
|
+
children: [
|
|
188
|
+
/* @__PURE__ */ jsx2("option", { value: "", children: "Tutte" }),
|
|
189
|
+
/* @__PURE__ */ jsx2("option", { value: "clinic_intraoral", children: "clinic_intraoral" }),
|
|
190
|
+
/* @__PURE__ */ jsx2("option", { value: "lab_bench_scan", children: "lab_bench_scan" }),
|
|
191
|
+
/* @__PURE__ */ jsx2("option", { value: "unknown", children: "unknown" })
|
|
192
|
+
]
|
|
193
|
+
}
|
|
194
|
+
)
|
|
195
|
+
] }),
|
|
196
|
+
/* @__PURE__ */ jsxs2("div", { className: "field", children: [
|
|
197
|
+
/* @__PURE__ */ jsx2("label", { children: "Vendor" }),
|
|
198
|
+
/* @__PURE__ */ jsxs2(
|
|
199
|
+
"select",
|
|
200
|
+
{
|
|
201
|
+
value: filters.vendor,
|
|
202
|
+
onChange: (e) => set({ vendor: e.target.value }),
|
|
203
|
+
children: [
|
|
204
|
+
/* @__PURE__ */ jsx2("option", { value: "", children: "Tutti" }),
|
|
205
|
+
/* @__PURE__ */ jsx2("option", { value: "3shape", children: "3shape" }),
|
|
206
|
+
/* @__PURE__ */ jsx2("option", { value: "itero", children: "itero" }),
|
|
207
|
+
/* @__PURE__ */ jsx2("option", { value: "carestream", children: "carestream" }),
|
|
208
|
+
/* @__PURE__ */ jsx2("option", { value: "sirona", children: "sirona" }),
|
|
209
|
+
/* @__PURE__ */ jsx2("option", { value: "exocad", children: "exocad" }),
|
|
210
|
+
/* @__PURE__ */ jsx2("option", { value: "unknown", children: "unknown" })
|
|
211
|
+
]
|
|
212
|
+
}
|
|
213
|
+
)
|
|
214
|
+
] }),
|
|
215
|
+
/* @__PURE__ */ jsxs2("div", { className: "field", children: [
|
|
216
|
+
/* @__PURE__ */ jsx2("label", { children: "Host app" }),
|
|
217
|
+
/* @__PURE__ */ jsxs2(
|
|
218
|
+
"select",
|
|
219
|
+
{
|
|
220
|
+
value: filters.hostApp,
|
|
221
|
+
onChange: (e) => set({ hostApp: e.target.value }),
|
|
222
|
+
children: [
|
|
223
|
+
/* @__PURE__ */ jsx2("option", { value: "", children: "Tutte" }),
|
|
224
|
+
/* @__PURE__ */ jsx2("option", { value: "primoupcore", children: "primoupcore" }),
|
|
225
|
+
/* @__PURE__ */ jsx2("option", { value: "primolabcore", children: "primolabcore" }),
|
|
226
|
+
/* @__PURE__ */ jsx2("option", { value: "standalone", children: "standalone" })
|
|
227
|
+
]
|
|
228
|
+
}
|
|
229
|
+
)
|
|
230
|
+
] }),
|
|
231
|
+
/* @__PURE__ */ jsxs2("div", { className: "field", children: [
|
|
232
|
+
/* @__PURE__ */ jsx2("label", { children: "Profilo validazione" }),
|
|
233
|
+
/* @__PURE__ */ jsxs2(
|
|
234
|
+
"select",
|
|
235
|
+
{
|
|
236
|
+
value: filters.validationProfile,
|
|
237
|
+
onChange: (e) => set({ validationProfile: e.target.value }),
|
|
238
|
+
children: [
|
|
239
|
+
/* @__PURE__ */ jsx2("option", { value: "", children: "Tutti" }),
|
|
240
|
+
/* @__PURE__ */ jsx2("option", { value: "clinic_intraoral_default", children: validationProfileLabel.clinic_intraoral_default }),
|
|
241
|
+
/* @__PURE__ */ jsx2("option", { value: "lab_bench_scan_default", children: validationProfileLabel.lab_bench_scan_default }),
|
|
242
|
+
/* @__PURE__ */ jsx2("option", { value: "generic_default", children: validationProfileLabel.generic_default })
|
|
243
|
+
]
|
|
244
|
+
}
|
|
245
|
+
)
|
|
246
|
+
] })
|
|
247
|
+
] })
|
|
248
|
+
] }),
|
|
249
|
+
/* @__PURE__ */ jsxs2("div", { className: "btn-row", style: { marginBottom: "1rem" }, children: [
|
|
250
|
+
onCreateOpen ? /* @__PURE__ */ jsx2(
|
|
251
|
+
"button",
|
|
252
|
+
{
|
|
253
|
+
type: "button",
|
|
254
|
+
className: "btn btn-primary",
|
|
255
|
+
onClick: () => onCreateOpen(),
|
|
256
|
+
children: "Nuova acquisizione"
|
|
257
|
+
}
|
|
258
|
+
) : null,
|
|
259
|
+
onSeedDemo ? /* @__PURE__ */ jsx2(
|
|
260
|
+
"button",
|
|
261
|
+
{
|
|
262
|
+
type: "button",
|
|
263
|
+
className: "btn btn-ghost",
|
|
264
|
+
disabled: seedBusy,
|
|
265
|
+
onClick: () => {
|
|
266
|
+
setSeedBusy(true);
|
|
267
|
+
void onSeedDemo().catch(() => {
|
|
268
|
+
}).finally(() => setSeedBusy(false));
|
|
269
|
+
},
|
|
270
|
+
children: "Carica dati demo"
|
|
271
|
+
}
|
|
272
|
+
) : null
|
|
273
|
+
] }),
|
|
274
|
+
!acquisitions ? /* @__PURE__ */ jsx2("p", { className: "muted", children: "Caricamento\u2026" }) : /* @__PURE__ */ jsx2("div", { className: "acq-grid", children: acquisitions.map((a) => /* @__PURE__ */ jsxs2(
|
|
275
|
+
"button",
|
|
276
|
+
{
|
|
277
|
+
type: "button",
|
|
278
|
+
className: "acq-card",
|
|
279
|
+
onClick: () => onOpen(a._id),
|
|
280
|
+
children: [
|
|
281
|
+
/* @__PURE__ */ jsx2("div", { className: "btn-row", style: { marginBottom: "0.5rem" }, children: /* @__PURE__ */ jsx2(StatusBadge, { status: a.status }) }),
|
|
282
|
+
/* @__PURE__ */ jsx2("div", { className: "acq-card-title", children: a.title }),
|
|
283
|
+
/* @__PURE__ */ jsxs2("p", { className: "acq-card-sub", children: [
|
|
284
|
+
originLabel(a.origin),
|
|
285
|
+
" \xB7 ",
|
|
286
|
+
hostAppLabel(a.hostApp)
|
|
287
|
+
] }),
|
|
288
|
+
/* @__PURE__ */ jsxs2("p", { className: "acq-card-sub", children: [
|
|
289
|
+
"Vendor: ",
|
|
290
|
+
vendorLabel(a.sourceVendor),
|
|
291
|
+
" (",
|
|
292
|
+
a.vendorConfidence.toFixed(2),
|
|
293
|
+
")"
|
|
294
|
+
] }),
|
|
295
|
+
/* @__PURE__ */ jsxs2("p", { className: "acq-card-sub", children: [
|
|
296
|
+
"Profilo validazione:",
|
|
297
|
+
" ",
|
|
298
|
+
/* @__PURE__ */ jsx2("code", { style: { fontSize: "0.8rem" }, children: a.validationProfile ?? "\u2014" })
|
|
299
|
+
] })
|
|
300
|
+
]
|
|
301
|
+
},
|
|
302
|
+
a._id
|
|
303
|
+
)) })
|
|
304
|
+
] });
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// src/components/DigitalAcquisitionDetail.tsx
|
|
308
|
+
import { useMemo, useState as useState5 } from "react";
|
|
309
|
+
|
|
310
|
+
// src/lib/meshFormats.ts
|
|
311
|
+
var MESH_VIEWER_FORMATS = ["stl", "ply", "obj"];
|
|
312
|
+
function isMeshViewerFormat(format) {
|
|
313
|
+
const f = format.toLowerCase();
|
|
314
|
+
return MESH_VIEWER_FORMATS.includes(f);
|
|
315
|
+
}
|
|
316
|
+
var MESH_VIEWER_LARGE_BYTES = 250 * 1024 * 1024;
|
|
317
|
+
|
|
318
|
+
// src/components/FileList.tsx
|
|
319
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
320
|
+
function FileList({
|
|
321
|
+
files,
|
|
322
|
+
onVisualizeMesh,
|
|
323
|
+
renderFileActions
|
|
324
|
+
}) {
|
|
325
|
+
if (!files?.length) return /* @__PURE__ */ jsx3("p", { className: "muted", children: "Nessun file caricato." });
|
|
326
|
+
return /* @__PURE__ */ jsx3("div", { children: files.map((f) => /* @__PURE__ */ jsxs3("div", { className: "file-row", children: [
|
|
327
|
+
/* @__PURE__ */ jsxs3("div", { className: "file-row-main", style: { flex: "1 1 200px" }, children: [
|
|
328
|
+
/* @__PURE__ */ jsx3("div", { style: { fontWeight: 600, wordBreak: "break-word" }, children: f.originalName }),
|
|
329
|
+
/* @__PURE__ */ jsxs3("div", { className: "muted file-row-path", style: { fontSize: "0.8rem" }, children: [
|
|
330
|
+
f.originalPath,
|
|
331
|
+
f.extractedFromZip && f.extractedPath ? /* @__PURE__ */ jsxs3("span", { children: [
|
|
332
|
+
" ",
|
|
333
|
+
"\xB7 da ZIP: ",
|
|
334
|
+
/* @__PURE__ */ jsx3("code", { children: f.extractedPath })
|
|
335
|
+
] }) : null
|
|
336
|
+
] }),
|
|
337
|
+
f.warnings?.length ? /* @__PURE__ */ jsx3("div", { className: "warning-box", style: { marginTop: "0.35rem" }, children: f.warnings.join(" \xB7 ") }) : null
|
|
338
|
+
] }),
|
|
339
|
+
/* @__PURE__ */ jsx3(SmallBadge, { variant: "format", children: f.format }),
|
|
340
|
+
/* @__PURE__ */ jsx3(SmallBadge, { variant: "role", children: f.role }),
|
|
341
|
+
/* @__PURE__ */ jsxs3("span", { className: "muted", style: { fontSize: "0.8rem" }, children: [
|
|
342
|
+
(f.sizeBytes / 1024).toFixed(1),
|
|
343
|
+
" KB"
|
|
344
|
+
] }),
|
|
345
|
+
/* @__PURE__ */ jsx3("span", { className: "muted", style: { fontSize: "0.75rem" }, children: f.classificationSource }),
|
|
346
|
+
renderFileActions ? renderFileActions(f) : null,
|
|
347
|
+
onVisualizeMesh && isMeshViewerFormat(f.format) && f.storageId ? /* @__PURE__ */ jsx3(
|
|
348
|
+
"button",
|
|
349
|
+
{
|
|
350
|
+
type: "button",
|
|
351
|
+
className: "btn btn-ghost",
|
|
352
|
+
style: { fontSize: "0.75rem", padding: "0.25rem 0.5rem" },
|
|
353
|
+
onClick: () => onVisualizeMesh(f),
|
|
354
|
+
children: "Visualizza"
|
|
355
|
+
}
|
|
356
|
+
) : null
|
|
357
|
+
] }, f._id)) });
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/components/UploadPanel.tsx
|
|
361
|
+
import { useState as useState2 } from "react";
|
|
362
|
+
import { extractZipSafely, collectCadMetadataFromZipEntries } from "@primocaredentgroup/dental-digital-intake-shared";
|
|
363
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
364
|
+
function guessContentType(extension) {
|
|
365
|
+
const e = extension.toLowerCase();
|
|
366
|
+
if (e === "stl") return "model/stl";
|
|
367
|
+
if (e === "ply") return "application/octet-stream";
|
|
368
|
+
if (e === "obj") return "text/plain";
|
|
369
|
+
if (e === "jpg" || e === "jpeg") return "image/jpeg";
|
|
370
|
+
if (e === "png") return "image/png";
|
|
371
|
+
if (e === "pdf") return "application/pdf";
|
|
372
|
+
if (e === "json") return "application/json";
|
|
373
|
+
if (e === "xml") return "application/xml";
|
|
374
|
+
if (e === "txt") return "text/plain";
|
|
375
|
+
if (e === "dcm") return "application/dicom";
|
|
376
|
+
return void 0;
|
|
377
|
+
}
|
|
378
|
+
function UploadPanel({
|
|
379
|
+
upload
|
|
380
|
+
}) {
|
|
381
|
+
const [busy, setBusy] = useState2(false);
|
|
382
|
+
const [err, setErr] = useState2(null);
|
|
383
|
+
const [status, setStatus] = useState2(null);
|
|
384
|
+
const [dragOver, setDragOver] = useState2(false);
|
|
385
|
+
const uploadZipViaBrowser = async (file) => {
|
|
386
|
+
setErr(null);
|
|
387
|
+
setBusy(true);
|
|
388
|
+
setStatus(`Lettura ed estrazione ZIP in locale (${file.name})\u2026`);
|
|
389
|
+
try {
|
|
390
|
+
const buf = new Uint8Array(await file.arrayBuffer());
|
|
391
|
+
const extracted = extractZipSafely(buf);
|
|
392
|
+
if (!extracted.ok) {
|
|
393
|
+
setErr(extracted.fatal);
|
|
394
|
+
setStatus(null);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const entries = extracted.entries;
|
|
398
|
+
setStatus(
|
|
399
|
+
`${entries.length} file da caricare (estrazione browser). Upload in corso\u2026`
|
|
400
|
+
);
|
|
401
|
+
const urls = await upload.generateBulkPackageUploadUrls(entries.length);
|
|
402
|
+
if (urls.length !== entries.length) {
|
|
403
|
+
throw new Error("Numero di URL di upload incoerente con i file estratti.");
|
|
404
|
+
}
|
|
405
|
+
const filesPayload = [];
|
|
406
|
+
for (let i = 0; i < entries.length; i++) {
|
|
407
|
+
const eEntry = entries[i];
|
|
408
|
+
setStatus(`Upload ${i + 1}/${entries.length}: ${eEntry.safePath}\u2026`);
|
|
409
|
+
const ct = guessContentType(eEntry.extension);
|
|
410
|
+
const init = {
|
|
411
|
+
method: "POST",
|
|
412
|
+
body: new Blob([eEntry.data])
|
|
413
|
+
};
|
|
414
|
+
if (ct) {
|
|
415
|
+
init.headers = { "Content-Type": ct };
|
|
416
|
+
}
|
|
417
|
+
const uploadRes = await fetch(urls[i], init);
|
|
418
|
+
if (!uploadRes.ok) {
|
|
419
|
+
throw new Error(
|
|
420
|
+
`Upload fallito per ${eEntry.safePath} (HTTP ${uploadRes.status}).`
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
const body = await uploadRes.json();
|
|
424
|
+
if (!body.storageId) {
|
|
425
|
+
throw new Error(`Risposta storage senza storageId per ${eEntry.safePath}.`);
|
|
426
|
+
}
|
|
427
|
+
const baseName = eEntry.safePath.split("/").pop() ?? eEntry.safePath;
|
|
428
|
+
filesPayload.push({
|
|
429
|
+
storageId: body.storageId,
|
|
430
|
+
originalName: baseName,
|
|
431
|
+
extractedPath: eEntry.safePath,
|
|
432
|
+
extension: eEntry.extension,
|
|
433
|
+
sizeBytes: eEntry.data.byteLength,
|
|
434
|
+
contentType: ct
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
setStatus("Registrazione file sul server\u2026");
|
|
438
|
+
const cadProjectMetadata = collectCadMetadataFromZipEntries(
|
|
439
|
+
entries.map((e) => ({ safePath: e.safePath, data: e.data }))
|
|
440
|
+
);
|
|
441
|
+
await upload.registerClientExtractedZipFiles({
|
|
442
|
+
originalZipFileName: file.name,
|
|
443
|
+
zipWarnings: extracted.warnings,
|
|
444
|
+
...cadProjectMetadata ? { cadProjectMetadata } : {},
|
|
445
|
+
files: filesPayload
|
|
446
|
+
});
|
|
447
|
+
setStatus(
|
|
448
|
+
"ZIP processato nel browser e registrato: parsing avviato sul server."
|
|
449
|
+
);
|
|
450
|
+
} catch (e) {
|
|
451
|
+
setErr(e instanceof Error ? e.message : "Errore durante l'elaborazione ZIP.");
|
|
452
|
+
setStatus(null);
|
|
453
|
+
} finally {
|
|
454
|
+
setBusy(false);
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
const uploadSingleOrServerSmallZip = async (file) => {
|
|
458
|
+
setErr(null);
|
|
459
|
+
setBusy(true);
|
|
460
|
+
setStatus(`Caricamento su storage\u2026 (${file.name})`);
|
|
461
|
+
try {
|
|
462
|
+
const postUrl = await upload.generatePackageUploadUrl();
|
|
463
|
+
const headers = file.type ? { "Content-Type": file.type } : {};
|
|
464
|
+
const uploadRes = await fetch(postUrl, {
|
|
465
|
+
method: "POST",
|
|
466
|
+
headers,
|
|
467
|
+
body: file
|
|
468
|
+
});
|
|
469
|
+
if (!uploadRes.ok) {
|
|
470
|
+
throw new Error(
|
|
471
|
+
`Upload verso storage fallito (HTTP ${uploadRes.status}).`
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
const body = await uploadRes.json();
|
|
475
|
+
if (!body.storageId) {
|
|
476
|
+
throw new Error("Risposta storage senza storageId.");
|
|
477
|
+
}
|
|
478
|
+
setStatus("Registrazione pacchetto lato server\u2026");
|
|
479
|
+
const result = await upload.registerUploadedPackage({
|
|
480
|
+
storageId: body.storageId,
|
|
481
|
+
originalFileName: file.name,
|
|
482
|
+
declaredContentType: file.type || void 0
|
|
483
|
+
});
|
|
484
|
+
if (result.kind === "zip") {
|
|
485
|
+
setStatus(
|
|
486
|
+
"ZIP piccolo: estrazione sul backend in corso (aggiornamento automatico)."
|
|
487
|
+
);
|
|
488
|
+
} else if (result.kind === "zip_too_large") {
|
|
489
|
+
setErr(
|
|
490
|
+
"Questo ZIP supera il limite dell\u2019estrazione server: usa sempre la modalit\xE0 browser (trascina di nuovo il file)."
|
|
491
|
+
);
|
|
492
|
+
setStatus(null);
|
|
493
|
+
} else {
|
|
494
|
+
setStatus("File singolo registrato: parsing avviato sul server.");
|
|
495
|
+
}
|
|
496
|
+
} catch (e) {
|
|
497
|
+
setErr(e instanceof Error ? e.message : "Errore durante l'upload.");
|
|
498
|
+
setStatus(null);
|
|
499
|
+
} finally {
|
|
500
|
+
setBusy(false);
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
const uploadFile = async (file) => {
|
|
504
|
+
const lower = file.name.toLowerCase();
|
|
505
|
+
const isZip = lower.endsWith(".zip") || file.type !== "" && file.type.toLowerCase().includes("zip");
|
|
506
|
+
if (isZip) {
|
|
507
|
+
await uploadZipViaBrowser(file);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
await uploadSingleOrServerSmallZip(file);
|
|
511
|
+
};
|
|
512
|
+
const onInputChange = (e) => {
|
|
513
|
+
const file = e.target.files?.[0];
|
|
514
|
+
e.target.value = "";
|
|
515
|
+
if (file) void uploadFile(file);
|
|
516
|
+
};
|
|
517
|
+
const onDrop = (e) => {
|
|
518
|
+
e.preventDefault();
|
|
519
|
+
setDragOver(false);
|
|
520
|
+
const file = e.dataTransfer.files?.[0];
|
|
521
|
+
if (file) void uploadFile(file);
|
|
522
|
+
};
|
|
523
|
+
return /* @__PURE__ */ jsxs4("div", { children: [
|
|
524
|
+
/* @__PURE__ */ jsx4("h3", { style: { marginTop: 0 }, children: "Upload reale (Convex storage)" }),
|
|
525
|
+
/* @__PURE__ */ jsxs4("p", { className: "muted", style: { marginTop: 0 }, children: [
|
|
526
|
+
"I file ",
|
|
527
|
+
/* @__PURE__ */ jsx4("strong", { children: ".zip" }),
|
|
528
|
+
" sono estratti nel ",
|
|
529
|
+
/* @__PURE__ */ jsx4("strong", { children: "browser" }),
|
|
530
|
+
" (RAM del tuo PC), poi ogni file operativo viene caricato su Convex: cos\xEC non si applica il limite ~64 MB delle action sullo ZIP completo. File singoli (.stl, .ply, \u2026) continuano a passare dall\u2019upload classico."
|
|
531
|
+
] }),
|
|
532
|
+
err ? /* @__PURE__ */ jsx4("div", { className: "err-banner", children: err }) : null,
|
|
533
|
+
status ? /* @__PURE__ */ jsx4("div", { className: "panel", style: { marginBottom: "0.75rem", padding: "0.65rem 1rem" }, children: status }) : null,
|
|
534
|
+
/* @__PURE__ */ jsxs4(
|
|
535
|
+
"div",
|
|
536
|
+
{
|
|
537
|
+
className: `upload-dropzone ${dragOver ? "upload-dropzone-active" : ""}`,
|
|
538
|
+
onDragOver: (e) => {
|
|
539
|
+
e.preventDefault();
|
|
540
|
+
setDragOver(true);
|
|
541
|
+
},
|
|
542
|
+
onDragLeave: () => setDragOver(false),
|
|
543
|
+
onDrop,
|
|
544
|
+
children: [
|
|
545
|
+
/* @__PURE__ */ jsx4("p", { style: { margin: "0 0 0.5rem" }, children: "Trascina qui un file oppure scegli dal disco." }),
|
|
546
|
+
/* @__PURE__ */ jsx4(
|
|
547
|
+
"input",
|
|
548
|
+
{
|
|
549
|
+
type: "file",
|
|
550
|
+
disabled: busy,
|
|
551
|
+
accept: ".zip,.stl,.ply,.obj,.dcm,.xml,.json,.png,.jpg,.jpeg,.pdf,.txt,application/zip",
|
|
552
|
+
onChange: onInputChange
|
|
553
|
+
}
|
|
554
|
+
)
|
|
555
|
+
]
|
|
556
|
+
}
|
|
557
|
+
),
|
|
558
|
+
/* @__PURE__ */ jsx4("p", { className: "muted", style: { fontSize: "0.85rem", marginBottom: 0 }, children: "ZIP \u2192 tipo rilevato dal nome / content-type; estrazione locale + pi\xF9 POST verso l\u2019host. I file singoli usano l\u2019upload classico lato server." })
|
|
559
|
+
] });
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// src/components/ReviewPanel.tsx
|
|
563
|
+
import { useState as useState3 } from "react";
|
|
564
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
565
|
+
var roles = [
|
|
566
|
+
"upper_arch",
|
|
567
|
+
"lower_arch",
|
|
568
|
+
"bite",
|
|
569
|
+
"scanbody",
|
|
570
|
+
"abutment",
|
|
571
|
+
"model",
|
|
572
|
+
"impression",
|
|
573
|
+
"dicom_volume",
|
|
574
|
+
"project_file",
|
|
575
|
+
"preview",
|
|
576
|
+
"document",
|
|
577
|
+
"unknown"
|
|
578
|
+
];
|
|
579
|
+
function ReviewPanel({
|
|
580
|
+
acquisitionId,
|
|
581
|
+
files,
|
|
582
|
+
review,
|
|
583
|
+
onPreviewMesh
|
|
584
|
+
}) {
|
|
585
|
+
const [map2, setMap] = useState3({});
|
|
586
|
+
const [busy, setBusy] = useState3(false);
|
|
587
|
+
const [err, setErr] = useState3(null);
|
|
588
|
+
const onRole = (id, role) => {
|
|
589
|
+
setMap((m) => ({ ...m, [id]: role }));
|
|
590
|
+
};
|
|
591
|
+
const submit = async (rejected) => {
|
|
592
|
+
if (!files?.length) return;
|
|
593
|
+
setErr(null);
|
|
594
|
+
setBusy(true);
|
|
595
|
+
try {
|
|
596
|
+
await review.saveReview({
|
|
597
|
+
acquisitionId,
|
|
598
|
+
updates: files.map((f) => ({
|
|
599
|
+
fileId: f._id,
|
|
600
|
+
role: map2[f._id] ?? f.role
|
|
601
|
+
})),
|
|
602
|
+
markRejected: rejected
|
|
603
|
+
});
|
|
604
|
+
} catch (e) {
|
|
605
|
+
setErr(e instanceof Error ? e.message : "Errore review");
|
|
606
|
+
} finally {
|
|
607
|
+
setBusy(false);
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
if (!files?.length) return null;
|
|
611
|
+
return /* @__PURE__ */ jsxs5("div", { children: [
|
|
612
|
+
/* @__PURE__ */ jsx5("h3", { style: { marginTop: 0 }, children: "Review manuale" }),
|
|
613
|
+
/* @__PURE__ */ jsx5("p", { className: "muted", children: "Correggi il ruolo dei file: le modifiche sono applicate solo tramite mutation server-side." }),
|
|
614
|
+
err ? /* @__PURE__ */ jsx5("div", { className: "err-banner", children: err }) : null,
|
|
615
|
+
/* @__PURE__ */ jsx5("div", { className: "btn-row", style: { marginBottom: "0.75rem" }, children: /* @__PURE__ */ jsx5(
|
|
616
|
+
"button",
|
|
617
|
+
{
|
|
618
|
+
type: "button",
|
|
619
|
+
className: "btn btn-ghost",
|
|
620
|
+
disabled: busy,
|
|
621
|
+
onClick: () => void review.beginManualReview({ acquisitionId }),
|
|
622
|
+
children: 'Segna "in review"'
|
|
623
|
+
}
|
|
624
|
+
) }),
|
|
625
|
+
files.map((f) => /* @__PURE__ */ jsxs5("div", { className: "field", style: { marginBottom: "0.65rem" }, children: [
|
|
626
|
+
/* @__PURE__ */ jsxs5(
|
|
627
|
+
"div",
|
|
628
|
+
{
|
|
629
|
+
className: "btn-row",
|
|
630
|
+
style: {
|
|
631
|
+
justifyContent: "space-between",
|
|
632
|
+
alignItems: "center",
|
|
633
|
+
marginBottom: "0.35rem"
|
|
634
|
+
},
|
|
635
|
+
children: [
|
|
636
|
+
/* @__PURE__ */ jsx5("label", { style: { margin: 0 }, children: f.originalName }),
|
|
637
|
+
onPreviewMesh && isMeshViewerFormat(f.format) && f.storageId ? /* @__PURE__ */ jsx5(
|
|
638
|
+
"button",
|
|
639
|
+
{
|
|
640
|
+
type: "button",
|
|
641
|
+
className: "btn btn-ghost",
|
|
642
|
+
style: { fontSize: "0.75rem", padding: "0.25rem 0.55rem" },
|
|
643
|
+
onClick: () => onPreviewMesh(f._id),
|
|
644
|
+
children: "Visualizza 3D"
|
|
645
|
+
}
|
|
646
|
+
) : null
|
|
647
|
+
]
|
|
648
|
+
}
|
|
649
|
+
),
|
|
650
|
+
/* @__PURE__ */ jsx5(
|
|
651
|
+
"select",
|
|
652
|
+
{
|
|
653
|
+
value: map2[f._id] ?? f.role,
|
|
654
|
+
onChange: (e) => onRole(f._id, e.target.value),
|
|
655
|
+
children: roles.map((r) => /* @__PURE__ */ jsx5("option", { value: r, children: r }, r))
|
|
656
|
+
}
|
|
657
|
+
)
|
|
658
|
+
] }, f._id)),
|
|
659
|
+
/* @__PURE__ */ jsxs5("div", { className: "btn-row", style: { marginTop: "1rem" }, children: [
|
|
660
|
+
/* @__PURE__ */ jsx5(
|
|
661
|
+
"button",
|
|
662
|
+
{
|
|
663
|
+
type: "button",
|
|
664
|
+
className: "btn btn-primary",
|
|
665
|
+
disabled: busy,
|
|
666
|
+
onClick: () => void submit(false),
|
|
667
|
+
children: "Salva review e classifica"
|
|
668
|
+
}
|
|
669
|
+
),
|
|
670
|
+
/* @__PURE__ */ jsx5(
|
|
671
|
+
"button",
|
|
672
|
+
{
|
|
673
|
+
type: "button",
|
|
674
|
+
className: "btn btn-danger",
|
|
675
|
+
disabled: busy,
|
|
676
|
+
onClick: () => void submit(true),
|
|
677
|
+
children: "Respinti (solo review status)"
|
|
678
|
+
}
|
|
679
|
+
)
|
|
680
|
+
] })
|
|
681
|
+
] });
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// src/components/ManifestViewer.tsx
|
|
685
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
686
|
+
function ManifestViewer({ manifest }) {
|
|
687
|
+
if (manifest == null) {
|
|
688
|
+
return /* @__PURE__ */ jsx6("p", { className: "muted", children: "Manifest non ancora disponibile." });
|
|
689
|
+
}
|
|
690
|
+
return /* @__PURE__ */ jsx6("pre", { className: "manifest-pre", children: JSON.stringify(manifest, null, 2) });
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/components/CadProjectMetadataPanel.tsx
|
|
694
|
+
import { Fragment, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
695
|
+
var CONFIDENCE_LABEL = {
|
|
696
|
+
high: "Alta",
|
|
697
|
+
medium: "Media",
|
|
698
|
+
low: "Bassa",
|
|
699
|
+
inferred: "Dedotta"
|
|
700
|
+
};
|
|
701
|
+
function ConfidenceBadge({
|
|
702
|
+
confidence
|
|
703
|
+
}) {
|
|
704
|
+
return /* @__PURE__ */ jsx7("span", { className: `confidence-badge confidence-${confidence}`, children: CONFIDENCE_LABEL[confidence] });
|
|
705
|
+
}
|
|
706
|
+
var TS_COMPONENT_LABEL = {
|
|
707
|
+
analog: "Analogo",
|
|
708
|
+
analog_socket: "Socket analogo",
|
|
709
|
+
scanbody: "Scan body",
|
|
710
|
+
"link/tibase_candidate": "Link / Ti-Base (candidato)",
|
|
711
|
+
screw_or_transmucosal_component_candidate: "Vite / transmucoso (candidato)",
|
|
712
|
+
manufacturer_component_candidate: "Componente produttore (candidato)",
|
|
713
|
+
unknown: "Non classificato"
|
|
714
|
+
};
|
|
715
|
+
function ImplantComponentCard({ comp }) {
|
|
716
|
+
const geom = comp.parsedGeometry;
|
|
717
|
+
return /* @__PURE__ */ jsxs6("div", { className: "implant-card", children: [
|
|
718
|
+
/* @__PURE__ */ jsxs6("div", { className: "implant-card-head", children: [
|
|
719
|
+
/* @__PURE__ */ jsx7("strong", { children: comp.manufacturer ?? "Produttore n/d" }),
|
|
720
|
+
/* @__PURE__ */ jsx7(ConfidenceBadge, { confidence: comp.confidence })
|
|
721
|
+
] }),
|
|
722
|
+
/* @__PURE__ */ jsxs6("dl", { className: "implant-card-grid", children: [
|
|
723
|
+
comp.tooth ? /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
724
|
+
/* @__PURE__ */ jsx7("dt", { children: "Dente" }),
|
|
725
|
+
/* @__PURE__ */ jsx7("dd", { children: comp.tooth })
|
|
726
|
+
] }) : null,
|
|
727
|
+
comp.platform ? /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
728
|
+
/* @__PURE__ */ jsx7("dt", { children: "Piattaforma" }),
|
|
729
|
+
/* @__PURE__ */ jsx7("dd", { children: comp.platform })
|
|
730
|
+
] }) : null,
|
|
731
|
+
comp.connection ? /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
732
|
+
/* @__PURE__ */ jsx7("dt", { children: "Connessione" }),
|
|
733
|
+
/* @__PURE__ */ jsx7("dd", { children: comp.connection })
|
|
734
|
+
] }) : null,
|
|
735
|
+
comp.component ? /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
736
|
+
/* @__PURE__ */ jsx7("dt", { children: "Componente" }),
|
|
737
|
+
/* @__PURE__ */ jsx7("dd", { children: comp.component })
|
|
738
|
+
] }) : null,
|
|
739
|
+
comp.libraryName ? /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
740
|
+
/* @__PURE__ */ jsx7("dt", { children: "Libreria Exocad" }),
|
|
741
|
+
/* @__PURE__ */ jsx7("dd", { children: comp.libraryName })
|
|
742
|
+
] }) : null,
|
|
743
|
+
comp.geometryFile ? /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
744
|
+
/* @__PURE__ */ jsx7("dt", { children: "File geometria" }),
|
|
745
|
+
/* @__PURE__ */ jsx7("dd", { children: /* @__PURE__ */ jsx7("code", { children: comp.geometryFile }) })
|
|
746
|
+
] }) : null,
|
|
747
|
+
comp.libraryPath ? /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
748
|
+
/* @__PURE__ */ jsx7("dt", { children: "Path libreria" }),
|
|
749
|
+
/* @__PURE__ */ jsx7("dd", { children: /* @__PURE__ */ jsx7("code", { className: "implant-path", children: comp.libraryPath }) })
|
|
750
|
+
] }) : null
|
|
751
|
+
] }),
|
|
752
|
+
geom ? /* @__PURE__ */ jsxs6("div", { className: "implant-geom", children: [
|
|
753
|
+
/* @__PURE__ */ jsx7("span", { className: "implant-geom-title", children: "Interpretazione codice (dedotta, da confermare)" }),
|
|
754
|
+
/* @__PURE__ */ jsxs6("ul", { className: "implant-geom-list", children: [
|
|
755
|
+
geom.baseType ? /* @__PURE__ */ jsxs6("li", { children: [
|
|
756
|
+
"Tipo base: ",
|
|
757
|
+
/* @__PURE__ */ jsx7("strong", { children: geom.baseType.value }),
|
|
758
|
+
" ",
|
|
759
|
+
/* @__PURE__ */ jsx7(ConfidenceBadge, { confidence: geom.baseType.confidence })
|
|
760
|
+
] }) : null,
|
|
761
|
+
geom.diameterOrPlatform ? /* @__PURE__ */ jsxs6("li", { children: [
|
|
762
|
+
"Diametro/piattaforma:",
|
|
763
|
+
" ",
|
|
764
|
+
/* @__PURE__ */ jsx7("strong", { children: geom.diameterOrPlatform.value }),
|
|
765
|
+
" ",
|
|
766
|
+
/* @__PURE__ */ jsx7(
|
|
767
|
+
ConfidenceBadge,
|
|
768
|
+
{
|
|
769
|
+
confidence: geom.diameterOrPlatform.confidence
|
|
770
|
+
}
|
|
771
|
+
)
|
|
772
|
+
] }) : null,
|
|
773
|
+
geom.abutmentHeight ? /* @__PURE__ */ jsxs6("li", { children: [
|
|
774
|
+
"Altezza abutment: ",
|
|
775
|
+
/* @__PURE__ */ jsx7("strong", { children: geom.abutmentHeight.value }),
|
|
776
|
+
" ",
|
|
777
|
+
/* @__PURE__ */ jsx7(ConfidenceBadge, { confidence: geom.abutmentHeight.confidence })
|
|
778
|
+
] }) : null
|
|
779
|
+
] })
|
|
780
|
+
] }) : null,
|
|
781
|
+
/* @__PURE__ */ jsxs6("p", { className: "implant-card-source muted", children: [
|
|
782
|
+
"Fonte: ",
|
|
783
|
+
comp.sourceFiles.join(", ")
|
|
784
|
+
] })
|
|
785
|
+
] });
|
|
786
|
+
}
|
|
787
|
+
function ExocadComponentsSection({ exocad }) {
|
|
788
|
+
const { implantComponents, case: caseInfo, restoration, model } = exocad;
|
|
789
|
+
return /* @__PURE__ */ jsxs6("div", { className: "exocad-section", children: [
|
|
790
|
+
/* @__PURE__ */ jsxs6("div", { className: "exocad-case", children: [
|
|
791
|
+
caseInfo.patientName ? /* @__PURE__ */ jsxs6("span", { children: [
|
|
792
|
+
/* @__PURE__ */ jsx7("strong", { children: "Paziente:" }),
|
|
793
|
+
" ",
|
|
794
|
+
caseInfo.patientName
|
|
795
|
+
] }) : null,
|
|
796
|
+
caseInfo.practiceName ? /* @__PURE__ */ jsxs6("span", { children: [
|
|
797
|
+
/* @__PURE__ */ jsx7("strong", { children: "Clinica:" }),
|
|
798
|
+
" ",
|
|
799
|
+
caseInfo.practiceName,
|
|
800
|
+
caseInfo.practiceId ? ` (#${caseInfo.practiceId})` : ""
|
|
801
|
+
] }) : null,
|
|
802
|
+
restoration.span ? /* @__PURE__ */ jsxs6("span", { children: [
|
|
803
|
+
/* @__PURE__ */ jsxs6("strong", { children: [
|
|
804
|
+
restoration.isBridge ? "Ponte" : "Span",
|
|
805
|
+
":"
|
|
806
|
+
] }),
|
|
807
|
+
" ",
|
|
808
|
+
restoration.span
|
|
809
|
+
] }) : null,
|
|
810
|
+
caseInfo.exocadVersion ? /* @__PURE__ */ jsx7("span", { className: "muted", children: caseInfo.exocadVersion }) : null
|
|
811
|
+
] }),
|
|
812
|
+
/* @__PURE__ */ jsxs6("div", { className: "implant-box", children: [
|
|
813
|
+
/* @__PURE__ */ jsxs6("div", { className: "implant-box-head", children: [
|
|
814
|
+
/* @__PURE__ */ jsx7("strong", { children: "Componentistica rilevata" }),
|
|
815
|
+
/* @__PURE__ */ jsx7(
|
|
816
|
+
"span",
|
|
817
|
+
{
|
|
818
|
+
className: `implant-flag ${exocad.isImplantCase ? "is-implant" : "no-implant"}`,
|
|
819
|
+
children: exocad.isImplantCase ? "Caso implantare" : "Nessun impianto rilevato"
|
|
820
|
+
}
|
|
821
|
+
)
|
|
822
|
+
] }),
|
|
823
|
+
implantComponents.length === 0 ? /* @__PURE__ */ jsxs6("p", { className: "muted", style: { margin: 0 }, children: [
|
|
824
|
+
"Nessuna componentistica implantare trovata nei file",
|
|
825
|
+
/* @__PURE__ */ jsx7("code", { children: " .constructionInfo" }),
|
|
826
|
+
" / ",
|
|
827
|
+
/* @__PURE__ */ jsx7("code", { children: ".modelInfo" }),
|
|
828
|
+
". Se il caso \xE8 su impianto, controlla che lo ZIP contenga questi file."
|
|
829
|
+
] }) : /* @__PURE__ */ jsx7("div", { className: "implant-card-list", children: implantComponents.map((comp, i) => /* @__PURE__ */ jsx7(
|
|
830
|
+
ImplantComponentCard,
|
|
831
|
+
{
|
|
832
|
+
comp
|
|
833
|
+
},
|
|
834
|
+
`${comp.geometryCode ?? comp.component ?? "c"}-${i}`
|
|
835
|
+
)) }),
|
|
836
|
+
model.analogLibrary || model.modelWorkflow ? /* @__PURE__ */ jsxs6("p", { className: "muted implant-model", children: [
|
|
837
|
+
model.analogLibrary ? /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
838
|
+
"Libreria analoghi: ",
|
|
839
|
+
/* @__PURE__ */ jsx7("strong", { children: model.analogLibrary }),
|
|
840
|
+
".",
|
|
841
|
+
" "
|
|
842
|
+
] }) : null,
|
|
843
|
+
model.modelWorkflow ? /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
844
|
+
"Workflow modello: ",
|
|
845
|
+
/* @__PURE__ */ jsx7("strong", { children: model.modelWorkflow }),
|
|
846
|
+
"."
|
|
847
|
+
] }) : null
|
|
848
|
+
] }) : null,
|
|
849
|
+
exocad.warnings.length > 0 ? /* @__PURE__ */ jsx7("ul", { className: "implant-warnings", children: exocad.warnings.map((w) => /* @__PURE__ */ jsx7("li", { children: w }, w)) }) : null
|
|
850
|
+
] })
|
|
851
|
+
] });
|
|
852
|
+
}
|
|
853
|
+
function zipPathForSourceFile(sourceFile, files) {
|
|
854
|
+
const base = sourceFile.replace(/\\/g, "/").split("/").pop() ?? sourceFile;
|
|
855
|
+
const match = files.find(
|
|
856
|
+
(f) => f.originalName === sourceFile || f.originalName.endsWith(`/${base}`) || f.originalName.split("/").pop() === base
|
|
857
|
+
);
|
|
858
|
+
return match?.originalName;
|
|
859
|
+
}
|
|
860
|
+
function CadSourceFileBlock({
|
|
861
|
+
entry,
|
|
862
|
+
zipPath,
|
|
863
|
+
defaultOpen
|
|
864
|
+
}) {
|
|
865
|
+
const hasDetails = entry.projectName || entry.projectDirectory || entry.toothNumbers.length > 0 || entry.materials.length > 0 || entry.shades.length > 0 || entry.reconstructions.length > 0;
|
|
866
|
+
return /* @__PURE__ */ jsxs6("details", { className: "cad-source-file", open: defaultOpen, children: [
|
|
867
|
+
/* @__PURE__ */ jsxs6("summary", { children: [
|
|
868
|
+
/* @__PURE__ */ jsx7("code", { children: entry.sourceFile }),
|
|
869
|
+
entry.source === "exocad" ? /* @__PURE__ */ jsx7("span", { className: "cad-source-file-vendor", children: " \xB7 Exocad" }) : null
|
|
870
|
+
] }),
|
|
871
|
+
zipPath && zipPath !== entry.sourceFile ? /* @__PURE__ */ jsxs6("p", { className: "cad-source-file-path", children: [
|
|
872
|
+
"Nel pacchetto ZIP: ",
|
|
873
|
+
/* @__PURE__ */ jsx7("code", { children: zipPath })
|
|
874
|
+
] }) : null,
|
|
875
|
+
!hasDetails ? /* @__PURE__ */ jsx7("p", { className: "muted", style: { margin: "0.5rem 0 0" }, children: "Nessun campo strutturato estratto da questo file (formato XML non riconosciuto o file vuoto)." }) : /* @__PURE__ */ jsxs6("div", { className: "cad-source-file-body", children: [
|
|
876
|
+
entry.projectName ? /* @__PURE__ */ jsxs6("p", { children: [
|
|
877
|
+
/* @__PURE__ */ jsx7("strong", { children: "Progetto:" }),
|
|
878
|
+
" ",
|
|
879
|
+
entry.projectName
|
|
880
|
+
] }) : null,
|
|
881
|
+
entry.projectDirectory ? /* @__PURE__ */ jsxs6("p", { children: [
|
|
882
|
+
/* @__PURE__ */ jsx7("strong", { children: "Directory:" }),
|
|
883
|
+
" ",
|
|
884
|
+
entry.projectDirectory
|
|
885
|
+
] }) : null,
|
|
886
|
+
entry.toothNumbers.length > 0 ? /* @__PURE__ */ jsxs6("p", { children: [
|
|
887
|
+
/* @__PURE__ */ jsx7("strong", { children: "Denti (FDI):" }),
|
|
888
|
+
" ",
|
|
889
|
+
entry.toothNumbers.join(", ")
|
|
890
|
+
] }) : null,
|
|
891
|
+
entry.materials.length > 0 ? /* @__PURE__ */ jsxs6("p", { children: [
|
|
892
|
+
/* @__PURE__ */ jsx7("strong", { children: "Materiali:" }),
|
|
893
|
+
" ",
|
|
894
|
+
entry.materials.join(", ")
|
|
895
|
+
] }) : null,
|
|
896
|
+
entry.shades.length > 0 ? /* @__PURE__ */ jsxs6("p", { children: [
|
|
897
|
+
/* @__PURE__ */ jsx7("strong", { children: "Colori / shade:" }),
|
|
898
|
+
" ",
|
|
899
|
+
entry.shades.join(", ")
|
|
900
|
+
] }) : null,
|
|
901
|
+
entry.reconstructions.length > 0 ? /* @__PURE__ */ jsxs6("div", { children: [
|
|
902
|
+
/* @__PURE__ */ jsx7("span", { className: "cad-recon-label", children: "Elementi" }),
|
|
903
|
+
/* @__PURE__ */ jsx7("ul", { className: "cad-recon-list", children: entry.reconstructions.map((r, i) => /* @__PURE__ */ jsxs6("li", { children: [
|
|
904
|
+
r.toothNumbers.length ? `Denti ${r.toothNumbers.join(", ")}` : "Elemento",
|
|
905
|
+
r.reconstructionType ? ` \xB7 ${r.reconstructionType}` : null,
|
|
906
|
+
r.material ? ` \xB7 ${r.material}` : null,
|
|
907
|
+
r.shade ? ` \xB7 ${r.shade}` : null,
|
|
908
|
+
r.meshFileName ? ` \xB7 ${r.meshFileName}` : null
|
|
909
|
+
] }, `${entry.sourceFile}-${r.toothNumbers.join("-")}-${i}`)) })
|
|
910
|
+
] }) : null
|
|
911
|
+
] }),
|
|
912
|
+
entry.parseWarnings.length > 0 ? /* @__PURE__ */ jsx7("ul", { className: "cad-recon-list muted", style: { fontSize: "0.88rem" }, children: entry.parseWarnings.map((w) => /* @__PURE__ */ jsx7("li", { children: w }, w)) }) : null
|
|
913
|
+
] });
|
|
914
|
+
}
|
|
915
|
+
function ThreeShapeSection({ ts }) {
|
|
916
|
+
const { case: caseInfo, restoration, implant } = ts;
|
|
917
|
+
return /* @__PURE__ */ jsxs6("div", { className: "exocad-section", children: [
|
|
918
|
+
/* @__PURE__ */ jsxs6("div", { className: "exocad-case", children: [
|
|
919
|
+
caseInfo.patientName ? /* @__PURE__ */ jsxs6("span", { children: [
|
|
920
|
+
/* @__PURE__ */ jsx7("strong", { children: "Paziente:" }),
|
|
921
|
+
" ",
|
|
922
|
+
caseInfo.patientName
|
|
923
|
+
] }) : null,
|
|
924
|
+
caseInfo.orderId ? /* @__PURE__ */ jsxs6("span", { children: [
|
|
925
|
+
/* @__PURE__ */ jsx7("strong", { children: "Ordine:" }),
|
|
926
|
+
" ",
|
|
927
|
+
caseInfo.orderId
|
|
928
|
+
] }) : null,
|
|
929
|
+
caseInfo.customer ? /* @__PURE__ */ jsxs6("span", { children: [
|
|
930
|
+
/* @__PURE__ */ jsx7("strong", { children: "Cliente:" }),
|
|
931
|
+
" ",
|
|
932
|
+
caseInfo.customer
|
|
933
|
+
] }) : null,
|
|
934
|
+
restoration.teeth.length > 0 ? /* @__PURE__ */ jsxs6("span", { children: [
|
|
935
|
+
/* @__PURE__ */ jsx7("strong", { children: "Denti (FDI):" }),
|
|
936
|
+
" ",
|
|
937
|
+
restoration.teeth.join(", ")
|
|
938
|
+
] }) : null,
|
|
939
|
+
restoration.type ? /* @__PURE__ */ jsxs6("span", { children: [
|
|
940
|
+
/* @__PURE__ */ jsx7("strong", { children: "Lavorazione:" }),
|
|
941
|
+
" ",
|
|
942
|
+
restoration.type
|
|
943
|
+
] }) : null,
|
|
944
|
+
restoration.materialName ? /* @__PURE__ */ jsxs6("span", { children: [
|
|
945
|
+
/* @__PURE__ */ jsx7("strong", { children: "Materiale:" }),
|
|
946
|
+
" ",
|
|
947
|
+
restoration.materialName
|
|
948
|
+
] }) : null,
|
|
949
|
+
/* @__PURE__ */ jsx7("span", { className: "muted", children: caseInfo.software })
|
|
950
|
+
] }),
|
|
951
|
+
/* @__PURE__ */ jsxs6("div", { className: "implant-box", children: [
|
|
952
|
+
/* @__PURE__ */ jsxs6("div", { className: "implant-box-head", children: [
|
|
953
|
+
/* @__PURE__ */ jsx7("strong", { children: "Componentistica rilevata \xB7 3Shape" }),
|
|
954
|
+
/* @__PURE__ */ jsx7(
|
|
955
|
+
"span",
|
|
956
|
+
{
|
|
957
|
+
className: `implant-flag ${implant.isImplantCase ? "is-implant" : "no-implant"}`,
|
|
958
|
+
children: implant.isImplantCase ? "Caso implantare" : "Nessun impianto rilevato"
|
|
959
|
+
}
|
|
960
|
+
)
|
|
961
|
+
] }),
|
|
962
|
+
implant.isImplantCase ? /* @__PURE__ */ jsxs6("dl", { className: "implant-card-grid", style: { marginBottom: "0.6rem" }, children: [
|
|
963
|
+
implant.manufacturer ? /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
964
|
+
/* @__PURE__ */ jsx7("dt", { children: "Produttore" }),
|
|
965
|
+
/* @__PURE__ */ jsx7("dd", { children: implant.manufacturer })
|
|
966
|
+
] }) : null,
|
|
967
|
+
implant.connection ? /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
968
|
+
/* @__PURE__ */ jsx7("dt", { children: "Connessione" }),
|
|
969
|
+
/* @__PURE__ */ jsx7("dd", { children: implant.connection })
|
|
970
|
+
] }) : null,
|
|
971
|
+
implant.abutmentKitId ? /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
972
|
+
/* @__PURE__ */ jsx7("dt", { children: "Abutment kit" }),
|
|
973
|
+
/* @__PURE__ */ jsx7("dd", { children: /* @__PURE__ */ jsx7("code", { children: implant.abutmentKitId }) })
|
|
974
|
+
] }) : null,
|
|
975
|
+
implant.connectionId ? /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
976
|
+
/* @__PURE__ */ jsx7("dt", { children: "Connection ID" }),
|
|
977
|
+
/* @__PURE__ */ jsx7("dd", { children: /* @__PURE__ */ jsx7("code", { children: implant.connectionId }) })
|
|
978
|
+
] }) : null,
|
|
979
|
+
implant.implantSystemId ? /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
980
|
+
/* @__PURE__ */ jsx7("dt", { children: "Implant system ID" }),
|
|
981
|
+
/* @__PURE__ */ jsx7("dd", { children: /* @__PURE__ */ jsx7("code", { children: implant.implantSystemId }) })
|
|
982
|
+
] }) : null
|
|
983
|
+
] }) : null,
|
|
984
|
+
implant.components.length > 0 ? /* @__PURE__ */ jsxs6("div", { className: "ts-components", children: [
|
|
985
|
+
/* @__PURE__ */ jsx7("span", { className: "implant-geom-title", children: "Componenti trovati" }),
|
|
986
|
+
/* @__PURE__ */ jsx7("ul", { className: "ts-component-list", children: implant.components.map((c) => /* @__PURE__ */ jsxs6("li", { children: [
|
|
987
|
+
/* @__PURE__ */ jsx7("code", { children: c.fileName }),
|
|
988
|
+
/* @__PURE__ */ jsx7("span", { className: "ts-component-type", children: TS_COMPONENT_LABEL[c.type] }),
|
|
989
|
+
/* @__PURE__ */ jsx7(ConfidenceBadge, { confidence: c.confidence })
|
|
990
|
+
] }, c.sourcePath)) })
|
|
991
|
+
] }) : implant.isImplantCase ? /* @__PURE__ */ jsxs6("p", { className: "muted", style: { margin: 0 }, children: [
|
|
992
|
+
"Caso implantare ma nessun componente trovato in",
|
|
993
|
+
/* @__PURE__ */ jsx7("code", { children: " External models/" }),
|
|
994
|
+
"."
|
|
995
|
+
] }) : null,
|
|
996
|
+
ts.warnings.length > 0 ? /* @__PURE__ */ jsx7("ul", { className: "implant-warnings", children: ts.warnings.map((w) => /* @__PURE__ */ jsx7("li", { children: w }, w)) }) : null
|
|
997
|
+
] })
|
|
998
|
+
] });
|
|
999
|
+
}
|
|
1000
|
+
function CadProjectMetadataPanel({
|
|
1001
|
+
cadProject,
|
|
1002
|
+
manifestFiles = []
|
|
1003
|
+
}) {
|
|
1004
|
+
const perFile = cadProject.bySourceFile ?? [];
|
|
1005
|
+
const hasPerFileBreakdown = perFile.length > 0;
|
|
1006
|
+
return /* @__PURE__ */ jsxs6("div", { className: "panel", style: { marginBottom: "1rem" }, children: [
|
|
1007
|
+
/* @__PURE__ */ jsx7("h3", { style: { marginTop: 0 }, children: "Metadati progetto CAD" }),
|
|
1008
|
+
/* @__PURE__ */ jsxs6("div", { className: "cad-project-summary", children: [
|
|
1009
|
+
/* @__PURE__ */ jsxs6("p", { className: "muted", style: { marginTop: 0 }, children: [
|
|
1010
|
+
"Riepilogo da",
|
|
1011
|
+
" ",
|
|
1012
|
+
/* @__PURE__ */ jsx7("code", { children: cadProject.sourceFiles.join(", ") }),
|
|
1013
|
+
cadProject.source === "exocad" ? " \xB7 Exocad" : null
|
|
1014
|
+
] }),
|
|
1015
|
+
cadProject.projectName ? /* @__PURE__ */ jsxs6("p", { style: { margin: "0.35rem 0" }, children: [
|
|
1016
|
+
/* @__PURE__ */ jsx7("strong", { children: "Progetto:" }),
|
|
1017
|
+
" ",
|
|
1018
|
+
cadProject.projectName
|
|
1019
|
+
] }) : null,
|
|
1020
|
+
cadProject.toothNumbers.length > 0 ? /* @__PURE__ */ jsxs6("p", { style: { margin: "0.35rem 0" }, children: [
|
|
1021
|
+
/* @__PURE__ */ jsx7("strong", { children: "Denti (FDI):" }),
|
|
1022
|
+
" ",
|
|
1023
|
+
cadProject.toothNumbers.join(", ")
|
|
1024
|
+
] }) : null,
|
|
1025
|
+
cadProject.materials.length > 0 ? /* @__PURE__ */ jsxs6("p", { style: { margin: "0.35rem 0" }, children: [
|
|
1026
|
+
/* @__PURE__ */ jsx7("strong", { children: "Materiali:" }),
|
|
1027
|
+
" ",
|
|
1028
|
+
cadProject.materials.join(", ")
|
|
1029
|
+
] }) : null,
|
|
1030
|
+
cadProject.shades.length > 0 ? /* @__PURE__ */ jsxs6("p", { style: { margin: "0.35rem 0" }, children: [
|
|
1031
|
+
/* @__PURE__ */ jsx7("strong", { children: "Colori / shade:" }),
|
|
1032
|
+
" ",
|
|
1033
|
+
cadProject.shades.join(", ")
|
|
1034
|
+
] }) : null
|
|
1035
|
+
] }),
|
|
1036
|
+
cadProject.exocad ? /* @__PURE__ */ jsx7(ExocadComponentsSection, { exocad: cadProject.exocad }) : null,
|
|
1037
|
+
cadProject.threeShape ? /* @__PURE__ */ jsx7(ThreeShapeSection, { ts: cadProject.threeShape }) : null,
|
|
1038
|
+
/* @__PURE__ */ jsx7("strong", { className: "cad-source-files-title", children: "Dettaglio per file sorgente" }),
|
|
1039
|
+
!hasPerFileBreakdown ? /* @__PURE__ */ jsxs6("p", { className: "muted", style: { margin: 0, fontSize: "0.9em" }, children: [
|
|
1040
|
+
"File analizzati:",
|
|
1041
|
+
" ",
|
|
1042
|
+
/* @__PURE__ */ jsx7("code", { children: cadProject.sourceFiles.join(", ") }),
|
|
1043
|
+
". Ri-carica lo ZIP per vedere i campi estratti separatamente da ciascun file."
|
|
1044
|
+
] }) : perFile.map((entry, index) => /* @__PURE__ */ jsx7(
|
|
1045
|
+
CadSourceFileBlock,
|
|
1046
|
+
{
|
|
1047
|
+
entry,
|
|
1048
|
+
zipPath: zipPathForSourceFile(entry.sourceFile, manifestFiles),
|
|
1049
|
+
defaultOpen: index === 0
|
|
1050
|
+
},
|
|
1051
|
+
entry.sourceFile
|
|
1052
|
+
))
|
|
1053
|
+
] });
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// src/components/CaseSummaryCard.tsx
|
|
1057
|
+
import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1058
|
+
function uniq(values) {
|
|
1059
|
+
return [...new Set(values.filter((v) => Boolean(v)))];
|
|
1060
|
+
}
|
|
1061
|
+
function buildSummary(cad) {
|
|
1062
|
+
const ex = cad.exocad;
|
|
1063
|
+
const ts = cad.threeShape;
|
|
1064
|
+
const vendor = ts ? "3Shape" : ex || cad.source === "exocad" ? "Exocad" : "\u2014";
|
|
1065
|
+
const software = ts ? uniq([ts.case.software, ts.case.softwareVersion]).join(" ") || null : ex?.case.exocadVersion ?? null;
|
|
1066
|
+
const workTypes = ts?.restoration.type ? [ts.restoration.type] : ex ? uniq(ex.restoration.items.map((i) => i.type)) : uniq(cad.reconstructions.map((r) => r.reconstructionType));
|
|
1067
|
+
const teeth = ts && ts.restoration.teeth.length > 0 ? ts.restoration.teeth.map(String) : ex && ex.restoration.teeth.length > 0 ? ex.restoration.teeth : cad.toothNumbers.map(String);
|
|
1068
|
+
const materials = ts?.restoration.materialName ? [ts.restoration.materialName] : cad.materials;
|
|
1069
|
+
const shades = ts?.restoration.shade ? [ts.restoration.shade] : cad.shades;
|
|
1070
|
+
const isImplant = ts?.implant.isImplantCase ?? ex?.isImplantCase ?? false;
|
|
1071
|
+
const manufacturer = ts?.implant.manufacturer ?? ex?.implantComponents.find((c) => c.manufacturer)?.manufacturer ?? null;
|
|
1072
|
+
const connection = ts?.implant.connection ?? ex?.implantComponents.find((c) => c.connection)?.connection ?? null;
|
|
1073
|
+
const components = ts ? ts.implant.components.map((c) => c.fileName) : ex ? ex.implantComponents.map(
|
|
1074
|
+
(c) => c.component ?? c.geometryFile ?? c.manufacturer ?? "componente"
|
|
1075
|
+
) : [];
|
|
1076
|
+
return {
|
|
1077
|
+
patientName: ex?.case.patientName ?? ts?.case.patientName ?? null,
|
|
1078
|
+
software,
|
|
1079
|
+
scanSource: ts?.case.scanSource ?? null,
|
|
1080
|
+
workTypes,
|
|
1081
|
+
teeth,
|
|
1082
|
+
materials,
|
|
1083
|
+
shades,
|
|
1084
|
+
isImplant,
|
|
1085
|
+
manufacturer,
|
|
1086
|
+
connection,
|
|
1087
|
+
abutmentKit: ts?.implant.abutmentKitId ?? null,
|
|
1088
|
+
components,
|
|
1089
|
+
vendor
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
function Row({
|
|
1093
|
+
label,
|
|
1094
|
+
children
|
|
1095
|
+
}) {
|
|
1096
|
+
return /* @__PURE__ */ jsxs7("div", { className: "summary-row", children: [
|
|
1097
|
+
/* @__PURE__ */ jsx8("span", { className: "summary-label", children: label }),
|
|
1098
|
+
/* @__PURE__ */ jsx8("span", { className: "summary-value", children })
|
|
1099
|
+
] });
|
|
1100
|
+
}
|
|
1101
|
+
function CaseSummaryCard({
|
|
1102
|
+
cadProject,
|
|
1103
|
+
fallbackTitle
|
|
1104
|
+
}) {
|
|
1105
|
+
const s = buildSummary(cadProject);
|
|
1106
|
+
return /* @__PURE__ */ jsxs7("div", { className: "panel summary-card", style: { marginBottom: "1rem" }, children: [
|
|
1107
|
+
/* @__PURE__ */ jsxs7("div", { className: "summary-head", children: [
|
|
1108
|
+
/* @__PURE__ */ jsx8("h3", { style: { margin: 0 }, children: s.patientName ?? fallbackTitle ?? "Riepilogo caso" }),
|
|
1109
|
+
s.vendor !== "\u2014" ? /* @__PURE__ */ jsx8("span", { className: "vendor-pill vendor-detail", children: s.vendor }) : null
|
|
1110
|
+
] }),
|
|
1111
|
+
/* @__PURE__ */ jsxs7("div", { className: "summary-list", children: [
|
|
1112
|
+
/* @__PURE__ */ jsx8(Row, { label: "Lavorazione", children: s.workTypes.length > 0 ? s.workTypes.join(", ") : "\u2014" }),
|
|
1113
|
+
/* @__PURE__ */ jsxs7(Row, { label: "Software / scanner", children: [
|
|
1114
|
+
s.software ?? "\u2014",
|
|
1115
|
+
s.scanSource ? ` \xB7 ${s.scanSource}` : ""
|
|
1116
|
+
] }),
|
|
1117
|
+
/* @__PURE__ */ jsx8(Row, { label: "Denti (FDI)", children: s.teeth.length > 0 ? s.teeth.join(", ") : "\u2014" }),
|
|
1118
|
+
/* @__PURE__ */ jsx8(Row, { label: "Materiale", children: s.materials.length > 0 ? s.materials.join(", ") : "\u2014" }),
|
|
1119
|
+
/* @__PURE__ */ jsx8(Row, { label: "Shade / colore", children: s.shades.length > 0 ? s.shades.join(", ") : "\u2014" }),
|
|
1120
|
+
/* @__PURE__ */ jsx8(Row, { label: "Tipo caso", children: /* @__PURE__ */ jsx8(
|
|
1121
|
+
"span",
|
|
1122
|
+
{
|
|
1123
|
+
className: `case-flag ${s.isImplant ? "case-implant" : "case-noimplant"}`,
|
|
1124
|
+
children: s.isImplant ? "Implantare" : "Non implantare"
|
|
1125
|
+
}
|
|
1126
|
+
) }),
|
|
1127
|
+
s.isImplant ? /* @__PURE__ */ jsxs7(Fragment2, { children: [
|
|
1128
|
+
/* @__PURE__ */ jsx8(Row, { label: "Produttore", children: s.manufacturer ?? "Non identificato" }),
|
|
1129
|
+
/* @__PURE__ */ jsx8(Row, { label: "Connessione", children: s.connection ?? "\u2014" }),
|
|
1130
|
+
s.abutmentKit ? /* @__PURE__ */ jsx8(Row, { label: "Abutment kit", children: /* @__PURE__ */ jsx8("code", { children: s.abutmentKit }) }) : null,
|
|
1131
|
+
s.components.length > 0 ? /* @__PURE__ */ jsx8(Row, { label: "Componenti", children: s.components.join(", ") }) : null
|
|
1132
|
+
] }) : null
|
|
1133
|
+
] })
|
|
1134
|
+
] });
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// src/components/EventTimeline.tsx
|
|
1138
|
+
import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1139
|
+
function EventTimeline({
|
|
1140
|
+
events
|
|
1141
|
+
}) {
|
|
1142
|
+
if (!events?.length) {
|
|
1143
|
+
return /* @__PURE__ */ jsx9("p", { className: "muted", children: "Nessun evento registrato. Le azioni sul backend appariranno qui in ordine cronologico." });
|
|
1144
|
+
}
|
|
1145
|
+
return /* @__PURE__ */ jsx9("div", { className: "timeline", children: events.map((e) => /* @__PURE__ */ jsxs8("div", { className: "timeline-item", children: [
|
|
1146
|
+
/* @__PURE__ */ jsx9("div", { style: { fontSize: "0.72rem", color: "#8ea0bf" }, children: new Date(e.createdAt).toLocaleString("it-IT") }),
|
|
1147
|
+
/* @__PURE__ */ jsx9("div", { style: { fontWeight: 600, marginTop: "0.15rem" }, children: e.type }),
|
|
1148
|
+
/* @__PURE__ */ jsx9("div", { className: "muted", style: { marginTop: "0.2rem" }, children: e.message })
|
|
1149
|
+
] }, e._id)) });
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// src/components/MeshViewer.tsx
|
|
1153
|
+
import { useEffect, useRef, useState as useState4 } from "react";
|
|
1154
|
+
import { BufferAttribute as BufferAttribute2, Mesh as Mesh3 } from "three";
|
|
1155
|
+
|
|
1156
|
+
// src/lib/meshLoaders.ts
|
|
1157
|
+
import {
|
|
1158
|
+
Box3,
|
|
1159
|
+
BufferGeometry,
|
|
1160
|
+
Group,
|
|
1161
|
+
Mesh,
|
|
1162
|
+
MeshStandardMaterial,
|
|
1163
|
+
Vector3,
|
|
1164
|
+
DoubleSide
|
|
1165
|
+
} from "three";
|
|
1166
|
+
import { STLLoader } from "three/examples/jsm/loaders/STLLoader.js";
|
|
1167
|
+
import { PLYLoader } from "three/examples/jsm/loaders/PLYLoader.js";
|
|
1168
|
+
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader.js";
|
|
1169
|
+
function neutralMaterial() {
|
|
1170
|
+
return new MeshStandardMaterial({
|
|
1171
|
+
color: 12109020,
|
|
1172
|
+
metalness: 0.06,
|
|
1173
|
+
roughness: 0.42,
|
|
1174
|
+
side: DoubleSide,
|
|
1175
|
+
flatShading: false
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
function loadStlMesh(data) {
|
|
1179
|
+
const loader = new STLLoader();
|
|
1180
|
+
const geometry = loader.parse(data);
|
|
1181
|
+
geometry.computeVertexNormals();
|
|
1182
|
+
return new Mesh(geometry, neutralMaterial());
|
|
1183
|
+
}
|
|
1184
|
+
function loadPlyMesh(data) {
|
|
1185
|
+
const loader = new PLYLoader();
|
|
1186
|
+
const geometry = loader.parse(data);
|
|
1187
|
+
geometry.computeVertexNormals();
|
|
1188
|
+
return new Mesh(geometry, neutralMaterial());
|
|
1189
|
+
}
|
|
1190
|
+
function loadObjMesh(data) {
|
|
1191
|
+
const text = new TextDecoder("utf-8").decode(data);
|
|
1192
|
+
const loader = new OBJLoader();
|
|
1193
|
+
const group = loader.parse(text);
|
|
1194
|
+
group.traverse((child) => {
|
|
1195
|
+
if (child instanceof Mesh) {
|
|
1196
|
+
child.material = neutralMaterial();
|
|
1197
|
+
if (child.geometry instanceof BufferGeometry) {
|
|
1198
|
+
child.geometry.computeVertexNormals();
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
});
|
|
1202
|
+
return group;
|
|
1203
|
+
}
|
|
1204
|
+
function loadMeshByFormat(format, data) {
|
|
1205
|
+
const f = format.toLowerCase();
|
|
1206
|
+
if (f === "stl") return loadStlMesh(data);
|
|
1207
|
+
if (f === "ply") return loadPlyMesh(data);
|
|
1208
|
+
if (f === "obj") return loadObjMesh(data);
|
|
1209
|
+
throw new Error(`Formato mesh non supportato nel viewer: ${format}`);
|
|
1210
|
+
}
|
|
1211
|
+
function wrapLoadedMeshRoot(loaded) {
|
|
1212
|
+
const root = new Group();
|
|
1213
|
+
root.add(loaded);
|
|
1214
|
+
return root;
|
|
1215
|
+
}
|
|
1216
|
+
function normalizeMeshRoot(root, targetSize = 2) {
|
|
1217
|
+
root.updateMatrixWorld(true);
|
|
1218
|
+
const box = new Box3().setFromObject(root);
|
|
1219
|
+
const center = new Vector3();
|
|
1220
|
+
const size = new Vector3();
|
|
1221
|
+
box.getCenter(center);
|
|
1222
|
+
box.getSize(size);
|
|
1223
|
+
root.position.sub(center);
|
|
1224
|
+
const maxDim = Math.max(size.x, size.y, size.z, 1e-6);
|
|
1225
|
+
const s = targetSize / maxDim;
|
|
1226
|
+
root.scale.setScalar(s);
|
|
1227
|
+
root.updateMatrixWorld(true);
|
|
1228
|
+
return new Box3().setFromObject(root);
|
|
1229
|
+
}
|
|
1230
|
+
function disposeObject3DTree(root) {
|
|
1231
|
+
if (!root) return;
|
|
1232
|
+
root.traverse((obj) => {
|
|
1233
|
+
if (obj instanceof Mesh) {
|
|
1234
|
+
obj.geometry?.dispose();
|
|
1235
|
+
const m = obj.material;
|
|
1236
|
+
if (Array.isArray(m)) {
|
|
1237
|
+
for (const x of m) x.dispose();
|
|
1238
|
+
} else {
|
|
1239
|
+
m?.dispose();
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// src/dentalGeom/DentalGeomIntakeViewport.ts
|
|
1246
|
+
import * as THREE from "three";
|
|
1247
|
+
import { OrbitControls } from "three-stdlib";
|
|
1248
|
+
|
|
1249
|
+
// src/dentalGeom/constants.ts
|
|
1250
|
+
var CAMERA_CONFIG = {
|
|
1251
|
+
FRUSTUM_SIZE: 100,
|
|
1252
|
+
NEAR_PLANE: 0.1,
|
|
1253
|
+
FAR_PLANE: 1e3,
|
|
1254
|
+
DEFAULT_POSITION: { x: 20, y: 20, z: 20 },
|
|
1255
|
+
DEFAULT_UP: { x: 0, y: 0, z: -1 },
|
|
1256
|
+
DEFAULT_LOOK_AT: { x: 0, y: 0, z: -1 }
|
|
1257
|
+
};
|
|
1258
|
+
var ANIMATION_CONFIG = {
|
|
1259
|
+
DURATION_MS: 1e3,
|
|
1260
|
+
EASING_THRESHOLD: 0.5,
|
|
1261
|
+
EASING_POWER: 2
|
|
1262
|
+
};
|
|
1263
|
+
var CONTROLS_CONFIG = {
|
|
1264
|
+
ENABLE_DAMPING: true,
|
|
1265
|
+
DAMPING_FACTOR: 0.05,
|
|
1266
|
+
DEFAULT_TARGET: { x: 0, y: 0, z: 0 }
|
|
1267
|
+
};
|
|
1268
|
+
var LIGHTING_CONFIG = {
|
|
1269
|
+
AMBIENT: { COLOR: 16777215, INTENSITY: 0.4 },
|
|
1270
|
+
KEY_LIGHT: {
|
|
1271
|
+
COLOR: 16777215,
|
|
1272
|
+
INTENSITY: 1.2,
|
|
1273
|
+
POSITION: { x: 50, y: 100, z: 50 }
|
|
1274
|
+
},
|
|
1275
|
+
FILL_LIGHT: {
|
|
1276
|
+
COLOR: 16777215,
|
|
1277
|
+
INTENSITY: 0.6,
|
|
1278
|
+
POSITION: { x: -50, y: 50, z: -50 }
|
|
1279
|
+
},
|
|
1280
|
+
RIM_LIGHT: {
|
|
1281
|
+
COLOR: 16777215,
|
|
1282
|
+
INTENSITY: 0.7,
|
|
1283
|
+
POSITION: { x: 0, y: 50, z: -100 }
|
|
1284
|
+
},
|
|
1285
|
+
HEMISPHERE: {
|
|
1286
|
+
SKY_COLOR: 16777215,
|
|
1287
|
+
GROUND_COLOR: 4473924,
|
|
1288
|
+
INTENSITY: 0.5
|
|
1289
|
+
}
|
|
1290
|
+
};
|
|
1291
|
+
var SCENE_CONFIG = {
|
|
1292
|
+
BACKGROUND_COLOR: 4868730
|
|
1293
|
+
};
|
|
1294
|
+
var RENDERER_CONFIG = {
|
|
1295
|
+
ANTIALIAS: true,
|
|
1296
|
+
LOCAL_CLIPPING_ENABLED: true
|
|
1297
|
+
};
|
|
1298
|
+
var VIEW_DIRECTIONS = {
|
|
1299
|
+
TOP: { name: "top", up: { x: 0, y: 1, z: 0 } },
|
|
1300
|
+
BOTTOM: { name: "bottom", up: { x: 0, y: 1, z: 0 } },
|
|
1301
|
+
FRONT: { name: "front", up: { x: 0, y: 0, z: -1 } },
|
|
1302
|
+
BACK: { name: "back", up: { x: 0, y: 0, z: -1 } },
|
|
1303
|
+
LEFT: { name: "left", up: { x: 0, y: 0, z: -1 } },
|
|
1304
|
+
RIGHT: { name: "right", up: { x: 0, y: 0, z: -1 } }
|
|
1305
|
+
};
|
|
1306
|
+
|
|
1307
|
+
// src/dentalGeom/DentalGeomIntakeViewport.ts
|
|
1308
|
+
function setupLights(scene) {
|
|
1309
|
+
const L = LIGHTING_CONFIG;
|
|
1310
|
+
scene.add(new THREE.AmbientLight(L.AMBIENT.COLOR, L.AMBIENT.INTENSITY));
|
|
1311
|
+
const key = new THREE.DirectionalLight(L.KEY_LIGHT.COLOR, L.KEY_LIGHT.INTENSITY);
|
|
1312
|
+
key.position.set(
|
|
1313
|
+
L.KEY_LIGHT.POSITION.x,
|
|
1314
|
+
L.KEY_LIGHT.POSITION.y,
|
|
1315
|
+
L.KEY_LIGHT.POSITION.z
|
|
1316
|
+
);
|
|
1317
|
+
scene.add(key);
|
|
1318
|
+
const fill = new THREE.DirectionalLight(L.FILL_LIGHT.COLOR, L.FILL_LIGHT.INTENSITY);
|
|
1319
|
+
fill.position.set(
|
|
1320
|
+
L.FILL_LIGHT.POSITION.x,
|
|
1321
|
+
L.FILL_LIGHT.POSITION.y,
|
|
1322
|
+
L.FILL_LIGHT.POSITION.z
|
|
1323
|
+
);
|
|
1324
|
+
scene.add(fill);
|
|
1325
|
+
const rim = new THREE.DirectionalLight(L.RIM_LIGHT.COLOR, L.RIM_LIGHT.INTENSITY);
|
|
1326
|
+
rim.position.set(
|
|
1327
|
+
L.RIM_LIGHT.POSITION.x,
|
|
1328
|
+
L.RIM_LIGHT.POSITION.y,
|
|
1329
|
+
L.RIM_LIGHT.POSITION.z
|
|
1330
|
+
);
|
|
1331
|
+
scene.add(rim);
|
|
1332
|
+
scene.add(
|
|
1333
|
+
new THREE.HemisphereLight(
|
|
1334
|
+
L.HEMISPHERE.SKY_COLOR,
|
|
1335
|
+
L.HEMISPHERE.GROUND_COLOR,
|
|
1336
|
+
L.HEMISPHERE.INTENSITY
|
|
1337
|
+
)
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
var DentalGeomIntakeViewport = class {
|
|
1341
|
+
scene;
|
|
1342
|
+
camera;
|
|
1343
|
+
renderer;
|
|
1344
|
+
controls;
|
|
1345
|
+
container;
|
|
1346
|
+
rootObject = null;
|
|
1347
|
+
animationHandle = null;
|
|
1348
|
+
resizeObserver = null;
|
|
1349
|
+
tick = () => {
|
|
1350
|
+
this.animationHandle = requestAnimationFrame(this.tick);
|
|
1351
|
+
this.controls.update();
|
|
1352
|
+
this.renderer.render(this.scene, this.camera);
|
|
1353
|
+
};
|
|
1354
|
+
constructor(container) {
|
|
1355
|
+
this.container = container;
|
|
1356
|
+
this.scene = new THREE.Scene();
|
|
1357
|
+
this.scene.background = new THREE.Color(SCENE_CONFIG.BACKGROUND_COLOR);
|
|
1358
|
+
const w = Math.max(1, container.clientWidth);
|
|
1359
|
+
const h = Math.max(1, container.clientHeight);
|
|
1360
|
+
this.camera = new THREE.OrthographicCamera(
|
|
1361
|
+
-1,
|
|
1362
|
+
1,
|
|
1363
|
+
1,
|
|
1364
|
+
-1,
|
|
1365
|
+
CAMERA_CONFIG.NEAR_PLANE,
|
|
1366
|
+
CAMERA_CONFIG.FAR_PLANE
|
|
1367
|
+
);
|
|
1368
|
+
this.setDefaultOrthographicFrustum();
|
|
1369
|
+
this.camera.position.set(
|
|
1370
|
+
CAMERA_CONFIG.DEFAULT_POSITION.x,
|
|
1371
|
+
CAMERA_CONFIG.DEFAULT_POSITION.y,
|
|
1372
|
+
CAMERA_CONFIG.DEFAULT_POSITION.z
|
|
1373
|
+
);
|
|
1374
|
+
this.camera.lookAt(
|
|
1375
|
+
CAMERA_CONFIG.DEFAULT_LOOK_AT.x,
|
|
1376
|
+
CAMERA_CONFIG.DEFAULT_LOOK_AT.y,
|
|
1377
|
+
CAMERA_CONFIG.DEFAULT_LOOK_AT.z
|
|
1378
|
+
);
|
|
1379
|
+
this.renderer = new THREE.WebGLRenderer({
|
|
1380
|
+
antialias: RENDERER_CONFIG.ANTIALIAS
|
|
1381
|
+
});
|
|
1382
|
+
this.renderer.setPixelRatio(Math.min(2, typeof window !== "undefined" ? window.devicePixelRatio : 1));
|
|
1383
|
+
this.renderer.setSize(w, h);
|
|
1384
|
+
this.renderer.localClippingEnabled = RENDERER_CONFIG.LOCAL_CLIPPING_ENABLED;
|
|
1385
|
+
container.appendChild(this.renderer.domElement);
|
|
1386
|
+
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
|
1387
|
+
this.controls.enableDamping = CONTROLS_CONFIG.ENABLE_DAMPING;
|
|
1388
|
+
this.controls.dampingFactor = CONTROLS_CONFIG.DAMPING_FACTOR;
|
|
1389
|
+
this.controls.target.set(
|
|
1390
|
+
CONTROLS_CONFIG.DEFAULT_TARGET.x,
|
|
1391
|
+
CONTROLS_CONFIG.DEFAULT_TARGET.y,
|
|
1392
|
+
CONTROLS_CONFIG.DEFAULT_TARGET.z
|
|
1393
|
+
);
|
|
1394
|
+
this.controls.update();
|
|
1395
|
+
setupLights(this.scene);
|
|
1396
|
+
this.resizeObserver = new ResizeObserver(() => this.handleResize());
|
|
1397
|
+
this.resizeObserver.observe(container);
|
|
1398
|
+
this.tick();
|
|
1399
|
+
}
|
|
1400
|
+
handleResize() {
|
|
1401
|
+
const w = Math.max(1, this.container.clientWidth);
|
|
1402
|
+
const h = Math.max(1, this.container.clientHeight);
|
|
1403
|
+
this.renderer.setSize(w, h);
|
|
1404
|
+
if (this.rootObject) {
|
|
1405
|
+
this.fitOrthographicFrustumToObject();
|
|
1406
|
+
} else {
|
|
1407
|
+
this.setDefaultOrthographicFrustum();
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
/** Frustum fisso (compat DentalGeom) prima del primo modello caricato. */
|
|
1411
|
+
setDefaultOrthographicFrustum() {
|
|
1412
|
+
const w = Math.max(1, this.container.clientWidth);
|
|
1413
|
+
const h = Math.max(1, this.container.clientHeight);
|
|
1414
|
+
const aspect = w / h;
|
|
1415
|
+
const frustum = CAMERA_CONFIG.FRUSTUM_SIZE;
|
|
1416
|
+
this.camera.left = -frustum * aspect / 2;
|
|
1417
|
+
this.camera.right = frustum * aspect / 2;
|
|
1418
|
+
this.camera.top = frustum / 2;
|
|
1419
|
+
this.camera.bottom = -frustum / 2;
|
|
1420
|
+
this.camera.zoom = 1;
|
|
1421
|
+
this.camera.updateProjectionMatrix();
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* Adatta left/right/top/bottom all’ingombro reale della mesh.
|
|
1425
|
+
* Prima il frustum ~100 unità con modello normalizzato ~2 unità → modello minuscolo.
|
|
1426
|
+
*/
|
|
1427
|
+
fitOrthographicFrustumToObject() {
|
|
1428
|
+
if (!this.rootObject) return;
|
|
1429
|
+
const box = new THREE.Box3().setFromObject(this.rootObject);
|
|
1430
|
+
if (box.isEmpty()) return;
|
|
1431
|
+
const sphere = new THREE.Sphere();
|
|
1432
|
+
box.getBoundingSphere(sphere);
|
|
1433
|
+
const r = Math.max(sphere.radius, 1e-6) * 1.35;
|
|
1434
|
+
const w = Math.max(1, this.container.clientWidth);
|
|
1435
|
+
const h = Math.max(1, this.container.clientHeight);
|
|
1436
|
+
const viewAspect = w / h;
|
|
1437
|
+
let halfW;
|
|
1438
|
+
let halfH;
|
|
1439
|
+
if (viewAspect >= 1) {
|
|
1440
|
+
halfH = r;
|
|
1441
|
+
halfW = r * viewAspect;
|
|
1442
|
+
} else {
|
|
1443
|
+
halfW = r;
|
|
1444
|
+
halfH = r / viewAspect;
|
|
1445
|
+
}
|
|
1446
|
+
this.camera.left = -halfW;
|
|
1447
|
+
this.camera.right = halfW;
|
|
1448
|
+
this.camera.top = halfH;
|
|
1449
|
+
this.camera.bottom = -halfH;
|
|
1450
|
+
this.camera.zoom = 1;
|
|
1451
|
+
this.camera.updateProjectionMatrix();
|
|
1452
|
+
}
|
|
1453
|
+
/** Sostituisce il modello nella scena (rimuove il precedente). */
|
|
1454
|
+
setRootObject(root) {
|
|
1455
|
+
if (this.rootObject) {
|
|
1456
|
+
this.scene.remove(this.rootObject);
|
|
1457
|
+
this.rootObject = null;
|
|
1458
|
+
}
|
|
1459
|
+
this.rootObject = root;
|
|
1460
|
+
if (root) this.scene.add(root);
|
|
1461
|
+
}
|
|
1462
|
+
centerCameraOnObject() {
|
|
1463
|
+
if (!this.rootObject) return;
|
|
1464
|
+
const box = new THREE.Box3().setFromObject(this.rootObject);
|
|
1465
|
+
if (box.isEmpty()) return;
|
|
1466
|
+
const center = new THREE.Vector3();
|
|
1467
|
+
box.getCenter(center);
|
|
1468
|
+
const size = new THREE.Vector3();
|
|
1469
|
+
box.getSize(size);
|
|
1470
|
+
const maxDim = Math.max(size.x, size.y, size.z, 1e-6);
|
|
1471
|
+
const distance = Math.abs(maxDim);
|
|
1472
|
+
const frontViewUp = VIEW_DIRECTIONS.FRONT.up;
|
|
1473
|
+
this.camera.position.set(center.x, center.y + distance, center.z);
|
|
1474
|
+
this.camera.up.set(frontViewUp.x, frontViewUp.y, frontViewUp.z);
|
|
1475
|
+
this.camera.lookAt(center);
|
|
1476
|
+
this.controls.target.copy(center);
|
|
1477
|
+
this.fitOrthographicFrustumToObject();
|
|
1478
|
+
this.controls.update();
|
|
1479
|
+
}
|
|
1480
|
+
setCameraView(view) {
|
|
1481
|
+
if (!this.rootObject) return;
|
|
1482
|
+
const box = new THREE.Box3().setFromObject(this.rootObject);
|
|
1483
|
+
if (box.isEmpty()) return;
|
|
1484
|
+
const center = new THREE.Vector3();
|
|
1485
|
+
box.getCenter(center);
|
|
1486
|
+
const size = new THREE.Vector3();
|
|
1487
|
+
box.getSize(size);
|
|
1488
|
+
const distance = Math.max(size.x, size.y, size.z, 1e-6);
|
|
1489
|
+
const cfg = VIEW_DIRECTIONS[view];
|
|
1490
|
+
if (!cfg) return;
|
|
1491
|
+
const targetPosition = this.calcViewPos(view, center, distance);
|
|
1492
|
+
const targetUp = new THREE.Vector3(cfg.up.x, cfg.up.y, cfg.up.z);
|
|
1493
|
+
this.animateCameraTransition(targetPosition, targetUp, center);
|
|
1494
|
+
}
|
|
1495
|
+
calcViewPos(view, center, distance) {
|
|
1496
|
+
switch (view) {
|
|
1497
|
+
case "TOP":
|
|
1498
|
+
return new THREE.Vector3(center.x, center.y, center.z + distance);
|
|
1499
|
+
case "BOTTOM":
|
|
1500
|
+
return new THREE.Vector3(center.x, center.y, center.z - distance);
|
|
1501
|
+
case "FRONT":
|
|
1502
|
+
return new THREE.Vector3(center.x, center.y + distance, center.z);
|
|
1503
|
+
case "BACK":
|
|
1504
|
+
return new THREE.Vector3(center.x, center.y - distance, center.z);
|
|
1505
|
+
case "LEFT":
|
|
1506
|
+
return new THREE.Vector3(center.x + distance, center.y, center.z);
|
|
1507
|
+
case "RIGHT":
|
|
1508
|
+
return new THREE.Vector3(center.x - distance, center.y, center.z);
|
|
1509
|
+
default:
|
|
1510
|
+
return center.clone();
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
animateCameraTransition(targetPosition, targetUp, targetCenter) {
|
|
1514
|
+
const startPosition = this.camera.position.clone();
|
|
1515
|
+
const startUp = this.camera.up.clone();
|
|
1516
|
+
const startTarget = this.controls.target.clone();
|
|
1517
|
+
const startTime = Date.now();
|
|
1518
|
+
const duration = ANIMATION_CONFIG.DURATION_MS;
|
|
1519
|
+
const animate = () => {
|
|
1520
|
+
const elapsed = Date.now() - startTime;
|
|
1521
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
1522
|
+
const eased = progress < ANIMATION_CONFIG.EASING_THRESHOLD ? ANIMATION_CONFIG.EASING_POWER * progress * progress : 1 - Math.pow(-2 * progress + 2, 2) / 2;
|
|
1523
|
+
this.camera.position.lerpVectors(startPosition, targetPosition, eased);
|
|
1524
|
+
this.camera.up.lerpVectors(startUp, targetUp, eased);
|
|
1525
|
+
this.controls.target.lerpVectors(startTarget, targetCenter, eased);
|
|
1526
|
+
this.camera.lookAt(this.controls.target);
|
|
1527
|
+
this.controls.update();
|
|
1528
|
+
if (progress < 1) requestAnimationFrame(animate);
|
|
1529
|
+
};
|
|
1530
|
+
animate();
|
|
1531
|
+
}
|
|
1532
|
+
dispose() {
|
|
1533
|
+
if (this.animationHandle !== null) {
|
|
1534
|
+
cancelAnimationFrame(this.animationHandle);
|
|
1535
|
+
this.animationHandle = null;
|
|
1536
|
+
}
|
|
1537
|
+
this.resizeObserver?.disconnect();
|
|
1538
|
+
this.controls.dispose();
|
|
1539
|
+
this.setRootObject(null);
|
|
1540
|
+
this.renderer.dispose();
|
|
1541
|
+
if (this.renderer.domElement.parentNode) {
|
|
1542
|
+
this.renderer.domElement.parentNode.removeChild(this.renderer.domElement);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
};
|
|
1546
|
+
|
|
1547
|
+
// src/dentalGeom/mockSegmentation.ts
|
|
1548
|
+
import {
|
|
1549
|
+
Box3 as Box33,
|
|
1550
|
+
BufferAttribute,
|
|
1551
|
+
Color as Color2,
|
|
1552
|
+
Mesh as Mesh2,
|
|
1553
|
+
MeshStandardMaterial as MeshStandardMaterial2,
|
|
1554
|
+
Vector3 as Vector33
|
|
1555
|
+
} from "three";
|
|
1556
|
+
function applyMockToothBandColors(geometry) {
|
|
1557
|
+
const pos = geometry.getAttribute("position");
|
|
1558
|
+
if (!pos) return;
|
|
1559
|
+
const n = pos.count;
|
|
1560
|
+
const colors = new Float32Array(n * 3);
|
|
1561
|
+
const tmp = new Vector33();
|
|
1562
|
+
const box = new Box33();
|
|
1563
|
+
for (let i = 0; i < n; i++) {
|
|
1564
|
+
tmp.fromBufferAttribute(pos, i);
|
|
1565
|
+
box.expandByPoint(tmp);
|
|
1566
|
+
}
|
|
1567
|
+
const size = new Vector33();
|
|
1568
|
+
box.getSize(size);
|
|
1569
|
+
const minY = box.min.y;
|
|
1570
|
+
for (let i = 0; i < n; i++) {
|
|
1571
|
+
const y = pos.getY(i);
|
|
1572
|
+
const t = size.y > 1e-9 ? (y - minY) / size.y : 0;
|
|
1573
|
+
const segment = Math.min(14, Math.floor(t * 15));
|
|
1574
|
+
const hue = segment / 15 * 0.85;
|
|
1575
|
+
const c = new Color2().setHSL(hue, 0.62, 0.52);
|
|
1576
|
+
colors[i * 3] = c.r;
|
|
1577
|
+
colors[i * 3 + 1] = c.g;
|
|
1578
|
+
colors[i * 3 + 2] = c.b;
|
|
1579
|
+
}
|
|
1580
|
+
geometry.setAttribute("color", new BufferAttribute(colors, 3));
|
|
1581
|
+
const colAttr = geometry.getAttribute("color");
|
|
1582
|
+
if (colAttr) colAttr.needsUpdate = true;
|
|
1583
|
+
}
|
|
1584
|
+
function enableVertexColorsOnObjectTree(root) {
|
|
1585
|
+
root.traverse((o) => {
|
|
1586
|
+
if (o instanceof Mesh2 && o.material) {
|
|
1587
|
+
const mat = o.material;
|
|
1588
|
+
if (Array.isArray(mat)) {
|
|
1589
|
+
for (const m of mat) {
|
|
1590
|
+
if (m instanceof MeshStandardMaterial2) {
|
|
1591
|
+
m.vertexColors = true;
|
|
1592
|
+
m.needsUpdate = true;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
} else if (mat instanceof MeshStandardMaterial2) {
|
|
1596
|
+
mat.vertexColors = true;
|
|
1597
|
+
mat.needsUpdate = true;
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
// src/components/MeshViewer.tsx
|
|
1604
|
+
import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1605
|
+
var VIEW_BUTTONS = [
|
|
1606
|
+
{ key: "TOP", label: "Sopra" },
|
|
1607
|
+
{ key: "BOTTOM", label: "Sotto" },
|
|
1608
|
+
{ key: "FRONT", label: "Fronte" },
|
|
1609
|
+
{ key: "BACK", label: "Retro" },
|
|
1610
|
+
{ key: "LEFT", label: "Sin" },
|
|
1611
|
+
{ key: "RIGHT", label: "Des" }
|
|
1612
|
+
];
|
|
1613
|
+
function MeshViewer({
|
|
1614
|
+
meshDownloadUrl,
|
|
1615
|
+
selectedFile,
|
|
1616
|
+
segmentationBridge
|
|
1617
|
+
}) {
|
|
1618
|
+
const containerRef = useRef(null);
|
|
1619
|
+
const viewportRef = useRef(null);
|
|
1620
|
+
const backupRootRef = useRef(null);
|
|
1621
|
+
const sceneRootLiveRef = useRef(null);
|
|
1622
|
+
const [phase, setPhase] = useState4("idle");
|
|
1623
|
+
const [errorMsg, setErrorMsg] = useState4(null);
|
|
1624
|
+
const [confirmedLarge, setConfirmedLarge] = useState4(false);
|
|
1625
|
+
const [sceneRoot, setSceneRoot] = useState4(null);
|
|
1626
|
+
const [segmented, setSegmented] = useState4(false);
|
|
1627
|
+
const [segBusy, setSegBusy] = useState4(false);
|
|
1628
|
+
useEffect(() => {
|
|
1629
|
+
sceneRootLiveRef.current = sceneRoot;
|
|
1630
|
+
}, [sceneRoot]);
|
|
1631
|
+
useEffect(() => {
|
|
1632
|
+
return () => {
|
|
1633
|
+
if (sceneRootLiveRef.current) {
|
|
1634
|
+
disposeObject3DTree(sceneRootLiveRef.current);
|
|
1635
|
+
sceneRootLiveRef.current = null;
|
|
1636
|
+
}
|
|
1637
|
+
if (backupRootRef.current) {
|
|
1638
|
+
disposeObject3DTree(backupRootRef.current);
|
|
1639
|
+
backupRootRef.current = null;
|
|
1640
|
+
}
|
|
1641
|
+
};
|
|
1642
|
+
}, []);
|
|
1643
|
+
useEffect(() => {
|
|
1644
|
+
if (!selectedFile) {
|
|
1645
|
+
if (viewportRef.current) {
|
|
1646
|
+
viewportRef.current.dispose();
|
|
1647
|
+
viewportRef.current = null;
|
|
1648
|
+
}
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
const el = containerRef.current;
|
|
1652
|
+
if (!el) return;
|
|
1653
|
+
const vp = new DentalGeomIntakeViewport(el);
|
|
1654
|
+
viewportRef.current = vp;
|
|
1655
|
+
return () => {
|
|
1656
|
+
vp.dispose();
|
|
1657
|
+
viewportRef.current = null;
|
|
1658
|
+
};
|
|
1659
|
+
}, [selectedFile?._id]);
|
|
1660
|
+
useEffect(() => {
|
|
1661
|
+
setConfirmedLarge(false);
|
|
1662
|
+
setSegmented(false);
|
|
1663
|
+
setErrorMsg(null);
|
|
1664
|
+
setSceneRoot((prev) => {
|
|
1665
|
+
if (prev) disposeObject3DTree(prev);
|
|
1666
|
+
return null;
|
|
1667
|
+
});
|
|
1668
|
+
if (backupRootRef.current) {
|
|
1669
|
+
disposeObject3DTree(backupRootRef.current);
|
|
1670
|
+
backupRootRef.current = null;
|
|
1671
|
+
}
|
|
1672
|
+
if (!selectedFile) {
|
|
1673
|
+
setPhase("idle");
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
if (!isMeshViewerFormat(selectedFile.format)) {
|
|
1677
|
+
setPhase("error");
|
|
1678
|
+
setErrorMsg(
|
|
1679
|
+
`Formato \xAB${selectedFile.format}\xBB non supportato nel viewer (solo STL, PLY, OBJ).`
|
|
1680
|
+
);
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
if (!selectedFile.storageId) {
|
|
1684
|
+
setPhase("error");
|
|
1685
|
+
setErrorMsg(
|
|
1686
|
+
"Nessun file su storage per questa mesh (es. record solo mock senza upload reale)."
|
|
1687
|
+
);
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
if (selectedFile.sizeBytes > MESH_VIEWER_LARGE_BYTES) {
|
|
1691
|
+
setPhase("confirm_large");
|
|
1692
|
+
} else {
|
|
1693
|
+
setPhase("loading");
|
|
1694
|
+
}
|
|
1695
|
+
}, [selectedFile?._id]);
|
|
1696
|
+
useEffect(() => {
|
|
1697
|
+
if (!selectedFile || !isMeshViewerFormat(selectedFile.format)) return;
|
|
1698
|
+
if (!selectedFile.storageId) return;
|
|
1699
|
+
if (selectedFile.sizeBytes > MESH_VIEWER_LARGE_BYTES && !confirmedLarge) {
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
if (meshDownloadUrl === void 0) {
|
|
1703
|
+
setPhase("loading");
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
if (meshDownloadUrl === null) {
|
|
1707
|
+
setPhase("error");
|
|
1708
|
+
setErrorMsg(
|
|
1709
|
+
"URL di download non disponibile. L\u2019acquisizione potrebbe essere archiviata oppure il file non ha storage."
|
|
1710
|
+
);
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
let cancelled = false;
|
|
1714
|
+
(async () => {
|
|
1715
|
+
try {
|
|
1716
|
+
setPhase("loading");
|
|
1717
|
+
setErrorMsg(null);
|
|
1718
|
+
const res = await fetch(meshDownloadUrl);
|
|
1719
|
+
if (!res.ok) {
|
|
1720
|
+
throw new Error(`Download fallito (HTTP ${res.status}).`);
|
|
1721
|
+
}
|
|
1722
|
+
const buf = await res.arrayBuffer();
|
|
1723
|
+
if (cancelled) return;
|
|
1724
|
+
const loaded = loadMeshByFormat(selectedFile.format, buf);
|
|
1725
|
+
const wrapped = wrapLoadedMeshRoot(loaded);
|
|
1726
|
+
if (backupRootRef.current) {
|
|
1727
|
+
disposeObject3DTree(backupRootRef.current);
|
|
1728
|
+
}
|
|
1729
|
+
backupRootRef.current = wrapped.clone(true);
|
|
1730
|
+
const normalized = wrapped.clone(true);
|
|
1731
|
+
const fmt = selectedFile.format.toLowerCase();
|
|
1732
|
+
if (fmt === "stl") {
|
|
1733
|
+
normalized.rotation.x = Math.PI;
|
|
1734
|
+
}
|
|
1735
|
+
normalizeMeshRoot(normalized, 2);
|
|
1736
|
+
if (cancelled) {
|
|
1737
|
+
disposeObject3DTree(normalized);
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
setSceneRoot((prev) => {
|
|
1741
|
+
if (prev) disposeObject3DTree(prev);
|
|
1742
|
+
return normalized;
|
|
1743
|
+
});
|
|
1744
|
+
setPhase("ready");
|
|
1745
|
+
} catch (e) {
|
|
1746
|
+
if (cancelled) return;
|
|
1747
|
+
setPhase("error");
|
|
1748
|
+
setErrorMsg(
|
|
1749
|
+
e instanceof Error ? e.message : "Errore durante il caricamento della mesh."
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
})();
|
|
1753
|
+
return () => {
|
|
1754
|
+
cancelled = true;
|
|
1755
|
+
};
|
|
1756
|
+
}, [
|
|
1757
|
+
selectedFile?._id,
|
|
1758
|
+
selectedFile?.format,
|
|
1759
|
+
selectedFile?.storageId,
|
|
1760
|
+
selectedFile?.sizeBytes,
|
|
1761
|
+
meshDownloadUrl,
|
|
1762
|
+
confirmedLarge
|
|
1763
|
+
]);
|
|
1764
|
+
useEffect(() => {
|
|
1765
|
+
const vp = viewportRef.current;
|
|
1766
|
+
if (!vp || phase !== "ready" || !sceneRoot) {
|
|
1767
|
+
vp?.setRootObject(null);
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
vp.setRootObject(sceneRoot);
|
|
1771
|
+
vp.centerCameraOnObject();
|
|
1772
|
+
}, [phase, sceneRoot]);
|
|
1773
|
+
const handleRecentrar = () => {
|
|
1774
|
+
const raw = backupRootRef.current;
|
|
1775
|
+
if (!raw || !selectedFile) return;
|
|
1776
|
+
setSegmented(false);
|
|
1777
|
+
setSceneRoot((prev) => {
|
|
1778
|
+
if (prev) disposeObject3DTree(prev);
|
|
1779
|
+
const next = raw.clone(true);
|
|
1780
|
+
if (selectedFile.format.toLowerCase() === "stl") {
|
|
1781
|
+
next.rotation.x = Math.PI;
|
|
1782
|
+
}
|
|
1783
|
+
normalizeMeshRoot(next, 2);
|
|
1784
|
+
return next;
|
|
1785
|
+
});
|
|
1786
|
+
};
|
|
1787
|
+
const handleView = (v) => {
|
|
1788
|
+
viewportRef.current?.setCameraView(v);
|
|
1789
|
+
};
|
|
1790
|
+
const runSegmentation = async () => {
|
|
1791
|
+
const sf = selectedFile;
|
|
1792
|
+
if (!sf || !backupRootRef.current) return;
|
|
1793
|
+
if (segmentationBridge?.segmentMesh && (!meshDownloadUrl || meshDownloadUrl === null)) {
|
|
1794
|
+
setErrorMsg("URL mesh mancante per segmentazione remota.");
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
setSegBusy(true);
|
|
1798
|
+
setErrorMsg(null);
|
|
1799
|
+
try {
|
|
1800
|
+
let rgb = null;
|
|
1801
|
+
if (segmentationBridge?.segmentMesh && meshDownloadUrl) {
|
|
1802
|
+
const res = await fetch(meshDownloadUrl);
|
|
1803
|
+
if (!res.ok) throw new Error(`Segmentazione: download HTTP ${res.status}`);
|
|
1804
|
+
const buf = await res.arrayBuffer();
|
|
1805
|
+
rgb = await segmentationBridge.segmentMesh({
|
|
1806
|
+
arrayBuffer: buf,
|
|
1807
|
+
format: sf.format,
|
|
1808
|
+
fileName: sf.originalName
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
const next = backupRootRef.current.clone(true);
|
|
1812
|
+
if (sf.format.toLowerCase() === "stl") {
|
|
1813
|
+
next.rotation.x = Math.PI;
|
|
1814
|
+
}
|
|
1815
|
+
normalizeMeshRoot(next, 2);
|
|
1816
|
+
let remaining = rgb && rgb.length > 0 ? rgb : null;
|
|
1817
|
+
next.traverse((o) => {
|
|
1818
|
+
if (o instanceof Mesh3 && o.geometry) {
|
|
1819
|
+
const geo = o.geometry;
|
|
1820
|
+
const pos = geo.getAttribute("position");
|
|
1821
|
+
const need = pos ? pos.count * 3 : 0;
|
|
1822
|
+
if (remaining !== null && need > 0 && remaining.length >= need) {
|
|
1823
|
+
const chunk = remaining.subarray(0, need);
|
|
1824
|
+
const copy = new Float32Array(chunk);
|
|
1825
|
+
geo.setAttribute("color", new BufferAttribute2(copy, 3));
|
|
1826
|
+
remaining = remaining.subarray(need);
|
|
1827
|
+
} else {
|
|
1828
|
+
applyMockToothBandColors(geo);
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
});
|
|
1832
|
+
enableVertexColorsOnObjectTree(next);
|
|
1833
|
+
setSceneRoot((prev) => {
|
|
1834
|
+
if (prev) disposeObject3DTree(prev);
|
|
1835
|
+
return next;
|
|
1836
|
+
});
|
|
1837
|
+
setSegmented(true);
|
|
1838
|
+
} catch (e) {
|
|
1839
|
+
setErrorMsg(
|
|
1840
|
+
e instanceof Error ? e.message : "Segmentazione non riuscita."
|
|
1841
|
+
);
|
|
1842
|
+
} finally {
|
|
1843
|
+
setSegBusy(false);
|
|
1844
|
+
}
|
|
1845
|
+
};
|
|
1846
|
+
if (!selectedFile) {
|
|
1847
|
+
return /* @__PURE__ */ jsxs9("div", { className: "preview-3d preview-3d-empty", children: [
|
|
1848
|
+
/* @__PURE__ */ jsx10("strong", { children: "Anteprima 3D" }),
|
|
1849
|
+
/* @__PURE__ */ jsx10("span", { children: "Seleziona una mesh (STL, PLY, OBJ) dalla lista o dal pannello review." })
|
|
1850
|
+
] });
|
|
1851
|
+
}
|
|
1852
|
+
return /* @__PURE__ */ jsxs9("div", { className: "mesh-viewer-wrap", children: [
|
|
1853
|
+
/* @__PURE__ */ jsxs9("div", { className: "mesh-viewer-toolbar", children: [
|
|
1854
|
+
/* @__PURE__ */ jsxs9("span", { className: "muted", style: { fontSize: "0.8rem", marginRight: "0.5rem" }, children: [
|
|
1855
|
+
"Viewer",
|
|
1856
|
+
" ",
|
|
1857
|
+
/* @__PURE__ */ jsx10(
|
|
1858
|
+
"a",
|
|
1859
|
+
{
|
|
1860
|
+
href: "https://github.com/Ellipsis1/DentalGeom",
|
|
1861
|
+
target: "_blank",
|
|
1862
|
+
rel: "noopener noreferrer",
|
|
1863
|
+
style: { color: "#93c5fd" },
|
|
1864
|
+
children: "DentalGeom"
|
|
1865
|
+
}
|
|
1866
|
+
),
|
|
1867
|
+
"-style"
|
|
1868
|
+
] }),
|
|
1869
|
+
VIEW_BUTTONS.map(({ key, label }) => /* @__PURE__ */ jsx10(
|
|
1870
|
+
"button",
|
|
1871
|
+
{
|
|
1872
|
+
type: "button",
|
|
1873
|
+
className: "btn btn-ghost",
|
|
1874
|
+
disabled: !sceneRoot,
|
|
1875
|
+
onClick: () => handleView(key),
|
|
1876
|
+
children: label
|
|
1877
|
+
},
|
|
1878
|
+
key
|
|
1879
|
+
)),
|
|
1880
|
+
/* @__PURE__ */ jsx10(
|
|
1881
|
+
"button",
|
|
1882
|
+
{
|
|
1883
|
+
type: "button",
|
|
1884
|
+
className: "btn btn-ghost",
|
|
1885
|
+
disabled: !sceneRoot,
|
|
1886
|
+
onClick: () => viewportRef.current?.centerCameraOnObject(),
|
|
1887
|
+
children: "Centra camera"
|
|
1888
|
+
}
|
|
1889
|
+
),
|
|
1890
|
+
/* @__PURE__ */ jsx10(
|
|
1891
|
+
"button",
|
|
1892
|
+
{
|
|
1893
|
+
type: "button",
|
|
1894
|
+
className: "btn btn-ghost",
|
|
1895
|
+
disabled: !sceneRoot,
|
|
1896
|
+
onClick: handleRecentrar,
|
|
1897
|
+
children: "Reset modello"
|
|
1898
|
+
}
|
|
1899
|
+
),
|
|
1900
|
+
/* @__PURE__ */ jsx10(
|
|
1901
|
+
"button",
|
|
1902
|
+
{
|
|
1903
|
+
type: "button",
|
|
1904
|
+
className: "btn btn-primary",
|
|
1905
|
+
disabled: !sceneRoot || segBusy || phase !== "ready",
|
|
1906
|
+
title: segmentationBridge?.segmentMesh ? "Segmentazione tramite bridge host (es. MeshSegNet worker)" : "Demo locale (bande color)\u2014non \xE8 MeshSegNet clinico",
|
|
1907
|
+
onClick: () => void runSegmentation(),
|
|
1908
|
+
children: segBusy ? "Segmentazione\u2026" : segmented ? "Ri-segmenta" : "Segmenta denti"
|
|
1909
|
+
}
|
|
1910
|
+
)
|
|
1911
|
+
] }),
|
|
1912
|
+
/* @__PURE__ */ jsxs9("p", { className: "muted", style: { fontSize: "0.76rem", margin: "0.25rem 0" }, children: [
|
|
1913
|
+
"MeshSegNet (",
|
|
1914
|
+
/* @__PURE__ */ jsx10(
|
|
1915
|
+
"a",
|
|
1916
|
+
{
|
|
1917
|
+
href: "https://github.com/Tai-Hsien/MeshSegNet",
|
|
1918
|
+
target: "_blank",
|
|
1919
|
+
rel: "noopener noreferrer",
|
|
1920
|
+
children: "Tai-Hsien/MeshSegNet"
|
|
1921
|
+
}
|
|
1922
|
+
),
|
|
1923
|
+
") \xE8 PyTorch: esporre un servizio esterno e passare",
|
|
1924
|
+
" ",
|
|
1925
|
+
/* @__PURE__ */ jsx10("code", { children: "segmentationBridge" }),
|
|
1926
|
+
" dall\u2019host, oppure usare la demo colori locale."
|
|
1927
|
+
] }),
|
|
1928
|
+
phase === "confirm_large" ? /* @__PURE__ */ jsxs9("div", { className: "warning-box", style: { marginBottom: "0.65rem" }, children: [
|
|
1929
|
+
"File di ",
|
|
1930
|
+
(selectedFile.sizeBytes / (1024 * 1024)).toFixed(1),
|
|
1931
|
+
" MB: il caricamento pu\xF2 essere lento o instabile nel browser. Continuare?",
|
|
1932
|
+
/* @__PURE__ */ jsx10("div", { className: "btn-row", style: { marginTop: "0.5rem" }, children: /* @__PURE__ */ jsx10(
|
|
1933
|
+
"button",
|
|
1934
|
+
{
|
|
1935
|
+
type: "button",
|
|
1936
|
+
className: "btn btn-primary",
|
|
1937
|
+
onClick: () => setConfirmedLarge(true),
|
|
1938
|
+
children: "Carica nel viewer"
|
|
1939
|
+
}
|
|
1940
|
+
) })
|
|
1941
|
+
] }) : null,
|
|
1942
|
+
phase === "loading" ? /* @__PURE__ */ jsx10("p", { className: "muted", style: { margin: "0.35rem 0 0.5rem" }, children: "Caricamento mesh\u2026" }) : null,
|
|
1943
|
+
errorMsg ? /* @__PURE__ */ jsx10("div", { className: "err-banner", children: errorMsg }) : null,
|
|
1944
|
+
/* @__PURE__ */ jsx10(
|
|
1945
|
+
"div",
|
|
1946
|
+
{
|
|
1947
|
+
ref: containerRef,
|
|
1948
|
+
className: "mesh-viewer-canvas dentalgeom-viewport",
|
|
1949
|
+
style: { minHeight: 420, width: "100%", borderRadius: 12 }
|
|
1950
|
+
}
|
|
1951
|
+
)
|
|
1952
|
+
] });
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
// src/components/MeshFileSelector.tsx
|
|
1956
|
+
import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1957
|
+
function MeshFileSelector({
|
|
1958
|
+
files,
|
|
1959
|
+
selectedId,
|
|
1960
|
+
onSelect
|
|
1961
|
+
}) {
|
|
1962
|
+
const meshes = files?.filter((f) => isMeshViewerFormat(f.format)) ?? [];
|
|
1963
|
+
if (meshes.length === 0) {
|
|
1964
|
+
return /* @__PURE__ */ jsx11("p", { className: "muted", style: { marginBottom: "0.75rem" }, children: "Nessuna mesh STL/PLY/OBJ in questa acquisizione. Carica file reali o usa il seed demo con geometrie mock." });
|
|
1965
|
+
}
|
|
1966
|
+
return /* @__PURE__ */ jsxs10("div", { className: "mesh-selector", style: { marginBottom: "0.85rem" }, children: [
|
|
1967
|
+
/* @__PURE__ */ jsxs10("div", { className: "muted", style: { marginBottom: "0.45rem", fontSize: "0.85rem" }, children: [
|
|
1968
|
+
"Mesh disponibili (",
|
|
1969
|
+
meshes.length,
|
|
1970
|
+
")"
|
|
1971
|
+
] }),
|
|
1972
|
+
/* @__PURE__ */ jsx11("div", { className: "mesh-selector-list", children: meshes.map((f) => {
|
|
1973
|
+
const active = f._id === selectedId;
|
|
1974
|
+
const large = f.sizeBytes > MESH_VIEWER_LARGE_BYTES;
|
|
1975
|
+
return /* @__PURE__ */ jsxs10(
|
|
1976
|
+
"button",
|
|
1977
|
+
{
|
|
1978
|
+
type: "button",
|
|
1979
|
+
className: `mesh-selector-item ${active ? "mesh-selector-item-active" : ""}`,
|
|
1980
|
+
onClick: () => onSelect(f),
|
|
1981
|
+
children: [
|
|
1982
|
+
/* @__PURE__ */ jsx11("div", { style: { fontWeight: 600 }, children: f.originalName }),
|
|
1983
|
+
/* @__PURE__ */ jsxs10("div", { className: "btn-row", style: { marginTop: "0.35rem", gap: "0.35rem" }, children: [
|
|
1984
|
+
/* @__PURE__ */ jsx11(SmallBadge, { variant: "role", children: f.role }),
|
|
1985
|
+
/* @__PURE__ */ jsx11(SmallBadge, { variant: "format", children: f.format }),
|
|
1986
|
+
/* @__PURE__ */ jsxs10("span", { className: "muted", style: { fontSize: "0.75rem" }, children: [
|
|
1987
|
+
(f.sizeBytes / 1024).toFixed(0),
|
|
1988
|
+
" KB",
|
|
1989
|
+
large ? " \xB7 grande" : ""
|
|
1990
|
+
] })
|
|
1991
|
+
] })
|
|
1992
|
+
]
|
|
1993
|
+
},
|
|
1994
|
+
f._id
|
|
1995
|
+
);
|
|
1996
|
+
}) })
|
|
1997
|
+
] });
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
// src/components/DigitalAcquisitionDetail.tsx
|
|
2001
|
+
import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
2002
|
+
function readinessText(k) {
|
|
2003
|
+
if (!k) return "\u2014";
|
|
2004
|
+
return readinessLabelUi[k];
|
|
2005
|
+
}
|
|
2006
|
+
function DigitalAcquisitionDetail({
|
|
2007
|
+
acquisitionId,
|
|
2008
|
+
onBack,
|
|
2009
|
+
doc,
|
|
2010
|
+
files,
|
|
2011
|
+
events,
|
|
2012
|
+
manifest,
|
|
2013
|
+
packageDownloadUrl,
|
|
2014
|
+
meshDownloadUrl,
|
|
2015
|
+
meshSegmentationBridge,
|
|
2016
|
+
pipeline,
|
|
2017
|
+
upload,
|
|
2018
|
+
review,
|
|
2019
|
+
renderFileListActions,
|
|
2020
|
+
previewMeshId,
|
|
2021
|
+
onPreviewMeshIdChange
|
|
2022
|
+
}) {
|
|
2023
|
+
const [patientId, setPatientId] = useState5("");
|
|
2024
|
+
const [prescriptionId, setPrescriptionId] = useState5("");
|
|
2025
|
+
const [labWorkOrderId, setLabWorkOrderId] = useState5("");
|
|
2026
|
+
const [customerId, setCustomerId] = useState5("");
|
|
2027
|
+
const [externalCaseId, setExternalCaseId] = useState5("");
|
|
2028
|
+
const [err, setErr] = useState5(null);
|
|
2029
|
+
const [busy, setBusy] = useState5(false);
|
|
2030
|
+
const viewableMeshCount = useMemo(
|
|
2031
|
+
() => (files ?? []).filter((f) => isMeshViewerFormat(f.format)).length,
|
|
2032
|
+
[files]
|
|
2033
|
+
);
|
|
2034
|
+
const proprietary3dFiles = useMemo(
|
|
2035
|
+
() => (files ?? []).filter((f) => {
|
|
2036
|
+
const fmt = f.format.toLowerCase();
|
|
2037
|
+
const ext = f.extension.toLowerCase();
|
|
2038
|
+
return ["dcm", "3ml"].includes(fmt) || ["dcm", "3ml"].includes(ext);
|
|
2039
|
+
}),
|
|
2040
|
+
[files]
|
|
2041
|
+
);
|
|
2042
|
+
const selectedMeshFile = useMemo(() => {
|
|
2043
|
+
if (!previewMeshId || !files?.length) return null;
|
|
2044
|
+
const f = files.find((x) => x._id === previewMeshId);
|
|
2045
|
+
if (!f) return null;
|
|
2046
|
+
return {
|
|
2047
|
+
_id: f._id,
|
|
2048
|
+
originalName: f.originalName,
|
|
2049
|
+
format: f.format,
|
|
2050
|
+
sizeBytes: f.sizeBytes,
|
|
2051
|
+
role: f.role,
|
|
2052
|
+
storageId: f.storageId
|
|
2053
|
+
};
|
|
2054
|
+
}, [previewMeshId, files]);
|
|
2055
|
+
const run = async (fn) => {
|
|
2056
|
+
setErr(null);
|
|
2057
|
+
setBusy(true);
|
|
2058
|
+
try {
|
|
2059
|
+
await fn();
|
|
2060
|
+
} catch (e) {
|
|
2061
|
+
setErr(e instanceof Error ? e.message : "Errore");
|
|
2062
|
+
} finally {
|
|
2063
|
+
setBusy(false);
|
|
2064
|
+
}
|
|
2065
|
+
};
|
|
2066
|
+
if (!doc) {
|
|
2067
|
+
return /* @__PURE__ */ jsx12("div", { className: "panel", children: /* @__PURE__ */ jsx12("p", { className: "muted", children: "Caricamento dettaglio\u2026" }) });
|
|
2068
|
+
}
|
|
2069
|
+
const extraction = doc.extractionStatus;
|
|
2070
|
+
const extractionError = doc.extractionError;
|
|
2071
|
+
const validationProfile = doc.validationProfile;
|
|
2072
|
+
const validationResult = doc.validationResult;
|
|
2073
|
+
const originalPackageName = doc.originalPackageName;
|
|
2074
|
+
const originalPackageSizeBytes = doc.originalPackageSizeBytes;
|
|
2075
|
+
const hasOriginalPackage = Boolean(doc.originalPackageStorageId);
|
|
2076
|
+
const terminal = doc.status === "archived" || doc.status === "failed";
|
|
2077
|
+
const canMutate = !terminal;
|
|
2078
|
+
const canBeginUpload = canMutate && doc.status === "draft";
|
|
2079
|
+
const canMarkUploaded = canMutate && doc.status === "waiting_files";
|
|
2080
|
+
const extractionBlocksParse = extraction === "extracting" || extraction === "failed";
|
|
2081
|
+
const canParse = canMutate && doc.status === "uploaded" && !extractionBlocksParse;
|
|
2082
|
+
const canValidate = canMutate && Boolean(manifest?.readiness.canValidate);
|
|
2083
|
+
const canMarkReady = canMutate && Boolean(manifest?.validation.canMarkReady);
|
|
2084
|
+
const canArchive = canMutate && doc.status === "ready_for_next_step";
|
|
2085
|
+
const canFail = canMutate && doc.status !== "failed" && doc.status !== "archived";
|
|
2086
|
+
const markReadyLabel = validationResult?.readinessLabel === "ready_for_lab" ? "5 \xB7 Ready for Lab" : validationResult?.readinessLabel === "ready_for_cad" ? "5 \xB7 Ready for CAD" : "5 \xB7 Ready for Next Step";
|
|
2087
|
+
return /* @__PURE__ */ jsxs11("div", { children: [
|
|
2088
|
+
/* @__PURE__ */ jsx12("div", { className: "btn-row", style: { marginBottom: "1rem" }, children: /* @__PURE__ */ jsx12("button", { type: "button", className: "btn btn-ghost", onClick: onBack, children: "\u2190 Dashboard" }) }),
|
|
2089
|
+
/* @__PURE__ */ jsxs11("div", { className: "panel", style: { marginBottom: "1rem" }, children: [
|
|
2090
|
+
/* @__PURE__ */ jsx12("div", { className: "btn-row", style: { marginBottom: "0.75rem" }, children: /* @__PURE__ */ jsx12(StatusBadge, { status: doc.status }) }),
|
|
2091
|
+
/* @__PURE__ */ jsx12("h2", { style: { margin: "0 0 0.5rem" }, children: doc.title }),
|
|
2092
|
+
/* @__PURE__ */ jsxs11("p", { className: "muted", style: { margin: 0 }, children: [
|
|
2093
|
+
originLabel(doc.origin),
|
|
2094
|
+
" \xB7 ",
|
|
2095
|
+
hostAppLabel(doc.hostApp),
|
|
2096
|
+
" \xB7 Review:",
|
|
2097
|
+
" ",
|
|
2098
|
+
doc.reviewStatus,
|
|
2099
|
+
" \xB7 Vendor ",
|
|
2100
|
+
vendorLabel(doc.sourceVendor),
|
|
2101
|
+
" (",
|
|
2102
|
+
doc.vendorConfidence.toFixed(2),
|
|
2103
|
+
")"
|
|
2104
|
+
] }),
|
|
2105
|
+
doc.warnings?.length ? /* @__PURE__ */ jsxs11("div", { className: "warning-box", children: [
|
|
2106
|
+
/* @__PURE__ */ jsx12("strong", { children: "Warning" }),
|
|
2107
|
+
/* @__PURE__ */ jsx12("div", { style: { marginTop: "0.35rem" }, children: doc.warnings.join(" \xB7 ") })
|
|
2108
|
+
] }) : null,
|
|
2109
|
+
doc.sourceAdapterType ?? doc.sourceProviderId ?? doc.externalCaseId ?? doc.externalPatientId ?? (doc.externalReferences && typeof doc.externalReferences === "object" && Object.keys(doc.externalReferences).length > 0) ? /* @__PURE__ */ jsxs11(
|
|
2110
|
+
"div",
|
|
2111
|
+
{
|
|
2112
|
+
style: {
|
|
2113
|
+
marginTop: "0.75rem",
|
|
2114
|
+
padding: "0.65rem 0.85rem",
|
|
2115
|
+
borderRadius: 12,
|
|
2116
|
+
background: "rgba(59, 130, 246, 0.06)",
|
|
2117
|
+
border: "1px solid rgba(59, 130, 246, 0.2)",
|
|
2118
|
+
fontSize: "0.82rem"
|
|
2119
|
+
},
|
|
2120
|
+
children: [
|
|
2121
|
+
/* @__PURE__ */ jsx12("strong", { style: { color: "#93c5fd" }, children: "Riferimenti esterni / integrazioni" }),
|
|
2122
|
+
/* @__PURE__ */ jsxs11("div", { className: "muted", style: { marginTop: "0.35rem" }, children: [
|
|
2123
|
+
doc.sourceAdapterType ? /* @__PURE__ */ jsxs11(Fragment3, { children: [
|
|
2124
|
+
"Adapter:",
|
|
2125
|
+
" ",
|
|
2126
|
+
/* @__PURE__ */ jsx12("code", { children: doc.sourceAdapterType }),
|
|
2127
|
+
/* @__PURE__ */ jsx12("br", {})
|
|
2128
|
+
] }) : null,
|
|
2129
|
+
doc.sourceProviderId ? /* @__PURE__ */ jsxs11(Fragment3, { children: [
|
|
2130
|
+
"Provider id:",
|
|
2131
|
+
" ",
|
|
2132
|
+
/* @__PURE__ */ jsx12("code", { style: { wordBreak: "break-all" }, children: doc.sourceProviderId }),
|
|
2133
|
+
/* @__PURE__ */ jsx12("br", {})
|
|
2134
|
+
] }) : null,
|
|
2135
|
+
doc.externalCaseId ? /* @__PURE__ */ jsxs11(Fragment3, { children: [
|
|
2136
|
+
"External case id:",
|
|
2137
|
+
" ",
|
|
2138
|
+
/* @__PURE__ */ jsx12("code", { children: doc.externalCaseId }),
|
|
2139
|
+
/* @__PURE__ */ jsx12("br", {})
|
|
2140
|
+
] }) : null,
|
|
2141
|
+
doc.externalPatientId ? /* @__PURE__ */ jsxs11(Fragment3, { children: [
|
|
2142
|
+
"External patient id: ",
|
|
2143
|
+
/* @__PURE__ */ jsx12("code", { children: doc.externalPatientId }),
|
|
2144
|
+
/* @__PURE__ */ jsx12("br", {})
|
|
2145
|
+
] }) : null,
|
|
2146
|
+
doc.linkedEntity.externalCaseId && !doc.externalCaseId ? /* @__PURE__ */ jsxs11(Fragment3, { children: [
|
|
2147
|
+
"Linked external case id:",
|
|
2148
|
+
" ",
|
|
2149
|
+
/* @__PURE__ */ jsx12("code", { children: doc.linkedEntity.externalCaseId }),
|
|
2150
|
+
/* @__PURE__ */ jsx12("br", {})
|
|
2151
|
+
] }) : null
|
|
2152
|
+
] }),
|
|
2153
|
+
doc.externalReferences && typeof doc.externalReferences === "object" && Object.keys(doc.externalReferences).length > 0 ? /* @__PURE__ */ jsxs11("div", { style: { marginTop: "0.5rem" }, children: [
|
|
2154
|
+
/* @__PURE__ */ jsx12("div", { className: "muted", style: { marginBottom: "0.25rem" }, children: /* @__PURE__ */ jsx12("code", { style: { fontSize: "0.78rem" }, children: "externalReferences" }) }),
|
|
2155
|
+
/* @__PURE__ */ jsx12(
|
|
2156
|
+
"pre",
|
|
2157
|
+
{
|
|
2158
|
+
className: "muted",
|
|
2159
|
+
style: {
|
|
2160
|
+
margin: 0,
|
|
2161
|
+
maxHeight: 220,
|
|
2162
|
+
overflow: "auto",
|
|
2163
|
+
fontSize: "0.75rem",
|
|
2164
|
+
lineHeight: 1.35,
|
|
2165
|
+
whiteSpace: "pre-wrap",
|
|
2166
|
+
wordBreak: "break-word"
|
|
2167
|
+
},
|
|
2168
|
+
children: JSON.stringify(doc.externalReferences, null, 2)
|
|
2169
|
+
}
|
|
2170
|
+
)
|
|
2171
|
+
] }) : null
|
|
2172
|
+
]
|
|
2173
|
+
}
|
|
2174
|
+
) : null,
|
|
2175
|
+
terminal ? /* @__PURE__ */ jsx12("p", { className: "muted", style: { marginTop: "0.75rem", marginBottom: 0 }, children: "Stato terminale: modifiche e upload disabilitati. Solo consultazione manifest e timeline." }) : null
|
|
2176
|
+
] }),
|
|
2177
|
+
/* @__PURE__ */ jsxs11("div", { className: "panel", style: { marginBottom: "1rem" }, children: [
|
|
2178
|
+
/* @__PURE__ */ jsx12("h3", { style: { marginTop: 0 }, children: "Validazione (engine lato server)" }),
|
|
2179
|
+
/* @__PURE__ */ jsxs11("p", { className: "muted", style: { marginTop: 0 }, children: [
|
|
2180
|
+
"Profilo:",
|
|
2181
|
+
" ",
|
|
2182
|
+
/* @__PURE__ */ jsx12("strong", { children: validationProfile ? validationProfileLabel[validationProfile] : "\u2014" }),
|
|
2183
|
+
" ",
|
|
2184
|
+
/* @__PURE__ */ jsxs11("code", { style: { fontSize: "0.8rem" }, children: [
|
|
2185
|
+
"(",
|
|
2186
|
+
validationProfile ?? "n/d",
|
|
2187
|
+
")"
|
|
2188
|
+
] })
|
|
2189
|
+
] }),
|
|
2190
|
+
/* @__PURE__ */ jsxs11("p", { className: "muted", children: [
|
|
2191
|
+
"Readiness se validazione OK:",
|
|
2192
|
+
" ",
|
|
2193
|
+
/* @__PURE__ */ jsx12("strong", { children: manifest?.validation.readinessLabel ? readinessText(manifest.validation.readinessLabel) : "\u2014" })
|
|
2194
|
+
] }),
|
|
2195
|
+
validationResult ? /* @__PURE__ */ jsxs11(Fragment3, { children: [
|
|
2196
|
+
/* @__PURE__ */ jsxs11("p", { className: "muted", style: { marginBottom: "0.35rem" }, children: [
|
|
2197
|
+
"Ultimo esito:",
|
|
2198
|
+
" ",
|
|
2199
|
+
/* @__PURE__ */ jsx12("strong", { children: validationResult.isValid ? "valido" : "non valido" }),
|
|
2200
|
+
" ",
|
|
2201
|
+
"\xB7 ",
|
|
2202
|
+
new Date(validationResult.checkedAt).toLocaleString()
|
|
2203
|
+
] }),
|
|
2204
|
+
validationResult.blockingIssues.length ? /* @__PURE__ */ jsxs11("div", { className: "err-banner", children: [
|
|
2205
|
+
/* @__PURE__ */ jsx12("strong", { children: "Blocking (ultimo run)" }),
|
|
2206
|
+
/* @__PURE__ */ jsx12("ul", { style: { margin: "0.35rem 0 0", paddingLeft: "1.1rem" }, children: validationResult.blockingIssues.map((x) => /* @__PURE__ */ jsx12("li", { children: x }, x)) })
|
|
2207
|
+
] }) : null,
|
|
2208
|
+
validationResult.warnings.length ? /* @__PURE__ */ jsxs11("div", { className: "warning-box", style: { marginTop: "0.5rem" }, children: [
|
|
2209
|
+
/* @__PURE__ */ jsx12("strong", { children: "Warning validazione" }),
|
|
2210
|
+
/* @__PURE__ */ jsx12("ul", { style: { margin: "0.35rem 0 0", paddingLeft: "1.1rem" }, children: validationResult.warnings.map((x) => /* @__PURE__ */ jsx12("li", { children: x }, x)) })
|
|
2211
|
+
] }) : null
|
|
2212
|
+
] }) : /* @__PURE__ */ jsx12("p", { className: "muted", children: "Nessuna validazione formale registrata." }),
|
|
2213
|
+
manifest && (manifest.validation.blockingIssues.length > 0 || manifest.validation.warnings.length > 0) ? /* @__PURE__ */ jsxs11("div", { style: { marginTop: "0.65rem" }, children: [
|
|
2214
|
+
/* @__PURE__ */ jsx12("strong", { className: "muted", children: "Snapshot engine (manifest)" }),
|
|
2215
|
+
manifest.validation.blockingIssues.length ? /* @__PURE__ */ jsx12(
|
|
2216
|
+
"ul",
|
|
2217
|
+
{
|
|
2218
|
+
className: "muted",
|
|
2219
|
+
style: { margin: "0.35rem 0 0", paddingLeft: "1.1rem" },
|
|
2220
|
+
children: manifest.validation.blockingIssues.map((x) => /* @__PURE__ */ jsx12("li", { children: x }, x))
|
|
2221
|
+
}
|
|
2222
|
+
) : null,
|
|
2223
|
+
manifest.validation.warnings.length ? /* @__PURE__ */ jsx12(
|
|
2224
|
+
"ul",
|
|
2225
|
+
{
|
|
2226
|
+
className: "muted",
|
|
2227
|
+
style: { margin: "0.35rem 0 0", paddingLeft: "1.1rem" },
|
|
2228
|
+
children: manifest.validation.warnings.map((x) => /* @__PURE__ */ jsx12("li", { children: x }, x))
|
|
2229
|
+
}
|
|
2230
|
+
) : null
|
|
2231
|
+
] }) : null
|
|
2232
|
+
] }),
|
|
2233
|
+
hasOriginalPackage ? /* @__PURE__ */ jsxs11("div", { className: "panel", style: { marginBottom: "1rem" }, children: [
|
|
2234
|
+
/* @__PURE__ */ jsx12("h3", { style: { marginTop: 0 }, children: "Pacchetto caricato" }),
|
|
2235
|
+
/* @__PURE__ */ jsxs11("p", { className: "muted", style: { marginTop: 0 }, children: [
|
|
2236
|
+
/* @__PURE__ */ jsx12("strong", { children: originalPackageName ?? "\u2014" }),
|
|
2237
|
+
originalPackageSizeBytes != null ? ` \xB7 ${(originalPackageSizeBytes / 1024).toFixed(1)} KB` : null
|
|
2238
|
+
] }),
|
|
2239
|
+
/* @__PURE__ */ jsxs11("p", { className: "muted", style: { margin: "0.35rem 0" }, children: [
|
|
2240
|
+
"Estrazione: ",
|
|
2241
|
+
/* @__PURE__ */ jsx12("code", { children: extraction ?? "n/d" }),
|
|
2242
|
+
extraction === "extracting" ? " \u2014 attendi il completamento sull\u2019action Convex." : null
|
|
2243
|
+
] }),
|
|
2244
|
+
extractionError ? /* @__PURE__ */ jsx12("div", { className: "err-banner", style: { marginTop: "0.5rem" }, children: extractionError }) : null,
|
|
2245
|
+
packageDownloadUrl ? /* @__PURE__ */ jsx12(
|
|
2246
|
+
"a",
|
|
2247
|
+
{
|
|
2248
|
+
href: packageDownloadUrl,
|
|
2249
|
+
target: "_blank",
|
|
2250
|
+
rel: "noopener noreferrer",
|
|
2251
|
+
className: "btn btn-ghost",
|
|
2252
|
+
style: { display: "inline-block", marginTop: "0.5rem" },
|
|
2253
|
+
children: "Scarica pacchetto originale"
|
|
2254
|
+
}
|
|
2255
|
+
) : packageDownloadUrl === void 0 && hasOriginalPackage ? /* @__PURE__ */ jsx12("p", { className: "muted", style: { marginBottom: 0 }, children: "Recupero URL download\u2026" }) : null
|
|
2256
|
+
] }) : null,
|
|
2257
|
+
manifest?.cadProject ? /* @__PURE__ */ jsx12(
|
|
2258
|
+
CaseSummaryCard,
|
|
2259
|
+
{
|
|
2260
|
+
cadProject: manifest.cadProject,
|
|
2261
|
+
fallbackTitle: doc.title
|
|
2262
|
+
}
|
|
2263
|
+
) : null,
|
|
2264
|
+
manifest?.cadProject ? /* @__PURE__ */ jsx12(
|
|
2265
|
+
CadProjectMetadataPanel,
|
|
2266
|
+
{
|
|
2267
|
+
cadProject: manifest.cadProject,
|
|
2268
|
+
manifestFiles: manifest.files
|
|
2269
|
+
}
|
|
2270
|
+
) : null,
|
|
2271
|
+
err ? /* @__PURE__ */ jsx12("div", { className: "err-banner", children: err }) : null,
|
|
2272
|
+
/* @__PURE__ */ jsxs11("div", { className: "panel", style: { marginBottom: "1rem" }, children: [
|
|
2273
|
+
/* @__PURE__ */ jsx12("h3", { style: { marginTop: 0 }, children: "Azioni pipeline (enforce server-side)" }),
|
|
2274
|
+
/* @__PURE__ */ jsxs11("p", { className: "muted", style: { marginTop: 0 }, children: [
|
|
2275
|
+
"Stato attuale: ",
|
|
2276
|
+
/* @__PURE__ */ jsx12("strong", { children: doc.status }),
|
|
2277
|
+
". I pulsanti disabilitati non sono ammessi dal backend in questa fase."
|
|
2278
|
+
] }),
|
|
2279
|
+
/* @__PURE__ */ jsxs11("div", { className: "tool-grid", children: [
|
|
2280
|
+
/* @__PURE__ */ jsx12(
|
|
2281
|
+
"button",
|
|
2282
|
+
{
|
|
2283
|
+
type: "button",
|
|
2284
|
+
className: "btn btn-primary",
|
|
2285
|
+
disabled: busy || !canBeginUpload,
|
|
2286
|
+
title: canBeginUpload ? "Avvia sessione upload" : "Solo da draft e se non terminale",
|
|
2287
|
+
onClick: () => void run(() => pipeline.beginUpload()),
|
|
2288
|
+
children: "1 \xB7 Inizia upload"
|
|
2289
|
+
}
|
|
2290
|
+
),
|
|
2291
|
+
/* @__PURE__ */ jsx12(
|
|
2292
|
+
"button",
|
|
2293
|
+
{
|
|
2294
|
+
type: "button",
|
|
2295
|
+
className: "btn btn-primary",
|
|
2296
|
+
disabled: busy || !canMarkUploaded,
|
|
2297
|
+
title: canMarkUploaded ? "Segna upload completato" : "Serve waiting_files e almeno un file",
|
|
2298
|
+
onClick: () => void run(() => pipeline.markUploaded()),
|
|
2299
|
+
children: "2 \xB7 Upload completato"
|
|
2300
|
+
}
|
|
2301
|
+
),
|
|
2302
|
+
/* @__PURE__ */ jsx12(
|
|
2303
|
+
"button",
|
|
2304
|
+
{
|
|
2305
|
+
type: "button",
|
|
2306
|
+
className: "btn btn-primary",
|
|
2307
|
+
disabled: busy || !canParse,
|
|
2308
|
+
title: canParse ? "Lancia parsing server" : extraction === "extracting" ? "Estrazione ZIP in corso" : extraction === "failed" ? "Estrazione fallita" : "Serve stato uploaded",
|
|
2309
|
+
onClick: () => void run(() => pipeline.parse()),
|
|
2310
|
+
children: "3 \xB7 Parsing"
|
|
2311
|
+
}
|
|
2312
|
+
),
|
|
2313
|
+
/* @__PURE__ */ jsx12(
|
|
2314
|
+
"button",
|
|
2315
|
+
{
|
|
2316
|
+
type: "button",
|
|
2317
|
+
className: "btn btn-primary",
|
|
2318
|
+
disabled: busy || !canValidate,
|
|
2319
|
+
title: canValidate ? "Validazione server-only" : manifest?.readiness.missingRequirements?.length ? manifest.readiness.missingRequirements.join(" \xB7 ") : "Stato non ammesso per validazione",
|
|
2320
|
+
onClick: () => void run(() => pipeline.validate()),
|
|
2321
|
+
children: "4 \xB7 Valida acquisizione"
|
|
2322
|
+
}
|
|
2323
|
+
),
|
|
2324
|
+
/* @__PURE__ */ jsx12(
|
|
2325
|
+
"button",
|
|
2326
|
+
{
|
|
2327
|
+
type: "button",
|
|
2328
|
+
className: "btn btn-primary",
|
|
2329
|
+
disabled: busy || !canMarkReady,
|
|
2330
|
+
title: canMarkReady ? manifest?.validation.nextStepSemanticLabel ? readinessText(
|
|
2331
|
+
manifest.validation.nextStepSemanticLabel
|
|
2332
|
+
) : "Ready" : "Serve validated + validationResult valido dal server",
|
|
2333
|
+
onClick: () => void run(() => pipeline.markReady()),
|
|
2334
|
+
children: markReadyLabel
|
|
2335
|
+
}
|
|
2336
|
+
),
|
|
2337
|
+
/* @__PURE__ */ jsx12(
|
|
2338
|
+
"button",
|
|
2339
|
+
{
|
|
2340
|
+
type: "button",
|
|
2341
|
+
className: "btn btn-ghost",
|
|
2342
|
+
disabled: busy || !canArchive,
|
|
2343
|
+
title: canArchive ? "Archivia" : "Serve ready_for_next_step",
|
|
2344
|
+
onClick: () => void run(() => pipeline.archive()),
|
|
2345
|
+
children: "Archivia"
|
|
2346
|
+
}
|
|
2347
|
+
),
|
|
2348
|
+
/* @__PURE__ */ jsx12(
|
|
2349
|
+
"button",
|
|
2350
|
+
{
|
|
2351
|
+
type: "button",
|
|
2352
|
+
className: "btn btn-danger",
|
|
2353
|
+
disabled: busy || !canFail,
|
|
2354
|
+
title: canFail ? "Imposta stato failed" : "Non disponibile (terminale)",
|
|
2355
|
+
onClick: () => void run(
|
|
2356
|
+
() => pipeline.fail("Segnalazione errore da UI example")
|
|
2357
|
+
),
|
|
2358
|
+
children: "Segna failed"
|
|
2359
|
+
}
|
|
2360
|
+
)
|
|
2361
|
+
] })
|
|
2362
|
+
] }),
|
|
2363
|
+
/* @__PURE__ */ jsxs11("div", { className: "detail-layout", children: [
|
|
2364
|
+
/* @__PURE__ */ jsxs11("div", { children: [
|
|
2365
|
+
(doc.status === "draft" || doc.status === "waiting_files") && canMutate && /* @__PURE__ */ jsx12("div", { className: "panel", style: { marginBottom: "1rem" }, children: /* @__PURE__ */ jsx12(UploadPanel, { upload }) }),
|
|
2366
|
+
/* @__PURE__ */ jsxs11("div", { className: "panel", style: { marginBottom: "1rem" }, children: [
|
|
2367
|
+
/* @__PURE__ */ jsx12("h3", { style: { marginTop: 0 }, children: "File" }),
|
|
2368
|
+
!files?.length ? /* @__PURE__ */ jsx12("p", { className: "muted", children: "Nessun file associato. Usa l'upload reale quando la pipeline lo consente, oppure i mock dalla dashboard se l'MVP lo prevede." }) : /* @__PURE__ */ jsx12(
|
|
2369
|
+
FileList,
|
|
2370
|
+
{
|
|
2371
|
+
files,
|
|
2372
|
+
renderFileActions: renderFileListActions,
|
|
2373
|
+
onVisualizeMesh: (f) => onPreviewMeshIdChange(f._id)
|
|
2374
|
+
}
|
|
2375
|
+
)
|
|
2376
|
+
] }),
|
|
2377
|
+
doc.status === "needs_review" && canMutate && /* @__PURE__ */ jsx12("div", { className: "panel", style: { marginBottom: "1rem" }, children: /* @__PURE__ */ jsx12(
|
|
2378
|
+
ReviewPanel,
|
|
2379
|
+
{
|
|
2380
|
+
acquisitionId,
|
|
2381
|
+
review,
|
|
2382
|
+
files: files?.map((f) => ({
|
|
2383
|
+
_id: f._id,
|
|
2384
|
+
originalName: f.originalName,
|
|
2385
|
+
role: f.role,
|
|
2386
|
+
format: f.format,
|
|
2387
|
+
storageId: f.storageId
|
|
2388
|
+
})),
|
|
2389
|
+
onPreviewMesh: (id) => onPreviewMeshIdChange(id)
|
|
2390
|
+
},
|
|
2391
|
+
files?.map(
|
|
2392
|
+
(f) => `${f._id}:${f.role}`
|
|
2393
|
+
).join("|") ?? "none"
|
|
2394
|
+
) }),
|
|
2395
|
+
/* @__PURE__ */ jsxs11("div", { className: "panel", style: { marginBottom: "1rem" }, children: [
|
|
2396
|
+
/* @__PURE__ */ jsx12("h3", { style: { marginTop: 0 }, children: "Collegamento entit\xE0" }),
|
|
2397
|
+
/* @__PURE__ */ jsx12("p", { className: "muted", children: "Requisiti MVP: clinica richiede patientId + prescriptionId; lab richiede labWorkOrderId. Il server decide se passare a linked." }),
|
|
2398
|
+
/* @__PURE__ */ jsxs11("div", { className: "field-grid", children: [
|
|
2399
|
+
/* @__PURE__ */ jsxs11("div", { className: "field", children: [
|
|
2400
|
+
/* @__PURE__ */ jsx12("label", { children: "patientId" }),
|
|
2401
|
+
/* @__PURE__ */ jsx12(
|
|
2402
|
+
"input",
|
|
2403
|
+
{
|
|
2404
|
+
value: patientId,
|
|
2405
|
+
onChange: (e) => setPatientId(e.target.value),
|
|
2406
|
+
placeholder: doc.linkedEntity.patientId ?? ""
|
|
2407
|
+
}
|
|
2408
|
+
)
|
|
2409
|
+
] }),
|
|
2410
|
+
/* @__PURE__ */ jsxs11("div", { className: "field", children: [
|
|
2411
|
+
/* @__PURE__ */ jsx12("label", { children: "prescriptionId" }),
|
|
2412
|
+
/* @__PURE__ */ jsx12(
|
|
2413
|
+
"input",
|
|
2414
|
+
{
|
|
2415
|
+
value: prescriptionId,
|
|
2416
|
+
onChange: (e) => setPrescriptionId(e.target.value),
|
|
2417
|
+
placeholder: doc.linkedEntity.prescriptionId ?? ""
|
|
2418
|
+
}
|
|
2419
|
+
)
|
|
2420
|
+
] }),
|
|
2421
|
+
/* @__PURE__ */ jsxs11("div", { className: "field", children: [
|
|
2422
|
+
/* @__PURE__ */ jsx12("label", { children: "labWorkOrderId" }),
|
|
2423
|
+
/* @__PURE__ */ jsx12(
|
|
2424
|
+
"input",
|
|
2425
|
+
{
|
|
2426
|
+
value: labWorkOrderId,
|
|
2427
|
+
onChange: (e) => setLabWorkOrderId(e.target.value),
|
|
2428
|
+
placeholder: doc.linkedEntity.labWorkOrderId ?? ""
|
|
2429
|
+
}
|
|
2430
|
+
)
|
|
2431
|
+
] }),
|
|
2432
|
+
/* @__PURE__ */ jsxs11("div", { className: "field", children: [
|
|
2433
|
+
/* @__PURE__ */ jsx12("label", { children: "customerId" }),
|
|
2434
|
+
/* @__PURE__ */ jsx12(
|
|
2435
|
+
"input",
|
|
2436
|
+
{
|
|
2437
|
+
value: customerId,
|
|
2438
|
+
onChange: (e) => setCustomerId(e.target.value),
|
|
2439
|
+
placeholder: doc.linkedEntity.customerId ?? ""
|
|
2440
|
+
}
|
|
2441
|
+
)
|
|
2442
|
+
] }),
|
|
2443
|
+
/* @__PURE__ */ jsxs11("div", { className: "field", children: [
|
|
2444
|
+
/* @__PURE__ */ jsx12("label", { children: "externalCaseId" }),
|
|
2445
|
+
/* @__PURE__ */ jsx12(
|
|
2446
|
+
"input",
|
|
2447
|
+
{
|
|
2448
|
+
value: externalCaseId,
|
|
2449
|
+
onChange: (e) => setExternalCaseId(e.target.value),
|
|
2450
|
+
placeholder: doc.linkedEntity.externalCaseId ?? ""
|
|
2451
|
+
}
|
|
2452
|
+
)
|
|
2453
|
+
] })
|
|
2454
|
+
] }),
|
|
2455
|
+
/* @__PURE__ */ jsx12(
|
|
2456
|
+
"button",
|
|
2457
|
+
{
|
|
2458
|
+
type: "button",
|
|
2459
|
+
className: "btn btn-primary",
|
|
2460
|
+
style: { marginTop: "0.75rem" },
|
|
2461
|
+
disabled: busy || !canMutate,
|
|
2462
|
+
onClick: () => void run(
|
|
2463
|
+
() => pipeline.updateLinkedEntity({
|
|
2464
|
+
patientId: patientId || void 0,
|
|
2465
|
+
prescriptionId: prescriptionId || void 0,
|
|
2466
|
+
labWorkOrderId: labWorkOrderId || void 0,
|
|
2467
|
+
customerId: customerId || void 0,
|
|
2468
|
+
externalCaseId: externalCaseId || void 0
|
|
2469
|
+
})
|
|
2470
|
+
),
|
|
2471
|
+
children: "Aggiorna collegamenti"
|
|
2472
|
+
}
|
|
2473
|
+
)
|
|
2474
|
+
] }),
|
|
2475
|
+
/* @__PURE__ */ jsxs11("div", { className: "panel", children: [
|
|
2476
|
+
/* @__PURE__ */ jsx12("h3", { style: { marginTop: 0 }, children: "Manifest normalizzato (solo backend)" }),
|
|
2477
|
+
/* @__PURE__ */ jsx12("p", { className: "muted", style: { marginTop: 0 }, children: "Generato dalle query Convex del componente, non costruito in UI." }),
|
|
2478
|
+
/* @__PURE__ */ jsx12(ManifestViewer, { manifest })
|
|
2479
|
+
] })
|
|
2480
|
+
] }),
|
|
2481
|
+
/* @__PURE__ */ jsxs11("div", { children: [
|
|
2482
|
+
/* @__PURE__ */ jsxs11("div", { className: "panel", style: { marginBottom: "1rem" }, children: [
|
|
2483
|
+
/* @__PURE__ */ jsx12("h3", { style: { marginTop: 0 }, children: "Anteprima 3D" }),
|
|
2484
|
+
/* @__PURE__ */ jsxs11("p", { className: "muted", style: { marginTop: 0 }, children: [
|
|
2485
|
+
"Viewer Three.js orthodontic-style (ispirato a",
|
|
2486
|
+
" ",
|
|
2487
|
+
/* @__PURE__ */ jsx12(
|
|
2488
|
+
"a",
|
|
2489
|
+
{
|
|
2490
|
+
href: "https://github.com/Ellipsis1/DentalGeom",
|
|
2491
|
+
target: "_blank",
|
|
2492
|
+
rel: "noopener noreferrer",
|
|
2493
|
+
children: "DentalGeom"
|
|
2494
|
+
}
|
|
2495
|
+
),
|
|
2496
|
+
"); i dati restano su storage Convex via URL firmato."
|
|
2497
|
+
] }),
|
|
2498
|
+
viewableMeshCount === 0 ? /* @__PURE__ */ jsxs11("div", { className: "warning-box", style: { marginBottom: "0.75rem" }, children: [
|
|
2499
|
+
/* @__PURE__ */ jsx12("strong", { children: "Nessuna mesh visualizzabile (STL / PLY / OBJ)" }),
|
|
2500
|
+
/* @__PURE__ */ jsx12("div", { style: { marginTop: "0.35rem" }, children: proprietary3dFiles.length > 0 ? /* @__PURE__ */ jsxs11(Fragment3, { children: [
|
|
2501
|
+
"Questo pacchetto contiene ",
|
|
2502
|
+
proprietary3dFiles.length,
|
|
2503
|
+
" file 3D in ",
|
|
2504
|
+
/* @__PURE__ */ jsx12("strong", { children: "formati proprietari 3Shape" }),
|
|
2505
|
+
" (",
|
|
2506
|
+
/* @__PURE__ */ jsx12("code", { children: ".dcm" }),
|
|
2507
|
+
" \u2014 non DICOM standard \u2014 e",
|
|
2508
|
+
" ",
|
|
2509
|
+
/* @__PURE__ */ jsx12("code", { children: ".3ml" }),
|
|
2510
|
+
", spesso cifrati) che il viewer browser non pu\xF2 aprire. Per la preview 3D serve un export",
|
|
2511
|
+
" ",
|
|
2512
|
+
/* @__PURE__ */ jsx12("strong", { children: "STL/PLY/OBJ" }),
|
|
2513
|
+
" dal software CAD."
|
|
2514
|
+
] }) : /* @__PURE__ */ jsxs11(Fragment3, { children: [
|
|
2515
|
+
"Carica o estrai file ",
|
|
2516
|
+
/* @__PURE__ */ jsx12("strong", { children: "STL, PLY o OBJ" }),
|
|
2517
|
+
" per abilitare l'anteprima 3D."
|
|
2518
|
+
] }) })
|
|
2519
|
+
] }) : null,
|
|
2520
|
+
/* @__PURE__ */ jsx12(
|
|
2521
|
+
MeshFileSelector,
|
|
2522
|
+
{
|
|
2523
|
+
files: files ?? void 0,
|
|
2524
|
+
selectedId: previewMeshId,
|
|
2525
|
+
onSelect: (f) => onPreviewMeshIdChange(f._id)
|
|
2526
|
+
}
|
|
2527
|
+
),
|
|
2528
|
+
/* @__PURE__ */ jsx12(
|
|
2529
|
+
MeshViewer,
|
|
2530
|
+
{
|
|
2531
|
+
meshDownloadUrl,
|
|
2532
|
+
selectedFile: selectedMeshFile,
|
|
2533
|
+
segmentationBridge: meshSegmentationBridge
|
|
2534
|
+
}
|
|
2535
|
+
)
|
|
2536
|
+
] }),
|
|
2537
|
+
/* @__PURE__ */ jsxs11("div", { className: "panel", children: [
|
|
2538
|
+
/* @__PURE__ */ jsx12("h3", { style: { marginTop: 0 }, children: "Timeline eventi" }),
|
|
2539
|
+
/* @__PURE__ */ jsx12(EventTimeline, { events: events ?? void 0 })
|
|
2540
|
+
] })
|
|
2541
|
+
] })
|
|
2542
|
+
] })
|
|
2543
|
+
] });
|
|
2544
|
+
}
|
|
2545
|
+
export {
|
|
2546
|
+
CadProjectMetadataPanel,
|
|
2547
|
+
CaseSummaryCard,
|
|
2548
|
+
DigitalAcquisitionDetail,
|
|
2549
|
+
DigitalIntakeDashboard,
|
|
2550
|
+
EventTimeline,
|
|
2551
|
+
FileList,
|
|
2552
|
+
ManifestViewer,
|
|
2553
|
+
MeshFileSelector,
|
|
2554
|
+
MeshViewer,
|
|
2555
|
+
ReviewPanel,
|
|
2556
|
+
SmallBadge,
|
|
2557
|
+
StatusBadge,
|
|
2558
|
+
UploadPanel,
|
|
2559
|
+
defaultDashboardFilters,
|
|
2560
|
+
defaultProfileForOriginUi,
|
|
2561
|
+
hostAppLabel,
|
|
2562
|
+
originLabel,
|
|
2563
|
+
readinessLabelUi,
|
|
2564
|
+
validationProfileLabel,
|
|
2565
|
+
vendorLabel
|
|
2566
|
+
};
|
|
2567
|
+
//# sourceMappingURL=index.js.map
|