@giouwur/biometric-sdk 1.0.0
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/README.md +215 -0
- package/bin/init.js +50 -0
- package/dist/index.d.mts +569 -0
- package/dist/index.d.ts +569 -0
- package/dist/index.js +4725 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +4667 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +45 -0
- package/templates/test-page.tsx +1219 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4725 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
AuthService: () => AuthService,
|
|
34
|
+
BiometricProvider: () => BiometricProvider,
|
|
35
|
+
BiometricService: () => BiometricService,
|
|
36
|
+
BiometricSlot: () => BiometricSlot,
|
|
37
|
+
Button: () => Button,
|
|
38
|
+
Card: () => Card,
|
|
39
|
+
FINGER_NAMES: () => FINGER_NAMES,
|
|
40
|
+
FaceCameraModal: () => FaceCameraModal,
|
|
41
|
+
FaceEnrollModule: () => FaceEnrollModule,
|
|
42
|
+
FingerCaptureModal: () => FingerCaptureModal,
|
|
43
|
+
FingerEnrollModule: () => FingerEnrollModule,
|
|
44
|
+
FingerRollCaptureModal: () => FingerRollCaptureModal,
|
|
45
|
+
FingerRollEnrollModule: () => FingerRollEnrollModule,
|
|
46
|
+
FingerprintImpressionType: () => FingerprintImpressionType,
|
|
47
|
+
FingerprintPosition: () => FingerprintPosition,
|
|
48
|
+
Input: () => Input,
|
|
49
|
+
IrisCameraModal: () => IrisCameraModal,
|
|
50
|
+
IrisEnrollModule: () => IrisEnrollModule,
|
|
51
|
+
IrisPosition: () => IrisPosition,
|
|
52
|
+
LEFT_FINGERS: () => LEFT_FINGERS,
|
|
53
|
+
Loader: () => Loader,
|
|
54
|
+
MissingBiometricModal: () => MissingBiometricModal,
|
|
55
|
+
MissingFingerModal: () => MissingFingerModal,
|
|
56
|
+
Modal: () => Modal,
|
|
57
|
+
ModuleHeader: () => ModuleHeader,
|
|
58
|
+
PalmCaptureModal: () => PalmCaptureModal,
|
|
59
|
+
PalmEnrollModule: () => PalmEnrollModule,
|
|
60
|
+
ProgressBar: () => ProgressBar,
|
|
61
|
+
QualityBadge: () => QualityBadge,
|
|
62
|
+
RIGHT_FINGERS: () => RIGHT_FINGERS,
|
|
63
|
+
Select: () => Select,
|
|
64
|
+
StatusChip: () => StatusChip,
|
|
65
|
+
TableHeader: () => TableHeader,
|
|
66
|
+
TableRow: () => TableRow,
|
|
67
|
+
TableWrapper: () => TableWrapper,
|
|
68
|
+
Tabs: () => Tabs,
|
|
69
|
+
TabsContent: () => TabsContent,
|
|
70
|
+
TabsList: () => TabsList,
|
|
71
|
+
TabsTrigger: () => TabsTrigger,
|
|
72
|
+
api: () => api_default,
|
|
73
|
+
base64toBlob: () => base64toBlob,
|
|
74
|
+
canvasToBase64: () => canvasToBase64,
|
|
75
|
+
detectImageFormat: () => detectImageFormat,
|
|
76
|
+
downloadImageAsBase64: () => downloadImageAsBase64,
|
|
77
|
+
getImageSrcForDisplay: () => getImageSrcForDisplay,
|
|
78
|
+
isRemoteImage: () => isRemoteImage,
|
|
79
|
+
isValidBase64: () => isValidBase64,
|
|
80
|
+
normalizeImageForStorage: () => normalizeImageForStorage,
|
|
81
|
+
setApiBaseUrl: () => setApiBaseUrl,
|
|
82
|
+
useAuthStore: () => useAuthStore,
|
|
83
|
+
useBiometricConfig: () => useBiometricConfig,
|
|
84
|
+
useBiometricStore: () => useBiometricStore,
|
|
85
|
+
useEnrollmentStore: () => useEnrollmentStore,
|
|
86
|
+
useUiStore: () => useUiStore
|
|
87
|
+
});
|
|
88
|
+
module.exports = __toCommonJS(index_exports);
|
|
89
|
+
|
|
90
|
+
// src/components/BiometricProvider.tsx
|
|
91
|
+
var import_react = require("react");
|
|
92
|
+
|
|
93
|
+
// src/lib/stores/biometricStore.ts
|
|
94
|
+
var import_zustand2 = require("zustand");
|
|
95
|
+
|
|
96
|
+
// src/lib/stores/uiStore.ts
|
|
97
|
+
var import_zustand = require("zustand");
|
|
98
|
+
var MAX_MESSAGES = 100;
|
|
99
|
+
var useUiStore = (0, import_zustand.create)((set, get) => ({
|
|
100
|
+
messages: [],
|
|
101
|
+
isConsoleOpen: false,
|
|
102
|
+
/**
|
|
103
|
+
* Añade un nuevo mensaje de log a la consola global.
|
|
104
|
+
*/
|
|
105
|
+
addMessage: (message) => {
|
|
106
|
+
const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
107
|
+
const formattedMessage = `[${timestamp}] ${message}`;
|
|
108
|
+
set((state) => ({
|
|
109
|
+
messages: [
|
|
110
|
+
...state.messages.slice(
|
|
111
|
+
state.messages.length >= MAX_MESSAGES ? 1 : 0
|
|
112
|
+
),
|
|
113
|
+
formattedMessage
|
|
114
|
+
]
|
|
115
|
+
}));
|
|
116
|
+
},
|
|
117
|
+
/**
|
|
118
|
+
* Abre o cierra la consola de eventos.
|
|
119
|
+
*/
|
|
120
|
+
toggleConsole: () => {
|
|
121
|
+
set((state) => ({ isConsoleOpen: !state.isConsoleOpen }));
|
|
122
|
+
},
|
|
123
|
+
/**
|
|
124
|
+
* Limpia todos los mensajes de la consola.
|
|
125
|
+
*/
|
|
126
|
+
clearMessages: () => {
|
|
127
|
+
set({ messages: [] });
|
|
128
|
+
}
|
|
129
|
+
}));
|
|
130
|
+
|
|
131
|
+
// src/lib/stores/biometricStore.ts
|
|
132
|
+
var DEFAULT_WS_URL = "ws://127.0.0.1:5000/biometric";
|
|
133
|
+
var RECONNECT_DELAY = 3e3;
|
|
134
|
+
var getOrCreateSessionId = () => {
|
|
135
|
+
let sid = localStorage.getItem("bio_session_id");
|
|
136
|
+
if (!sid) {
|
|
137
|
+
sid = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `sess_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
138
|
+
localStorage.setItem("bio_session_id", sid);
|
|
139
|
+
}
|
|
140
|
+
return sid;
|
|
141
|
+
};
|
|
142
|
+
var useBiometricStore = (0, import_zustand2.create)((set, get) => ({
|
|
143
|
+
socket: null,
|
|
144
|
+
isConnected: false,
|
|
145
|
+
deviceStatus: null,
|
|
146
|
+
lastMessage: null,
|
|
147
|
+
handlers: /* @__PURE__ */ new Map(),
|
|
148
|
+
missingFingers: {},
|
|
149
|
+
markFingerAsMissing: (position, reason) => {
|
|
150
|
+
const { missingFingers, sendMessage, isConnected } = get();
|
|
151
|
+
const newMissing = { ...missingFingers, [position]: reason };
|
|
152
|
+
set({ missingFingers: newMissing });
|
|
153
|
+
if (isConnected) {
|
|
154
|
+
console.log("\u{1F4E4} Sincronizando dedos faltantes con Backend...", Object.keys(newMissing));
|
|
155
|
+
sendMessage({
|
|
156
|
+
messageType: "UPDATE_MISSING_BIOMETRICS",
|
|
157
|
+
payload: Object.keys(newMissing)
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
restoreFinger: (position) => {
|
|
162
|
+
const { missingFingers, sendMessage, isConnected } = get();
|
|
163
|
+
const newMissing = { ...missingFingers };
|
|
164
|
+
delete newMissing[position];
|
|
165
|
+
set({ missingFingers: newMissing });
|
|
166
|
+
if (isConnected) {
|
|
167
|
+
console.log("\u{1F4E4} Sincronizando dedos faltantes con Backend...", Object.keys(newMissing));
|
|
168
|
+
sendMessage({
|
|
169
|
+
messageType: "UPDATE_MISSING_BIOMETRICS",
|
|
170
|
+
payload: Object.keys(newMissing)
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
getExpectedFingers: (allPositions) => {
|
|
175
|
+
const { missingFingers } = get();
|
|
176
|
+
return allPositions.filter((pos) => !missingFingers[pos]);
|
|
177
|
+
},
|
|
178
|
+
clearAllMissingFingers: () => {
|
|
179
|
+
const { isConnected, sendMessage } = get();
|
|
180
|
+
set({ missingFingers: {} });
|
|
181
|
+
if (isConnected) {
|
|
182
|
+
console.log("\u{1F9F9} Limpiando todas las excepciones biom\xE9tricas...");
|
|
183
|
+
sendMessage({
|
|
184
|
+
messageType: "UPDATE_MISSING_BIOMETRICS",
|
|
185
|
+
payload: []
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
init: (wsUrl, deviceId) => {
|
|
190
|
+
if (wsUrl && typeof window !== "undefined") {
|
|
191
|
+
localStorage.setItem("biometric_ws_url", wsUrl);
|
|
192
|
+
}
|
|
193
|
+
if (deviceId && typeof window !== "undefined") {
|
|
194
|
+
localStorage.setItem("biometric_device_id", deviceId);
|
|
195
|
+
}
|
|
196
|
+
get().connect();
|
|
197
|
+
},
|
|
198
|
+
registerHandler: (messageType, handler) => {
|
|
199
|
+
get().handlers.set(messageType, handler);
|
|
200
|
+
},
|
|
201
|
+
unregisterHandler: (messageType) => {
|
|
202
|
+
get().handlers.delete(messageType);
|
|
203
|
+
},
|
|
204
|
+
sendMessage: (message) => {
|
|
205
|
+
const { socket, isConnected } = get();
|
|
206
|
+
if (socket && isConnected) {
|
|
207
|
+
socket.send(JSON.stringify(message));
|
|
208
|
+
} else {
|
|
209
|
+
useUiStore.getState().addMessage("Socket no conectado. Mensaje no enviado.");
|
|
210
|
+
console.error("Socket no conectado.");
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
disconnect: () => {
|
|
214
|
+
const { socket } = get();
|
|
215
|
+
if (socket) {
|
|
216
|
+
socket.close();
|
|
217
|
+
}
|
|
218
|
+
set({ isConnected: false, socket: null });
|
|
219
|
+
},
|
|
220
|
+
resetSession: () => {
|
|
221
|
+
localStorage.removeItem("bio_session_id");
|
|
222
|
+
get().disconnect();
|
|
223
|
+
setTimeout(() => get().connect(), 500);
|
|
224
|
+
},
|
|
225
|
+
connect: () => {
|
|
226
|
+
const { socket } = get();
|
|
227
|
+
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
let baseUrl = DEFAULT_WS_URL;
|
|
231
|
+
if (typeof window !== "undefined") {
|
|
232
|
+
const storedUrl = localStorage.getItem("biometric_ws_url");
|
|
233
|
+
if (storedUrl) {
|
|
234
|
+
baseUrl = storedUrl;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const sid = getOrCreateSessionId();
|
|
238
|
+
const finalUrl = `${baseUrl.replace(/\/$/, "")}?sid=${sid}`;
|
|
239
|
+
const log = useUiStore.getState().addMessage;
|
|
240
|
+
console.log(`Intentando conectar a: ${finalUrl}`);
|
|
241
|
+
const ws = new WebSocket(finalUrl);
|
|
242
|
+
ws.onopen = () => {
|
|
243
|
+
log(`\u{1F4E1} Conexi\xF3n establecida (Sesi\xF3n: ${sid.substring(0, 8)}...)`);
|
|
244
|
+
set({ isConnected: true, socket: ws });
|
|
245
|
+
const { missingFingers } = get();
|
|
246
|
+
const missingKeys = Object.keys(missingFingers);
|
|
247
|
+
if (missingKeys.length > 0) {
|
|
248
|
+
ws.send(JSON.stringify({
|
|
249
|
+
messageType: "UPDATE_MISSING_BIOMETRICS",
|
|
250
|
+
payload: missingKeys
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
ws.onclose = (event) => {
|
|
255
|
+
log(`\u{1F50C} Desconectado. C\xF3digo: ${event.code}. Reconectando en 3s...`);
|
|
256
|
+
set({ isConnected: false, socket: null });
|
|
257
|
+
if (event.code !== 1e3) {
|
|
258
|
+
setTimeout(() => {
|
|
259
|
+
get().connect();
|
|
260
|
+
}, RECONNECT_DELAY);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
ws.onerror = (error) => {
|
|
264
|
+
log("\u{1F4A5} Error en WebSocket. Ver consola.");
|
|
265
|
+
console.error("\u{1F4A5} Error en WebSocket:", error);
|
|
266
|
+
};
|
|
267
|
+
ws.onmessage = (event) => {
|
|
268
|
+
try {
|
|
269
|
+
const message = JSON.parse(event.data);
|
|
270
|
+
if (!message.messageType.includes("_FRAME")) {
|
|
271
|
+
set({ lastMessage: message });
|
|
272
|
+
}
|
|
273
|
+
if (message.messageType === "DEVICE_STATUS_UPDATE") {
|
|
274
|
+
set({ deviceStatus: message.payload });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const handler = get().handlers.get(message.messageType);
|
|
278
|
+
if (handler) {
|
|
279
|
+
handler(message);
|
|
280
|
+
} else {
|
|
281
|
+
if (message.messageType !== "FINGER_PREVIEW_FRAME" && message.messageType !== "FACE_QUALITY_FEEDBACK" && message.messageType !== "PALM_PREVIEW_FRAME" && message.messageType !== "IRIS_PREVIEW_FRAME" && message.messageType !== "CURRENT_CONFIG") {
|
|
282
|
+
log(`Mensaje recibido sin handler: ${message.messageType}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
} catch (error) {
|
|
286
|
+
log("Error al procesar mensaje.");
|
|
287
|
+
console.error("Error parsing JSON:", error);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
set({ socket: ws });
|
|
291
|
+
}
|
|
292
|
+
}));
|
|
293
|
+
|
|
294
|
+
// src/components/BiometricProvider.tsx
|
|
295
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
296
|
+
var BiometricContext = (0, import_react.createContext)(null);
|
|
297
|
+
function useBiometricConfig() {
|
|
298
|
+
const ctx = (0, import_react.useContext)(BiometricContext);
|
|
299
|
+
if (!ctx) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
"useBiometricConfig must be used within a <BiometricProvider>. Wrap your app with <BiometricProvider config={{...}}>."
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
return ctx;
|
|
305
|
+
}
|
|
306
|
+
function BiometricProvider({
|
|
307
|
+
config,
|
|
308
|
+
children
|
|
309
|
+
}) {
|
|
310
|
+
const initialized = (0, import_react.useRef)(false);
|
|
311
|
+
(0, import_react.useEffect)(() => {
|
|
312
|
+
if (!initialized.current) {
|
|
313
|
+
initialized.current = true;
|
|
314
|
+
useBiometricStore.getState().init(config.wsUrl, config.deviceId);
|
|
315
|
+
}
|
|
316
|
+
}, [config.wsUrl, config.deviceId]);
|
|
317
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BiometricContext.Provider, { value: config, children });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/components/enrollment/FingerEnrollModule.tsx
|
|
321
|
+
var import_react5 = require("react");
|
|
322
|
+
var import_react_hot_toast = require("react-hot-toast");
|
|
323
|
+
|
|
324
|
+
// src/lib/stores/enrollmentStore.ts
|
|
325
|
+
var import_zustand3 = require("zustand");
|
|
326
|
+
|
|
327
|
+
// src/types/abis-types.ts
|
|
328
|
+
var FingerprintPosition = /* @__PURE__ */ ((FingerprintPosition2) => {
|
|
329
|
+
FingerprintPosition2["RightThumb"] = "RightThumb";
|
|
330
|
+
FingerprintPosition2["RightIndex"] = "RightIndex";
|
|
331
|
+
FingerprintPosition2["RightMiddle"] = "RightMiddle";
|
|
332
|
+
FingerprintPosition2["RightRing"] = "RightRing";
|
|
333
|
+
FingerprintPosition2["RightLittle"] = "RightLittle";
|
|
334
|
+
FingerprintPosition2["LeftThumb"] = "LeftThumb";
|
|
335
|
+
FingerprintPosition2["LeftIndex"] = "LeftIndex";
|
|
336
|
+
FingerprintPosition2["LeftMiddle"] = "LeftMiddle";
|
|
337
|
+
FingerprintPosition2["LeftRing"] = "LeftRing";
|
|
338
|
+
FingerprintPosition2["LeftLittle"] = "LeftLittle";
|
|
339
|
+
FingerprintPosition2["RightUpperPalm"] = "RightUpperPalm";
|
|
340
|
+
FingerprintPosition2["RightLowerPalm"] = "RightLowerPalm";
|
|
341
|
+
FingerprintPosition2["RightWritersPalm"] = "RightWritersPalm";
|
|
342
|
+
FingerprintPosition2["LeftUpperPalm"] = "LeftUpperPalm";
|
|
343
|
+
FingerprintPosition2["LeftLowerPalm"] = "LeftLowerPalm";
|
|
344
|
+
FingerprintPosition2["LeftWritersPalm"] = "LeftWritersPalm";
|
|
345
|
+
return FingerprintPosition2;
|
|
346
|
+
})(FingerprintPosition || {});
|
|
347
|
+
var FingerprintImpressionType = /* @__PURE__ */ ((FingerprintImpressionType2) => {
|
|
348
|
+
FingerprintImpressionType2["LiveScanPlain"] = "LiveScanPlain";
|
|
349
|
+
FingerprintImpressionType2["LiveScanRolled"] = "LiveScanRolled";
|
|
350
|
+
FingerprintImpressionType2["RolledContact"] = "RolledContact";
|
|
351
|
+
FingerprintImpressionType2["NonLiveScanPlain"] = "NonLiveScanPlain";
|
|
352
|
+
FingerprintImpressionType2["NonLiveScanRolled"] = "NonLiveScanRolled";
|
|
353
|
+
FingerprintImpressionType2["LatentImpression"] = "LatentImpression";
|
|
354
|
+
FingerprintImpressionType2["LatentTracing"] = "LatentTracing";
|
|
355
|
+
FingerprintImpressionType2["LatentPhoto"] = "LatentPhoto";
|
|
356
|
+
FingerprintImpressionType2["LatentLift"] = "LatentLift";
|
|
357
|
+
FingerprintImpressionType2["LiveScanPalm"] = "LiveScanPalm";
|
|
358
|
+
FingerprintImpressionType2["NonLiveScanPalm"] = "NonLiveScanPalm";
|
|
359
|
+
FingerprintImpressionType2["LatentPalmImpression"] = "LatentPalmImpression";
|
|
360
|
+
FingerprintImpressionType2["LatentPalmTracing"] = "LatentPalmTracing";
|
|
361
|
+
FingerprintImpressionType2["LatentPalmPhoto"] = "LatentPalmPhoto";
|
|
362
|
+
FingerprintImpressionType2["LatentPalmLift"] = "LatentPalmLift";
|
|
363
|
+
FingerprintImpressionType2["LiveScanOpticalContactPlain"] = "LiveScanOpticalContactPlain";
|
|
364
|
+
FingerprintImpressionType2["LiveScanOpticalContactRolled"] = "LiveScanOpticalContactRolled";
|
|
365
|
+
FingerprintImpressionType2["LiveScanOpticalContactlessPlain"] = "LiveScanOpticalContactlessPlain";
|
|
366
|
+
FingerprintImpressionType2["LiveScanOpticalContactlessRolled"] = "LiveScanOpticalContactlessRolled";
|
|
367
|
+
FingerprintImpressionType2["LiveScanCapacitivePlain"] = "LiveScanCapacitivePlain";
|
|
368
|
+
FingerprintImpressionType2["LiveScanCapacitiveRolled"] = "LiveScanCapacitiveRolled";
|
|
369
|
+
FingerprintImpressionType2["LiveScanUltrasonicPlain"] = "LiveScanUltrasonicPlain";
|
|
370
|
+
FingerprintImpressionType2["LiveScanUltrasonicRolled"] = "LiveScanUltrasonicRolled";
|
|
371
|
+
FingerprintImpressionType2["LiveScanThermalPlain"] = "LiveScanThermalPlain";
|
|
372
|
+
FingerprintImpressionType2["LiveScanThermalRolled"] = "LiveScanThermalRolled";
|
|
373
|
+
FingerprintImpressionType2["LiveScanVerticalSwipe"] = "LiveScanVerticalSwipe";
|
|
374
|
+
FingerprintImpressionType2["LiveScanOpticalPalm"] = "LiveScanOpticalPalm";
|
|
375
|
+
FingerprintImpressionType2["NonLiveScanOpticalPalm"] = "NonLiveScanOpticalPalm";
|
|
376
|
+
FingerprintImpressionType2["Swipe"] = "Swipe";
|
|
377
|
+
FingerprintImpressionType2["LiveScanContactless"] = "LiveScanContactless";
|
|
378
|
+
return FingerprintImpressionType2;
|
|
379
|
+
})(FingerprintImpressionType || {});
|
|
380
|
+
var IrisPosition = /* @__PURE__ */ ((IrisPosition2) => {
|
|
381
|
+
IrisPosition2["Right"] = "Right";
|
|
382
|
+
IrisPosition2["Left"] = "Left";
|
|
383
|
+
return IrisPosition2;
|
|
384
|
+
})(IrisPosition || {});
|
|
385
|
+
|
|
386
|
+
// src/lib/utils/imageHelpers.ts
|
|
387
|
+
function normalizeImageForStorage(imageString) {
|
|
388
|
+
if (!imageString) return "";
|
|
389
|
+
if (imageString.startsWith("http://") || imageString.startsWith("https://")) {
|
|
390
|
+
return imageString;
|
|
391
|
+
}
|
|
392
|
+
if (imageString.includes("data:image") && imageString.includes(",")) {
|
|
393
|
+
const parts = imageString.split(",");
|
|
394
|
+
return parts.length > 1 ? parts[1] : imageString;
|
|
395
|
+
}
|
|
396
|
+
return imageString;
|
|
397
|
+
}
|
|
398
|
+
function getImageSrcForDisplay(imageString, format = "png") {
|
|
399
|
+
if (!imageString) return void 0;
|
|
400
|
+
if (imageString.startsWith("http://") || imageString.startsWith("https://")) {
|
|
401
|
+
return imageString;
|
|
402
|
+
}
|
|
403
|
+
if (imageString.startsWith("data:image")) {
|
|
404
|
+
return imageString;
|
|
405
|
+
}
|
|
406
|
+
return `data:image/${format};base64,${imageString}`;
|
|
407
|
+
}
|
|
408
|
+
function isRemoteImage(imageString) {
|
|
409
|
+
if (!imageString) return false;
|
|
410
|
+
return imageString.startsWith("http://") || imageString.startsWith("https://");
|
|
411
|
+
}
|
|
412
|
+
async function downloadImageAsBase64(url) {
|
|
413
|
+
const response = await fetch(url);
|
|
414
|
+
if (!response.ok) {
|
|
415
|
+
throw new Error(`Failed to download image: ${response.status} ${response.statusText}`);
|
|
416
|
+
}
|
|
417
|
+
const blob = await response.blob();
|
|
418
|
+
return new Promise((resolve, reject) => {
|
|
419
|
+
const reader = new FileReader();
|
|
420
|
+
reader.onloadend = () => {
|
|
421
|
+
const dataUrl = reader.result;
|
|
422
|
+
const base64 = dataUrl.split(",")[1] || "";
|
|
423
|
+
resolve(base64);
|
|
424
|
+
};
|
|
425
|
+
reader.onerror = reject;
|
|
426
|
+
reader.readAsDataURL(blob);
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
function canvasToBase64(canvas, format = "image/jpeg", quality = 0.95) {
|
|
430
|
+
const dataUrl = canvas.toDataURL(format, quality);
|
|
431
|
+
return dataUrl.split(",")[1] || "";
|
|
432
|
+
}
|
|
433
|
+
function base64toBlob(base64Data) {
|
|
434
|
+
try {
|
|
435
|
+
const sliceSize = 512;
|
|
436
|
+
const byteCharacters = atob(base64Data);
|
|
437
|
+
const byteArrays = [];
|
|
438
|
+
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
|
|
439
|
+
const slice = byteCharacters.slice(offset, offset + sliceSize);
|
|
440
|
+
const byteNumbers = new Array(slice.length);
|
|
441
|
+
for (let i = 0; i < slice.length; i++) {
|
|
442
|
+
byteNumbers[i] = slice.charCodeAt(i);
|
|
443
|
+
}
|
|
444
|
+
byteArrays.push(new Uint8Array(byteNumbers));
|
|
445
|
+
}
|
|
446
|
+
return new Blob(byteArrays, { type: "application/octet-stream" });
|
|
447
|
+
} catch (error) {
|
|
448
|
+
console.error("Error converting base64 to blob:", error);
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function detectImageFormat(base64Data) {
|
|
453
|
+
if (!base64Data) return "unknown";
|
|
454
|
+
const cleanData = normalizeImageForStorage(base64Data);
|
|
455
|
+
if (!cleanData) return "unknown";
|
|
456
|
+
try {
|
|
457
|
+
if (cleanData.startsWith("iVBOR")) return "image/png";
|
|
458
|
+
if (cleanData.startsWith("/9j/")) return "image/jpeg";
|
|
459
|
+
if (cleanData.startsWith("R0lGO")) return "image/gif";
|
|
460
|
+
if (cleanData.startsWith("Qk")) return "image/bmp";
|
|
461
|
+
if (cleanData.includes("V0VCU")) return "image/webp";
|
|
462
|
+
if (cleanData.startsWith("AAAA")) return "image/jp2";
|
|
463
|
+
return "unknown";
|
|
464
|
+
} catch (error) {
|
|
465
|
+
console.error("Error detecting image format:", error);
|
|
466
|
+
return "unknown";
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function isValidBase64(str) {
|
|
470
|
+
if (!str || typeof str !== "string") return false;
|
|
471
|
+
try {
|
|
472
|
+
const cleaned = normalizeImageForStorage(str);
|
|
473
|
+
atob(cleaned);
|
|
474
|
+
return true;
|
|
475
|
+
} catch {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// src/lib/stores/enrollmentStore.ts
|
|
481
|
+
var FINGER_MAP_INV = {
|
|
482
|
+
RightThumb: "RIGHT_THUMB",
|
|
483
|
+
RightIndex: "RIGHT_INDEX",
|
|
484
|
+
RightMiddle: "RIGHT_MIDDLE",
|
|
485
|
+
RightRing: "RIGHT_RING",
|
|
486
|
+
RightLittle: "RIGHT_LITTLE",
|
|
487
|
+
LeftThumb: "LEFT_THUMB",
|
|
488
|
+
LeftIndex: "LEFT_INDEX",
|
|
489
|
+
LeftMiddle: "LEFT_MIDDLE",
|
|
490
|
+
LeftRing: "LEFT_RING",
|
|
491
|
+
LeftLittle: "LEFT_LITTLE"
|
|
492
|
+
};
|
|
493
|
+
var PALM_MAP_INV = {
|
|
494
|
+
RightUpperPalm: "UPPERPALMRIGHT",
|
|
495
|
+
RightLowerPalm: "LOWERPALMRIGHT",
|
|
496
|
+
RightWritersPalm: "WRITERSPALMRIGHT",
|
|
497
|
+
LeftUpperPalm: "UPPERPALMLEFT",
|
|
498
|
+
LeftLowerPalm: "LOWERPALMLEFT",
|
|
499
|
+
LeftWritersPalm: "WRITERSPALMLEFT"
|
|
500
|
+
};
|
|
501
|
+
var FINGER_MAP = {
|
|
502
|
+
RIGHT_THUMB: "RightThumb" /* RightThumb */,
|
|
503
|
+
RIGHT_INDEX: "RightIndex" /* RightIndex */,
|
|
504
|
+
RIGHT_MIDDLE: "RightMiddle" /* RightMiddle */,
|
|
505
|
+
RIGHT_RING: "RightRing" /* RightRing */,
|
|
506
|
+
RIGHT_LITTLE: "RightLittle" /* RightLittle */,
|
|
507
|
+
LEFT_THUMB: "LeftThumb" /* LeftThumb */,
|
|
508
|
+
LEFT_INDEX: "LeftIndex" /* LeftIndex */,
|
|
509
|
+
LEFT_MIDDLE: "LeftMiddle" /* LeftMiddle */,
|
|
510
|
+
LEFT_RING: "LeftRing" /* LeftRing */,
|
|
511
|
+
LEFT_LITTLE: "LeftLittle" /* LeftLittle */
|
|
512
|
+
};
|
|
513
|
+
var PALM_MAP = {
|
|
514
|
+
UPPERPALMLEFT: "LeftUpperPalm" /* LeftUpperPalm */,
|
|
515
|
+
LOWERPALMLEFT: "LeftLowerPalm" /* LeftLowerPalm */,
|
|
516
|
+
WRITERSPALMLEFT: "LeftWritersPalm" /* LeftWritersPalm */,
|
|
517
|
+
UPPERPALMRIGHT: "RightUpperPalm" /* RightUpperPalm */,
|
|
518
|
+
LOWERPALMRIGHT: "RightLowerPalm" /* RightLowerPalm */,
|
|
519
|
+
WRITERSPALMRIGHT: "RightWritersPalm" /* RightWritersPalm */
|
|
520
|
+
};
|
|
521
|
+
var useEnrollmentStore = (0, import_zustand3.create)((set, get) => ({
|
|
522
|
+
externalId: "",
|
|
523
|
+
fullName: "",
|
|
524
|
+
isUpdateMode: false,
|
|
525
|
+
isLoading: false,
|
|
526
|
+
fingerprints: {},
|
|
527
|
+
rolledFingerprints: {},
|
|
528
|
+
missingFingers: {},
|
|
529
|
+
palms: {},
|
|
530
|
+
missingPalms: {},
|
|
531
|
+
faces: [],
|
|
532
|
+
faceCaptures: Array(5).fill(null),
|
|
533
|
+
missingFaces: {},
|
|
534
|
+
irises: {},
|
|
535
|
+
missingIrises: {},
|
|
536
|
+
_originalSnapshot: null,
|
|
537
|
+
setDemographics: (id, name) => set({ externalId: id, fullName: name }),
|
|
538
|
+
addFingerprint: (pos, image, quality = 100) => set((state) => {
|
|
539
|
+
const nextMissing = { ...state.missingFingers };
|
|
540
|
+
delete nextMissing[pos];
|
|
541
|
+
return {
|
|
542
|
+
fingerprints: {
|
|
543
|
+
...state.fingerprints,
|
|
544
|
+
[pos]: { image, quality, impressionType: "LiveScanPlain" }
|
|
545
|
+
},
|
|
546
|
+
missingFingers: nextMissing
|
|
547
|
+
};
|
|
548
|
+
}),
|
|
549
|
+
addRolledFingerprint: (pos, image, quality = 100) => set((state) => {
|
|
550
|
+
const nextMissing = { ...state.missingFingers };
|
|
551
|
+
delete nextMissing[pos];
|
|
552
|
+
return {
|
|
553
|
+
rolledFingerprints: {
|
|
554
|
+
...state.rolledFingerprints,
|
|
555
|
+
[pos]: { image, quality, impressionType: "RolledContact" }
|
|
556
|
+
},
|
|
557
|
+
missingFingers: nextMissing
|
|
558
|
+
};
|
|
559
|
+
}),
|
|
560
|
+
removeFingerprint: (pos) => set((state) => {
|
|
561
|
+
const next = { ...state.fingerprints };
|
|
562
|
+
delete next[pos];
|
|
563
|
+
return { fingerprints: next };
|
|
564
|
+
}),
|
|
565
|
+
removeRolledFingerprint: (pos) => set((state) => {
|
|
566
|
+
const next = { ...state.rolledFingerprints };
|
|
567
|
+
delete next[pos];
|
|
568
|
+
return { rolledFingerprints: next };
|
|
569
|
+
}),
|
|
570
|
+
addMissingFinger: (pos, reason) => set((state) => {
|
|
571
|
+
const nextFingers = { ...state.fingerprints };
|
|
572
|
+
const nextRolled = { ...state.rolledFingerprints };
|
|
573
|
+
delete nextFingers[pos];
|
|
574
|
+
delete nextRolled[pos];
|
|
575
|
+
return {
|
|
576
|
+
missingFingers: { ...state.missingFingers, [pos]: reason },
|
|
577
|
+
fingerprints: nextFingers,
|
|
578
|
+
rolledFingerprints: nextRolled
|
|
579
|
+
};
|
|
580
|
+
}),
|
|
581
|
+
removeMissingFinger: (pos) => set((state) => {
|
|
582
|
+
const next = { ...state.missingFingers };
|
|
583
|
+
delete next[pos];
|
|
584
|
+
return { missingFingers: next };
|
|
585
|
+
}),
|
|
586
|
+
addPalm: (pos, image, quality = 100) => set((state) => {
|
|
587
|
+
const nextMissing = { ...state.missingPalms };
|
|
588
|
+
delete nextMissing[pos];
|
|
589
|
+
return {
|
|
590
|
+
palms: {
|
|
591
|
+
...state.palms,
|
|
592
|
+
[pos]: { image, quality, impressionType: "LiveScanPalm" }
|
|
593
|
+
},
|
|
594
|
+
missingPalms: nextMissing
|
|
595
|
+
};
|
|
596
|
+
}),
|
|
597
|
+
removePalm: (pos) => set((state) => {
|
|
598
|
+
const next = { ...state.palms };
|
|
599
|
+
delete next[pos];
|
|
600
|
+
return { palms: next };
|
|
601
|
+
}),
|
|
602
|
+
addMissingPalm: (pos, reason) => set((state) => {
|
|
603
|
+
const nextPalms = { ...state.palms };
|
|
604
|
+
delete nextPalms[pos];
|
|
605
|
+
return {
|
|
606
|
+
missingPalms: { ...state.missingPalms, [pos]: reason },
|
|
607
|
+
palms: nextPalms
|
|
608
|
+
};
|
|
609
|
+
}),
|
|
610
|
+
removeMissingPalm: (pos) => set((state) => {
|
|
611
|
+
const next = { ...state.missingPalms };
|
|
612
|
+
delete next[pos];
|
|
613
|
+
return { missingPalms: next };
|
|
614
|
+
}),
|
|
615
|
+
setFaces: (items) => set({ faces: items }),
|
|
616
|
+
setFaceCaptures: (items) => set({ faceCaptures: items }),
|
|
617
|
+
removeFace: (index) => set((state) => {
|
|
618
|
+
const nextCaptures = [...state.faceCaptures];
|
|
619
|
+
nextCaptures[index] = null;
|
|
620
|
+
const nextFaces = state.faces.filter((_, i) => i !== index);
|
|
621
|
+
return { faceCaptures: nextCaptures, faces: nextFaces };
|
|
622
|
+
}),
|
|
623
|
+
addMissingFace: (index, reason) => set((state) => {
|
|
624
|
+
const nextCaptures = [...state.faceCaptures];
|
|
625
|
+
nextCaptures[index] = null;
|
|
626
|
+
const nextFaces = state.faces.filter((_, i) => i !== index);
|
|
627
|
+
return {
|
|
628
|
+
missingFaces: { ...state.missingFaces, [index]: reason },
|
|
629
|
+
faceCaptures: nextCaptures,
|
|
630
|
+
faces: nextFaces
|
|
631
|
+
};
|
|
632
|
+
}),
|
|
633
|
+
removeMissingFace: (index) => set((state) => {
|
|
634
|
+
const next = { ...state.missingFaces };
|
|
635
|
+
delete next[index];
|
|
636
|
+
return { missingFaces: next };
|
|
637
|
+
}),
|
|
638
|
+
setIrises: (right, left) => set((state) => {
|
|
639
|
+
const nextMissing = { ...state.missingIrises };
|
|
640
|
+
if (right) delete nextMissing.right;
|
|
641
|
+
if (left) delete nextMissing.left;
|
|
642
|
+
return { irises: { right, left }, missingIrises: nextMissing };
|
|
643
|
+
}),
|
|
644
|
+
removeIris: (side) => set((state) => {
|
|
645
|
+
const next = { ...state.irises };
|
|
646
|
+
if (side === "left") delete next.left;
|
|
647
|
+
if (side === "right") delete next.right;
|
|
648
|
+
return { irises: next };
|
|
649
|
+
}),
|
|
650
|
+
addMissingIris: (side, reason) => set((state) => {
|
|
651
|
+
const nextIrises = { ...state.irises };
|
|
652
|
+
if (side === "left") delete nextIrises.left;
|
|
653
|
+
if (side === "right") delete nextIrises.right;
|
|
654
|
+
return {
|
|
655
|
+
missingIrises: { ...state.missingIrises, [side]: reason },
|
|
656
|
+
irises: nextIrises
|
|
657
|
+
};
|
|
658
|
+
}),
|
|
659
|
+
removeMissingIris: (side) => set((state) => {
|
|
660
|
+
const next = { ...state.missingIrises };
|
|
661
|
+
delete next[side];
|
|
662
|
+
return { missingIrises: next };
|
|
663
|
+
}),
|
|
664
|
+
resetEnrollment: () => set({
|
|
665
|
+
externalId: "",
|
|
666
|
+
fullName: "",
|
|
667
|
+
isUpdateMode: false,
|
|
668
|
+
fingerprints: {},
|
|
669
|
+
rolledFingerprints: {},
|
|
670
|
+
missingFingers: {},
|
|
671
|
+
palms: {},
|
|
672
|
+
missingPalms: {},
|
|
673
|
+
faces: [],
|
|
674
|
+
faceCaptures: Array(5).fill(null),
|
|
675
|
+
missingFaces: {},
|
|
676
|
+
irises: {},
|
|
677
|
+
missingIrises: {},
|
|
678
|
+
_originalSnapshot: null
|
|
679
|
+
}),
|
|
680
|
+
loadApplicantData: async (data) => {
|
|
681
|
+
set({ isLoading: true });
|
|
682
|
+
try {
|
|
683
|
+
const updates = {
|
|
684
|
+
externalId: data.externalId,
|
|
685
|
+
fullName: data.customDetails?.fullName || "Solicitante",
|
|
686
|
+
isUpdateMode: true,
|
|
687
|
+
fingerprints: {},
|
|
688
|
+
rolledFingerprints: {},
|
|
689
|
+
missingFingers: {},
|
|
690
|
+
palms: {},
|
|
691
|
+
missingPalms: {},
|
|
692
|
+
faces: [],
|
|
693
|
+
faceCaptures: Array(5).fill(null),
|
|
694
|
+
missingFaces: {},
|
|
695
|
+
irises: {},
|
|
696
|
+
missingIrises: {}
|
|
697
|
+
};
|
|
698
|
+
if (data.fingerprintModality?.fingerprints) {
|
|
699
|
+
for (const fp of data.fingerprintModality.fingerprints) {
|
|
700
|
+
let internalPos = FINGER_MAP_INV[fp.position];
|
|
701
|
+
let isPalm = false;
|
|
702
|
+
if (!internalPos) {
|
|
703
|
+
internalPos = PALM_MAP_INV[fp.position];
|
|
704
|
+
isPalm = true;
|
|
705
|
+
}
|
|
706
|
+
if (!internalPos) continue;
|
|
707
|
+
let rawImage = fp.image.dataBytes || "";
|
|
708
|
+
if (!rawImage && fp.image.dataUrl && isRemoteImage(fp.image.dataUrl)) {
|
|
709
|
+
try {
|
|
710
|
+
rawImage = await downloadImageAsBase64(fp.image.dataUrl);
|
|
711
|
+
} catch (e) {
|
|
712
|
+
console.warn(`No se pudo descargar imagen de ${fp.position}:`, e);
|
|
713
|
+
rawImage = fp.image.dataUrl;
|
|
714
|
+
}
|
|
715
|
+
} else if (!rawImage && fp.image.dataUrl) {
|
|
716
|
+
rawImage = fp.image.dataUrl;
|
|
717
|
+
}
|
|
718
|
+
const imageSrc = normalizeImageForStorage(rawImage);
|
|
719
|
+
const qualityVal = fp.display?.quality ?? fp.image.qualities?.placement ?? fp.image.qualities?.imageQuality ?? 0;
|
|
720
|
+
const impression = fp.impressionType || "Unknown";
|
|
721
|
+
const item = {
|
|
722
|
+
image: imageSrc,
|
|
723
|
+
quality: qualityVal,
|
|
724
|
+
impressionType: impression
|
|
725
|
+
};
|
|
726
|
+
if (isPalm) {
|
|
727
|
+
updates.palms[internalPos] = item;
|
|
728
|
+
} else {
|
|
729
|
+
const impressionLower = impression.toLowerCase();
|
|
730
|
+
const isRolled = impressionLower.includes("rolled");
|
|
731
|
+
if (isRolled) {
|
|
732
|
+
updates.rolledFingerprints[internalPos] = item;
|
|
733
|
+
} else {
|
|
734
|
+
updates.fingerprints[internalPos] = item;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (data.fingerprintModality?.missingFingerprints) {
|
|
740
|
+
data.fingerprintModality.missingFingerprints.forEach((mf) => {
|
|
741
|
+
const fingerPos = FINGER_MAP_INV[mf.position];
|
|
742
|
+
if (fingerPos) {
|
|
743
|
+
updates.missingFingers[fingerPos] = mf.missingReasonText || "Amputaci\xF3n";
|
|
744
|
+
if (updates.fingerprints) delete updates.fingerprints[fingerPos];
|
|
745
|
+
if (updates.rolledFingerprints) delete updates.rolledFingerprints[fingerPos];
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const palmPos = PALM_MAP_INV[mf.position];
|
|
749
|
+
if (palmPos) {
|
|
750
|
+
updates.missingPalms[palmPos] = mf.missingReasonText || "No capturable";
|
|
751
|
+
if (updates.palms) delete updates.palms[palmPos];
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
if (data.faceModality?.faces) {
|
|
756
|
+
const validFaces = data.faceModality.faces.filter(
|
|
757
|
+
(f) => f.template && f.image?.qualities?.valid === true
|
|
758
|
+
);
|
|
759
|
+
const latestFaces = validFaces.slice(-5);
|
|
760
|
+
const loadedFaces = [];
|
|
761
|
+
for (const f of latestFaces) {
|
|
762
|
+
let rawImage = f.image.dataBytes || "";
|
|
763
|
+
if (!rawImage && f.image.dataUrl && isRemoteImage(f.image.dataUrl)) {
|
|
764
|
+
try {
|
|
765
|
+
rawImage = await downloadImageAsBase64(f.image.dataUrl);
|
|
766
|
+
} catch (e) {
|
|
767
|
+
console.warn("No se pudo descargar imagen de rostro:", e);
|
|
768
|
+
rawImage = f.image.dataUrl;
|
|
769
|
+
}
|
|
770
|
+
} else if (!rawImage && f.image.dataUrl) {
|
|
771
|
+
rawImage = f.image.dataUrl;
|
|
772
|
+
}
|
|
773
|
+
const rawQuality = f.template?.qualities?.quality || 0;
|
|
774
|
+
const normalizedQuality = Math.min(Math.round(rawQuality / 2), 100);
|
|
775
|
+
loadedFaces.push({
|
|
776
|
+
image: normalizeImageForStorage(rawImage),
|
|
777
|
+
quality: normalizedQuality
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
updates.faces = loadedFaces;
|
|
781
|
+
const newCaptures = Array(5).fill(null);
|
|
782
|
+
loadedFaces.forEach((face, index) => {
|
|
783
|
+
if (index < 5) newCaptures[index] = face;
|
|
784
|
+
});
|
|
785
|
+
updates.faceCaptures = newCaptures;
|
|
786
|
+
}
|
|
787
|
+
if (data.irisModality?.irises) {
|
|
788
|
+
const newIrises = {};
|
|
789
|
+
for (const iris of data.irisModality.irises) {
|
|
790
|
+
let rawImage = iris.image.dataBytes || "";
|
|
791
|
+
if (!rawImage && iris.image.dataUrl && isRemoteImage(iris.image.dataUrl)) {
|
|
792
|
+
try {
|
|
793
|
+
rawImage = await downloadImageAsBase64(iris.image.dataUrl);
|
|
794
|
+
} catch (e) {
|
|
795
|
+
console.warn(
|
|
796
|
+
`No se pudo descargar imagen de iris ${iris.position}:`,
|
|
797
|
+
e
|
|
798
|
+
);
|
|
799
|
+
rawImage = iris.image.dataUrl;
|
|
800
|
+
}
|
|
801
|
+
} else if (!rawImage && iris.image.dataUrl) {
|
|
802
|
+
rawImage = iris.image.dataUrl;
|
|
803
|
+
}
|
|
804
|
+
const item = {
|
|
805
|
+
image: normalizeImageForStorage(rawImage),
|
|
806
|
+
quality: iris.image.qualities?.quality || 100
|
|
807
|
+
};
|
|
808
|
+
if (iris.position === "Right") newIrises.right = item;
|
|
809
|
+
if (iris.position === "Left") newIrises.left = item;
|
|
810
|
+
}
|
|
811
|
+
updates.irises = newIrises;
|
|
812
|
+
}
|
|
813
|
+
if (data.irisModality?.missingIrises) {
|
|
814
|
+
data.irisModality.missingIrises.forEach((mi) => {
|
|
815
|
+
if (mi.position === "Right") {
|
|
816
|
+
updates.missingIrises["right"] = mi.missingReasonText || "No capturable";
|
|
817
|
+
if (updates.irises) delete updates.irises.right;
|
|
818
|
+
}
|
|
819
|
+
if (mi.position === "Left") {
|
|
820
|
+
updates.missingIrises["left"] = mi.missingReasonText || "No capturable";
|
|
821
|
+
if (updates.irises) delete updates.irises.left;
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
if (data.faceModality?.missingFaces) {
|
|
826
|
+
data.faceModality.missingFaces.forEach((mf) => {
|
|
827
|
+
updates.missingFaces[mf.index] = mf.missingReasonText || "No capturable";
|
|
828
|
+
if (updates.faceCaptures) {
|
|
829
|
+
updates.faceCaptures[mf.index] = null;
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
if (updates.faces && updates.faces.length > 0) {
|
|
833
|
+
updates.faces = updates.faceCaptures.filter((item) => item !== null);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
const snapshot = {
|
|
837
|
+
fingerprints: { ...updates.fingerprints },
|
|
838
|
+
rolledFingerprints: { ...updates.rolledFingerprints },
|
|
839
|
+
missingFingers: { ...updates.missingFingers },
|
|
840
|
+
palms: { ...updates.palms },
|
|
841
|
+
missingPalms: { ...updates.missingPalms },
|
|
842
|
+
faces: [...updates.faces || []],
|
|
843
|
+
faceCaptures: [...updates.faceCaptures || Array(5).fill(null)],
|
|
844
|
+
missingFaces: { ...updates.missingFaces },
|
|
845
|
+
irises: { ...updates.irises },
|
|
846
|
+
missingIrises: { ...updates.missingIrises }
|
|
847
|
+
};
|
|
848
|
+
set((state) => ({
|
|
849
|
+
...state,
|
|
850
|
+
...updates,
|
|
851
|
+
_originalSnapshot: snapshot,
|
|
852
|
+
isLoading: false
|
|
853
|
+
}));
|
|
854
|
+
} catch (error) {
|
|
855
|
+
console.error("Error loading applicant data:", error);
|
|
856
|
+
} finally {
|
|
857
|
+
set({ isLoading: false });
|
|
858
|
+
}
|
|
859
|
+
},
|
|
860
|
+
buildEnrollmentRequest: () => {
|
|
861
|
+
const state = get();
|
|
862
|
+
const plainFingerprints = Object.entries(state.fingerprints).map(([key, item]) => ({
|
|
863
|
+
position: FINGER_MAP[key],
|
|
864
|
+
impressionType: "LiveScanPlain" /* LiveScanPlain */,
|
|
865
|
+
image: { dataBytes: normalizeImageForStorage(item.image) }
|
|
866
|
+
})).filter((f) => f.position);
|
|
867
|
+
const rolledFingerprints = Object.entries(state.rolledFingerprints).map(([key, item]) => ({
|
|
868
|
+
position: FINGER_MAP[key],
|
|
869
|
+
impressionType: "RolledContact" /* RolledContact */,
|
|
870
|
+
image: { dataBytes: normalizeImageForStorage(item.image) }
|
|
871
|
+
})).filter((f) => f.position);
|
|
872
|
+
const palmFingerprints = Object.entries(state.palms).map(([key, item]) => ({
|
|
873
|
+
position: PALM_MAP[key],
|
|
874
|
+
impressionType: "LiveScanPlain" /* LiveScanPlain */,
|
|
875
|
+
image: { dataBytes: normalizeImageForStorage(item.image) }
|
|
876
|
+
})).filter((f) => f.position);
|
|
877
|
+
const allFrictionRidge = [
|
|
878
|
+
...plainFingerprints,
|
|
879
|
+
...rolledFingerprints,
|
|
880
|
+
...palmFingerprints
|
|
881
|
+
];
|
|
882
|
+
const fingerMissing = Object.entries(state.missingFingers).map(([key, reason]) => ({
|
|
883
|
+
position: FINGER_MAP[key],
|
|
884
|
+
missingReasonCode: "UAP",
|
|
885
|
+
missingReasonText: reason
|
|
886
|
+
})).filter((f) => f.position);
|
|
887
|
+
const palmMissing = Object.entries(state.missingPalms).map(([key, reason]) => ({
|
|
888
|
+
position: PALM_MAP[key],
|
|
889
|
+
missingReasonCode: "UAP",
|
|
890
|
+
missingReasonText: reason
|
|
891
|
+
})).filter((f) => f.position);
|
|
892
|
+
const allMissingFingerprints = [...fingerMissing, ...palmMissing];
|
|
893
|
+
let finalFaces = [];
|
|
894
|
+
if (state.faces.length > 0) {
|
|
895
|
+
finalFaces = state.faces.map((item) => ({
|
|
896
|
+
image: { dataBytes: normalizeImageForStorage(item.image) }
|
|
897
|
+
}));
|
|
898
|
+
} else {
|
|
899
|
+
finalFaces = state.faceCaptures.filter((item) => item !== null).map((item) => ({
|
|
900
|
+
image: { dataBytes: normalizeImageForStorage(item.image) }
|
|
901
|
+
}));
|
|
902
|
+
}
|
|
903
|
+
const faceMissing = Object.entries(
|
|
904
|
+
state.missingFaces
|
|
905
|
+
).map(([idx, reason]) => ({
|
|
906
|
+
index: Number(idx),
|
|
907
|
+
missingReasonCode: "UAP",
|
|
908
|
+
missingReasonText: reason
|
|
909
|
+
}));
|
|
910
|
+
const finalIrises = [];
|
|
911
|
+
if (state.irises.right) {
|
|
912
|
+
finalIrises.push({
|
|
913
|
+
position: "Right" /* Right */,
|
|
914
|
+
image: {
|
|
915
|
+
dataBytes: normalizeImageForStorage(state.irises.right.image)
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
if (state.irises.left) {
|
|
920
|
+
finalIrises.push({
|
|
921
|
+
position: "Left" /* Left */,
|
|
922
|
+
image: { dataBytes: normalizeImageForStorage(state.irises.left.image) }
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
const irisMissing = Object.entries(
|
|
926
|
+
state.missingIrises
|
|
927
|
+
).map(([side, reason]) => ({
|
|
928
|
+
position: side === "right" ? "Right" /* Right */ : "Left" /* Left */,
|
|
929
|
+
missingReasonCode: "UAP",
|
|
930
|
+
missingReasonText: reason
|
|
931
|
+
}));
|
|
932
|
+
const request = {
|
|
933
|
+
externalId: state.externalId || `AUTO-${Date.now()}`,
|
|
934
|
+
enrollAction: { enrollActionType: "Masterize" },
|
|
935
|
+
customDetails: {
|
|
936
|
+
fullName: state.fullName || "Unknown Applicant"
|
|
937
|
+
},
|
|
938
|
+
fingerprintModality: {
|
|
939
|
+
fingerprints: allFrictionRidge,
|
|
940
|
+
missingFingerprints: allMissingFingerprints
|
|
941
|
+
},
|
|
942
|
+
faceModality: {
|
|
943
|
+
faces: finalFaces,
|
|
944
|
+
missingFaces: faceMissing.length > 0 ? faceMissing : void 0
|
|
945
|
+
},
|
|
946
|
+
irisModality: {
|
|
947
|
+
irises: finalIrises,
|
|
948
|
+
missingIrises: irisMissing.length > 0 ? irisMissing : void 0
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
return request;
|
|
952
|
+
},
|
|
953
|
+
getDirtyModalities: () => {
|
|
954
|
+
const state = get();
|
|
955
|
+
const snap = state._originalSnapshot;
|
|
956
|
+
const dirty = /* @__PURE__ */ new Set();
|
|
957
|
+
if (!snap) {
|
|
958
|
+
if (Object.keys(state.fingerprints).length > 0 || Object.keys(state.rolledFingerprints).length > 0 || Object.keys(state.missingFingers).length > 0 || Object.keys(state.palms).length > 0 || Object.keys(state.missingPalms).length > 0)
|
|
959
|
+
dirty.add("fingerprint");
|
|
960
|
+
if (state.faces.length > 0 || state.faceCaptures.some((f) => f !== null) || Object.keys(state.missingFaces).length > 0)
|
|
961
|
+
dirty.add("face");
|
|
962
|
+
if (state.irises.right || state.irises.left || Object.keys(state.missingIrises).length > 0)
|
|
963
|
+
dirty.add("iris");
|
|
964
|
+
return dirty;
|
|
965
|
+
}
|
|
966
|
+
const fingerprintChanged = JSON.stringify(state.fingerprints) !== JSON.stringify(snap.fingerprints) || JSON.stringify(state.rolledFingerprints) !== JSON.stringify(snap.rolledFingerprints) || JSON.stringify(state.missingFingers) !== JSON.stringify(snap.missingFingers) || JSON.stringify(state.palms) !== JSON.stringify(snap.palms) || JSON.stringify(state.missingPalms) !== JSON.stringify(snap.missingPalms);
|
|
967
|
+
if (fingerprintChanged) dirty.add("fingerprint");
|
|
968
|
+
const faceChanged = JSON.stringify(state.faces) !== JSON.stringify(snap.faces) || JSON.stringify(state.faceCaptures) !== JSON.stringify(snap.faceCaptures) || JSON.stringify(state.missingFaces) !== JSON.stringify(snap.missingFaces);
|
|
969
|
+
if (faceChanged) dirty.add("face");
|
|
970
|
+
const irisChanged = JSON.stringify(state.irises) !== JSON.stringify(snap.irises) || JSON.stringify(state.missingIrises) !== JSON.stringify(snap.missingIrises);
|
|
971
|
+
if (irisChanged) dirty.add("iris");
|
|
972
|
+
return dirty;
|
|
973
|
+
},
|
|
974
|
+
buildUpdateRequest: () => {
|
|
975
|
+
const state = get();
|
|
976
|
+
const dirty = state.getDirtyModalities();
|
|
977
|
+
const request = {
|
|
978
|
+
customDetails: {
|
|
979
|
+
fullName: state.fullName || "Unknown Applicant"
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
if (dirty.has("fingerprint")) {
|
|
983
|
+
const plainFingerprints = Object.entries(state.fingerprints).map(([key, item]) => ({
|
|
984
|
+
position: FINGER_MAP[key],
|
|
985
|
+
impressionType: "LiveScanPlain" /* LiveScanPlain */,
|
|
986
|
+
image: { dataBytes: normalizeImageForStorage(item.image) }
|
|
987
|
+
})).filter((f) => f.position);
|
|
988
|
+
const rolledFingerprints = Object.entries(state.rolledFingerprints).map(([key, item]) => ({
|
|
989
|
+
position: FINGER_MAP[key],
|
|
990
|
+
impressionType: "RolledContact" /* RolledContact */,
|
|
991
|
+
image: { dataBytes: normalizeImageForStorage(item.image) }
|
|
992
|
+
})).filter((f) => f.position);
|
|
993
|
+
const palmFingerprints = Object.entries(state.palms).map(([key, item]) => ({
|
|
994
|
+
position: PALM_MAP[key],
|
|
995
|
+
impressionType: "LiveScanPlain" /* LiveScanPlain */,
|
|
996
|
+
image: { dataBytes: normalizeImageForStorage(item.image) }
|
|
997
|
+
})).filter((f) => f.position);
|
|
998
|
+
const fingerMissing = Object.entries(state.missingFingers).map(([key, reason]) => ({
|
|
999
|
+
position: FINGER_MAP[key],
|
|
1000
|
+
missingReasonCode: "UAP",
|
|
1001
|
+
missingReasonText: reason
|
|
1002
|
+
})).filter((f) => f.position);
|
|
1003
|
+
const palmMissing = Object.entries(state.missingPalms).map(([key, reason]) => ({
|
|
1004
|
+
position: PALM_MAP[key],
|
|
1005
|
+
missingReasonCode: "UAP",
|
|
1006
|
+
missingReasonText: reason
|
|
1007
|
+
})).filter((f) => f.position);
|
|
1008
|
+
request.fingerprintModality = {
|
|
1009
|
+
fingerprints: [
|
|
1010
|
+
...plainFingerprints,
|
|
1011
|
+
...rolledFingerprints,
|
|
1012
|
+
...palmFingerprints
|
|
1013
|
+
],
|
|
1014
|
+
missingFingerprints: [...fingerMissing, ...palmMissing]
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
if (dirty.has("face")) {
|
|
1018
|
+
let finalFaces = [];
|
|
1019
|
+
if (state.faces.length > 0) {
|
|
1020
|
+
finalFaces = state.faces.map((item) => ({
|
|
1021
|
+
image: { dataBytes: normalizeImageForStorage(item.image) }
|
|
1022
|
+
}));
|
|
1023
|
+
} else {
|
|
1024
|
+
finalFaces = state.faceCaptures.filter((item) => item !== null).map((item) => ({
|
|
1025
|
+
image: { dataBytes: normalizeImageForStorage(item.image) }
|
|
1026
|
+
}));
|
|
1027
|
+
}
|
|
1028
|
+
const faceMissing = Object.entries(
|
|
1029
|
+
state.missingFaces
|
|
1030
|
+
).map(([idx, reason]) => ({
|
|
1031
|
+
index: Number(idx),
|
|
1032
|
+
missingReasonCode: "UAP",
|
|
1033
|
+
missingReasonText: reason
|
|
1034
|
+
}));
|
|
1035
|
+
request.faceModality = {
|
|
1036
|
+
faces: finalFaces,
|
|
1037
|
+
missingFaces: faceMissing.length > 0 ? faceMissing : void 0
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
if (dirty.has("iris")) {
|
|
1041
|
+
const finalIrises = [];
|
|
1042
|
+
if (state.irises.right) {
|
|
1043
|
+
finalIrises.push({
|
|
1044
|
+
position: "Right" /* Right */,
|
|
1045
|
+
image: {
|
|
1046
|
+
dataBytes: normalizeImageForStorage(state.irises.right.image)
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
if (state.irises.left) {
|
|
1051
|
+
finalIrises.push({
|
|
1052
|
+
position: "Left" /* Left */,
|
|
1053
|
+
image: {
|
|
1054
|
+
dataBytes: normalizeImageForStorage(state.irises.left.image)
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
const irisMissing = Object.entries(
|
|
1059
|
+
state.missingIrises
|
|
1060
|
+
).map(([side, reason]) => ({
|
|
1061
|
+
position: side === "right" ? "Right" /* Right */ : "Left" /* Left */,
|
|
1062
|
+
missingReasonCode: "UAP",
|
|
1063
|
+
missingReasonText: reason
|
|
1064
|
+
}));
|
|
1065
|
+
request.irisModality = {
|
|
1066
|
+
irises: finalIrises,
|
|
1067
|
+
missingIrises: irisMissing.length > 0 ? irisMissing : void 0
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
return request;
|
|
1071
|
+
}
|
|
1072
|
+
}));
|
|
1073
|
+
|
|
1074
|
+
// src/components/ui/BiometricSlot.tsx
|
|
1075
|
+
var import_framer_motion = require("framer-motion");
|
|
1076
|
+
var import_tb = require("react-icons/tb");
|
|
1077
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
1078
|
+
var iconMap = {
|
|
1079
|
+
finger: import_tb.TbFingerprint,
|
|
1080
|
+
face: import_tb.TbFaceId,
|
|
1081
|
+
palm: import_tb.TbHandStop,
|
|
1082
|
+
iris: import_tb.TbEye
|
|
1083
|
+
};
|
|
1084
|
+
function BiometricSlot({
|
|
1085
|
+
title,
|
|
1086
|
+
position,
|
|
1087
|
+
status,
|
|
1088
|
+
quality,
|
|
1089
|
+
imageUrl,
|
|
1090
|
+
onCapture,
|
|
1091
|
+
onRequestMissing,
|
|
1092
|
+
onRestore,
|
|
1093
|
+
onDelete,
|
|
1094
|
+
missingReason: externalMissingReason,
|
|
1095
|
+
iconType,
|
|
1096
|
+
isDisabled = false,
|
|
1097
|
+
className = "",
|
|
1098
|
+
isLoadedFromBackend = false
|
|
1099
|
+
}) {
|
|
1100
|
+
const IconComponent = iconMap[iconType];
|
|
1101
|
+
const missingReason = externalMissingReason || null;
|
|
1102
|
+
const isMissing = !!missingReason;
|
|
1103
|
+
let ringClass = "ring-border/50 dark:ring-white/10";
|
|
1104
|
+
let glowClass = "";
|
|
1105
|
+
let elevationClass = "elevation-1 hover:elevation-2";
|
|
1106
|
+
let statusColor = "text-foreground/40";
|
|
1107
|
+
let buttonStyle = "bg-primary text-primary-foreground hover:bg-primary/90 hover:shadow-glow-primary";
|
|
1108
|
+
let borderClass = "border-white/10";
|
|
1109
|
+
if (status === "captured" && !isMissing) {
|
|
1110
|
+
const q = quality || 0;
|
|
1111
|
+
if (q >= 80) {
|
|
1112
|
+
ringClass = "ring-quality-good/60";
|
|
1113
|
+
glowClass = "glow-success";
|
|
1114
|
+
elevationClass = "elevation-3";
|
|
1115
|
+
statusColor = "text-quality-good";
|
|
1116
|
+
buttonStyle = "bg-secondary text-foreground hover:bg-secondary/80 border border-border/50";
|
|
1117
|
+
borderClass = "border-quality-good/30";
|
|
1118
|
+
} else if (q >= 50) {
|
|
1119
|
+
ringClass = "ring-quality-ok/50";
|
|
1120
|
+
glowClass = "";
|
|
1121
|
+
elevationClass = "elevation-2";
|
|
1122
|
+
statusColor = "text-quality-ok";
|
|
1123
|
+
buttonStyle = "bg-secondary text-foreground hover:bg-secondary/80 border border-border/50";
|
|
1124
|
+
borderClass = "border-quality-ok/30";
|
|
1125
|
+
} else {
|
|
1126
|
+
ringClass = "ring-quality-bad/50";
|
|
1127
|
+
glowClass = "glow-danger";
|
|
1128
|
+
elevationClass = "elevation-2";
|
|
1129
|
+
statusColor = "text-quality-bad";
|
|
1130
|
+
buttonStyle = "bg-secondary text-foreground hover:bg-secondary/80 border border-border/50";
|
|
1131
|
+
borderClass = "border-quality-bad/30";
|
|
1132
|
+
}
|
|
1133
|
+
} else if (isMissing) {
|
|
1134
|
+
ringClass = "ring-red-500/30";
|
|
1135
|
+
borderClass = "border-red-500/20 border-dashed";
|
|
1136
|
+
glowClass = "";
|
|
1137
|
+
elevationClass = "elevation-1";
|
|
1138
|
+
statusColor = "text-red-400";
|
|
1139
|
+
buttonStyle = "bg-transparent border border-red-500/30 text-red-400 cursor-not-allowed opacity-50";
|
|
1140
|
+
}
|
|
1141
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
1142
|
+
import_framer_motion.motion.div,
|
|
1143
|
+
{
|
|
1144
|
+
className: `
|
|
1145
|
+
relative flex flex-col p-3 rounded-md transition-all duration-300
|
|
1146
|
+
group cursor-pointer select-none
|
|
1147
|
+
bg-card/40
|
|
1148
|
+
${elevationClass}
|
|
1149
|
+
${borderClass}
|
|
1150
|
+
${status === "captured" && !isMissing ? "ring-2 " + ringClass : "hover:ring-1 hover:ring-primary/30 hover:border-primary/30"}
|
|
1151
|
+
${glowClass}
|
|
1152
|
+
hover:-translate-y-1
|
|
1153
|
+
${isDisabled ? "opacity-50 pointer-events-none grayscale hover:translate-y-0" : ""}
|
|
1154
|
+
${isMissing ? "bg-red-500/5" : ""}
|
|
1155
|
+
${className}
|
|
1156
|
+
`,
|
|
1157
|
+
children: [
|
|
1158
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex justify-between items-start w-full mb-3 z-10 h-6", children: [
|
|
1159
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1160
|
+
"p",
|
|
1161
|
+
{
|
|
1162
|
+
className: "text-xs font-bold truncate text-foreground/70",
|
|
1163
|
+
title,
|
|
1164
|
+
children: title
|
|
1165
|
+
}
|
|
1166
|
+
),
|
|
1167
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center gap-1", children: [
|
|
1168
|
+
status === "captured" && !isMissing && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: isLoadedFromBackend ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "bg-blue-500/90 text-white text-[9px] px-1.5 py-0.5 rounded-full font-bold flex items-center gap-1", children: [
|
|
1169
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_tb.TbCloud, { size: 10 }),
|
|
1170
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "Cargado" })
|
|
1171
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "bg-green-500/90 text-white text-[9px] px-1.5 py-0.5 rounded-full font-bold flex items-center gap-1", children: [
|
|
1172
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_tb.TbCheck, { size: 10 }),
|
|
1173
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "Nuevo" })
|
|
1174
|
+
] }) }),
|
|
1175
|
+
isMissing ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1176
|
+
"button",
|
|
1177
|
+
{
|
|
1178
|
+
onClick: (e) => {
|
|
1179
|
+
e.stopPropagation();
|
|
1180
|
+
onRestore?.();
|
|
1181
|
+
},
|
|
1182
|
+
className: "text-green-500 hover:bg-green-500/10 p-1 rounded-full transition-colors",
|
|
1183
|
+
title: "Restaurar",
|
|
1184
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_tb.TbRestore, { size: 16 })
|
|
1185
|
+
}
|
|
1186
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center gap-0.5", children: [
|
|
1187
|
+
status === "captured" && onDelete && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1188
|
+
"button",
|
|
1189
|
+
{
|
|
1190
|
+
onClick: (e) => {
|
|
1191
|
+
e.stopPropagation();
|
|
1192
|
+
onDelete();
|
|
1193
|
+
},
|
|
1194
|
+
disabled: isDisabled,
|
|
1195
|
+
className: "text-foreground/20 hover:text-red-500 hover:bg-red-500/10 p-1 rounded-full disabled:opacity-0 transition-colors",
|
|
1196
|
+
title: "Eliminar captura",
|
|
1197
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_tb.TbTrash, { size: 16 })
|
|
1198
|
+
}
|
|
1199
|
+
),
|
|
1200
|
+
onRequestMissing && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1201
|
+
"button",
|
|
1202
|
+
{
|
|
1203
|
+
onClick: (e) => {
|
|
1204
|
+
e.stopPropagation();
|
|
1205
|
+
onRequestMissing();
|
|
1206
|
+
},
|
|
1207
|
+
disabled: isDisabled,
|
|
1208
|
+
className: "text-foreground/20 hover:text-red-500 hover:bg-red-500/10 p-1 rounded-full disabled:opacity-0 transition-colors",
|
|
1209
|
+
title: "Marcar como no capturable",
|
|
1210
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_tb.TbBan, { size: 16 })
|
|
1211
|
+
}
|
|
1212
|
+
)
|
|
1213
|
+
] })
|
|
1214
|
+
] })
|
|
1215
|
+
] }),
|
|
1216
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1217
|
+
"div",
|
|
1218
|
+
{
|
|
1219
|
+
className: "\r\n relative w-full flex-1 min-h-[100px] flex items-center justify-center\r\n rounded-md overflow-hidden\r\n bg-black/5 dark:bg-white/5 border border-transparent\r\n group-hover:border-white/5 transition-all\r\n ",
|
|
1220
|
+
onClick: !isDisabled && !isMissing ? onCapture : void 0,
|
|
1221
|
+
children: isMissing ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-col items-center text-center p-2 animate-in zoom-in duration-300", children: [
|
|
1222
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_tb.TbBan, { size: 32, className: "text-red-400/50 mb-2" }),
|
|
1223
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "text-[10px] text-red-400 font-bold uppercase tracking-wider", children: missingReason })
|
|
1224
|
+
] }) : imageUrl ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1225
|
+
import_framer_motion.motion.img,
|
|
1226
|
+
{
|
|
1227
|
+
initial: { opacity: 0 },
|
|
1228
|
+
animate: { opacity: 1 },
|
|
1229
|
+
src: imageUrl,
|
|
1230
|
+
className: "w-full h-full object-contain p-2",
|
|
1231
|
+
alt: title
|
|
1232
|
+
}
|
|
1233
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1234
|
+
IconComponent,
|
|
1235
|
+
{
|
|
1236
|
+
size: 40,
|
|
1237
|
+
className: `
|
|
1238
|
+
text-foreground/10 transition-all duration-300
|
|
1239
|
+
${!isDisabled && "group-hover:scale-110 group-hover:text-primary/40"}
|
|
1240
|
+
`
|
|
1241
|
+
}
|
|
1242
|
+
)
|
|
1243
|
+
}
|
|
1244
|
+
),
|
|
1245
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "w-full mt-4 flex items-center justify-between gap-2", children: [
|
|
1246
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1247
|
+
"span",
|
|
1248
|
+
{
|
|
1249
|
+
className: `text-[10px] uppercase font-bold tracking-wider ${statusColor}`,
|
|
1250
|
+
children: isMissing ? "Omitido" : status === "captured" ? `${quality}% Calidad` : "Pendiente"
|
|
1251
|
+
}
|
|
1252
|
+
),
|
|
1253
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
1254
|
+
"button",
|
|
1255
|
+
{
|
|
1256
|
+
onClick: (e) => {
|
|
1257
|
+
e.stopPropagation();
|
|
1258
|
+
if (!isMissing) onCapture();
|
|
1259
|
+
},
|
|
1260
|
+
disabled: isDisabled || isMissing,
|
|
1261
|
+
className: `h-8 px-3 rounded-sm text-xs font-bold flex items-center gap-1 transition-all shadow-sm ${buttonStyle}`,
|
|
1262
|
+
children: [
|
|
1263
|
+
status === "captured" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_tb.TbRefresh, { size: 14 }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_tb.TbPlus, { size: 14 }),
|
|
1264
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: status === "captured" ? "Repetir" : "Capturar" })
|
|
1265
|
+
]
|
|
1266
|
+
}
|
|
1267
|
+
)
|
|
1268
|
+
] })
|
|
1269
|
+
]
|
|
1270
|
+
}
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// src/components/ui/MissingFingerModal.tsx
|
|
1275
|
+
var import_react3 = require("react");
|
|
1276
|
+
|
|
1277
|
+
// src/components/ui/Modal.tsx
|
|
1278
|
+
var import_framer_motion2 = require("framer-motion");
|
|
1279
|
+
var import_tb2 = require("react-icons/tb");
|
|
1280
|
+
var import_react2 = require("react");
|
|
1281
|
+
var import_react_dom = require("react-dom");
|
|
1282
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
1283
|
+
function Modal({
|
|
1284
|
+
isOpen,
|
|
1285
|
+
onClose,
|
|
1286
|
+
title,
|
|
1287
|
+
children,
|
|
1288
|
+
allowClose = true
|
|
1289
|
+
}) {
|
|
1290
|
+
const [mounted, setMounted] = (0, import_react2.useState)(false);
|
|
1291
|
+
(0, import_react2.useEffect)(() => {
|
|
1292
|
+
setMounted(true);
|
|
1293
|
+
return () => setMounted(false);
|
|
1294
|
+
}, []);
|
|
1295
|
+
(0, import_react2.useEffect)(() => {
|
|
1296
|
+
if (isOpen) document.body.style.overflow = "hidden";
|
|
1297
|
+
else document.body.style.overflow = "unset";
|
|
1298
|
+
return () => {
|
|
1299
|
+
document.body.style.overflow = "unset";
|
|
1300
|
+
};
|
|
1301
|
+
}, [isOpen]);
|
|
1302
|
+
if (!mounted) return null;
|
|
1303
|
+
const modalContent = /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_framer_motion2.AnimatePresence, { children: isOpen && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fixed inset-0 z-[9999] flex items-center justify-center p-4 sm:p-6", children: [
|
|
1304
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1305
|
+
import_framer_motion2.motion.div,
|
|
1306
|
+
{
|
|
1307
|
+
className: "absolute inset-0 backdrop-blur-md transition-all",
|
|
1308
|
+
initial: { opacity: 0 },
|
|
1309
|
+
animate: { opacity: 1 },
|
|
1310
|
+
exit: { opacity: 0 },
|
|
1311
|
+
onClick: allowClose ? onClose : void 0
|
|
1312
|
+
}
|
|
1313
|
+
),
|
|
1314
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
1315
|
+
import_framer_motion2.motion.div,
|
|
1316
|
+
{
|
|
1317
|
+
className: "\r\n relative z-10 w-full max-w-2xl flex flex-col\r\n rounded-sm overflow-hidden\r\n bg-card/95\r\n elevation-5\r\n max-h-[85vh]\r\n ",
|
|
1318
|
+
initial: { scale: 0.92, opacity: 0, y: 25 },
|
|
1319
|
+
animate: { scale: 1, opacity: 1, y: 0 },
|
|
1320
|
+
exit: { scale: 0.95, opacity: 0, y: 15 },
|
|
1321
|
+
transition: {
|
|
1322
|
+
type: "spring",
|
|
1323
|
+
bounce: 0.35,
|
|
1324
|
+
duration: 0.5
|
|
1325
|
+
},
|
|
1326
|
+
children: [
|
|
1327
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "absolute top-0 inset-x-0 h-[1px] bg-gradient-to-r from-transparent via-primary/60 to-transparent z-20" }),
|
|
1328
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center justify-between px-6 py-5 border-b border-white/5 bg-white/5 relative z-10", children: [
|
|
1329
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { className: "text-xl font-bold text-foreground tracking-tight drop-shadow-sm", children: title }),
|
|
1330
|
+
allowClose && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1331
|
+
"button",
|
|
1332
|
+
{
|
|
1333
|
+
onClick: onClose,
|
|
1334
|
+
className: "\r\n group p-2 rounded-full\r\n text-foreground/50 hover:text-foreground\r\n hover:bg-white/10 transition-all duration-200\r\n ",
|
|
1335
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1336
|
+
import_tb2.TbX,
|
|
1337
|
+
{
|
|
1338
|
+
size: 22,
|
|
1339
|
+
className: "group-hover:scale-110 transition-transform"
|
|
1340
|
+
}
|
|
1341
|
+
)
|
|
1342
|
+
}
|
|
1343
|
+
)
|
|
1344
|
+
] }),
|
|
1345
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1346
|
+
"div",
|
|
1347
|
+
{
|
|
1348
|
+
className: "\r\n p-6 sm:p-8 \r\n overflow-y-auto \r\n scrollbar-thin scrollbar-track-transparent scrollbar-thumb-border/40 hover:scrollbar-thumb-border/70\r\n ",
|
|
1349
|
+
children
|
|
1350
|
+
}
|
|
1351
|
+
)
|
|
1352
|
+
]
|
|
1353
|
+
}
|
|
1354
|
+
)
|
|
1355
|
+
] }) });
|
|
1356
|
+
return (0, import_react_dom.createPortal)(modalContent, document.body);
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// src/components/ui/Button.tsx
|
|
1360
|
+
var import_tb3 = require("react-icons/tb");
|
|
1361
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
1362
|
+
function Button({
|
|
1363
|
+
variant = "primary",
|
|
1364
|
+
size = "md",
|
|
1365
|
+
isLoading = false,
|
|
1366
|
+
icon,
|
|
1367
|
+
className = "",
|
|
1368
|
+
children,
|
|
1369
|
+
disabled,
|
|
1370
|
+
...props
|
|
1371
|
+
}) {
|
|
1372
|
+
const variantClasses = {
|
|
1373
|
+
primary: `
|
|
1374
|
+
bg-primary text-primary-foreground
|
|
1375
|
+
elevation-2
|
|
1376
|
+
hover:elevation-3 hover:-translate-y-1 hover:scale-[1.02]
|
|
1377
|
+
active:scale-[0.97] active:translate-y-0
|
|
1378
|
+
shadow-primary/20
|
|
1379
|
+
`,
|
|
1380
|
+
secondary: `
|
|
1381
|
+
bg-secondary text-secondary-foreground
|
|
1382
|
+
elevation-1
|
|
1383
|
+
hover:elevation-2 hover:-translate-y-1
|
|
1384
|
+
active:scale-[0.97]
|
|
1385
|
+
`,
|
|
1386
|
+
success: `
|
|
1387
|
+
bg-success/10 text-success
|
|
1388
|
+
elevation-1
|
|
1389
|
+
hover:bg-success hover:text-white hover:elevation-2 hover:-translate-y-1 hover:glow-success
|
|
1390
|
+
active:scale-[0.97]
|
|
1391
|
+
`,
|
|
1392
|
+
danger: `
|
|
1393
|
+
bg-danger/10 text-danger
|
|
1394
|
+
elevation-1
|
|
1395
|
+
hover:bg-danger hover:text-white hover:elevation-2 hover:-translate-y-1 hover:glow-danger
|
|
1396
|
+
active:scale-[0.97]
|
|
1397
|
+
`,
|
|
1398
|
+
warning: `
|
|
1399
|
+
bg-warning/10 text-warning
|
|
1400
|
+
elevation-1
|
|
1401
|
+
hover:bg-warning hover:text-foreground hover:elevation-2 hover:-translate-y-1 hover:glow-warning
|
|
1402
|
+
active:scale-[0.97]
|
|
1403
|
+
`,
|
|
1404
|
+
ghost: `
|
|
1405
|
+
bg-transparent
|
|
1406
|
+
border border-border-default
|
|
1407
|
+
hover:bg-secondary/50 hover:elevation-1 hover:-translate-y-1
|
|
1408
|
+
active:scale-[0.97]
|
|
1409
|
+
`
|
|
1410
|
+
};
|
|
1411
|
+
const sizeClasses = {
|
|
1412
|
+
sm: "px-3 py-1.5 text-xs rounded-sm",
|
|
1413
|
+
md: "px-4 py-2.5 text-sm rounded-md",
|
|
1414
|
+
lg: "px-6 py-3.5 text-base rounded-md"
|
|
1415
|
+
};
|
|
1416
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
1417
|
+
"button",
|
|
1418
|
+
{
|
|
1419
|
+
className: `
|
|
1420
|
+
${variantClasses[variant]}
|
|
1421
|
+
${sizeClasses[size]}
|
|
1422
|
+
font-semibold inline-flex items-center justify-center gap-2
|
|
1423
|
+
transition-all duration-250
|
|
1424
|
+
disabled:opacity-50 disabled:cursor-not-allowed
|
|
1425
|
+
${className}
|
|
1426
|
+
`,
|
|
1427
|
+
disabled: disabled || isLoading,
|
|
1428
|
+
...props,
|
|
1429
|
+
children: [
|
|
1430
|
+
isLoading ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_tb3.TbLoader, { className: "animate-spin" }) : icon,
|
|
1431
|
+
children
|
|
1432
|
+
]
|
|
1433
|
+
}
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// src/components/ui/MissingFingerModal.tsx
|
|
1438
|
+
var import_tb4 = require("react-icons/tb");
|
|
1439
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
1440
|
+
var REASONS_BY_TYPE = {
|
|
1441
|
+
finger: [
|
|
1442
|
+
"Amputaci\xF3n Total",
|
|
1443
|
+
"Amputaci\xF3n Parcial",
|
|
1444
|
+
"Herida / Vendaje",
|
|
1445
|
+
"Deformidad",
|
|
1446
|
+
"Imposibilidad F\xEDsica",
|
|
1447
|
+
"Otro"
|
|
1448
|
+
],
|
|
1449
|
+
palm: [
|
|
1450
|
+
"Herida / Vendaje",
|
|
1451
|
+
"Deformidad",
|
|
1452
|
+
"Imposibilidad F\xEDsica",
|
|
1453
|
+
"Otro"
|
|
1454
|
+
],
|
|
1455
|
+
iris: [
|
|
1456
|
+
"Pr\xF3tesis Ocular",
|
|
1457
|
+
"Lesi\xF3n / Cirug\xEDa",
|
|
1458
|
+
"Ceguera",
|
|
1459
|
+
"Imposibilidad F\xEDsica",
|
|
1460
|
+
"Otro"
|
|
1461
|
+
],
|
|
1462
|
+
face: [
|
|
1463
|
+
"Lesi\xF3n Facial",
|
|
1464
|
+
"Vendaje / Cubrimiento",
|
|
1465
|
+
"Imposibilidad F\xEDsica",
|
|
1466
|
+
"Otro"
|
|
1467
|
+
]
|
|
1468
|
+
};
|
|
1469
|
+
var WARNINGS = {
|
|
1470
|
+
finger: "Al marcar este dedo como no capturable, el sistema lo ignorar\xE1 autom\xE1ticamente durante el escaneo.",
|
|
1471
|
+
palm: "Al marcar esta palma como no capturable, ser\xE1 excluida del enrolamiento.",
|
|
1472
|
+
iris: "Al marcar este iris como no capturable, ser\xE1 excluido del enrolamiento.",
|
|
1473
|
+
face: "Al marcar este \xE1ngulo facial como no capturable, ser\xE1 excluido del enrolamiento."
|
|
1474
|
+
};
|
|
1475
|
+
var MissingBiometricModal = ({
|
|
1476
|
+
isOpen,
|
|
1477
|
+
onClose,
|
|
1478
|
+
onConfirm,
|
|
1479
|
+
positionName,
|
|
1480
|
+
biometricType = "finger"
|
|
1481
|
+
}) => {
|
|
1482
|
+
const reasons = REASONS_BY_TYPE[biometricType];
|
|
1483
|
+
const [selectedReason, setSelectedReason] = (0, import_react3.useState)(reasons[0]);
|
|
1484
|
+
(0, import_react3.useEffect)(() => {
|
|
1485
|
+
if (isOpen) {
|
|
1486
|
+
setSelectedReason(REASONS_BY_TYPE[biometricType][0]);
|
|
1487
|
+
}
|
|
1488
|
+
}, [isOpen, biometricType]);
|
|
1489
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1490
|
+
Modal,
|
|
1491
|
+
{
|
|
1492
|
+
isOpen,
|
|
1493
|
+
onClose,
|
|
1494
|
+
title: `Omitir captura: ${positionName}`,
|
|
1495
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "p-4 space-y-4", children: [
|
|
1496
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "bg-warning/10 border-l-4 border-warning p-3 text-sm text-warning", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { children: WARNINGS[biometricType] }) }),
|
|
1497
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "space-y-2 mt-4", children: [
|
|
1498
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "font-semibold text-foreground/70 mb-2", children: "Seleccione el motivo:" }),
|
|
1499
|
+
reasons.map((reason) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
1500
|
+
"label",
|
|
1501
|
+
{
|
|
1502
|
+
className: `flex items-center p-3 border rounded-sm cursor-pointer transition-colors ${selectedReason === reason ? "bg-primary/10 border-primary ring-1 ring-primary" : "hover:bg-secondary/50 border-border-default"}`,
|
|
1503
|
+
children: [
|
|
1504
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1505
|
+
"input",
|
|
1506
|
+
{
|
|
1507
|
+
type: "radio",
|
|
1508
|
+
name: "reason",
|
|
1509
|
+
value: reason,
|
|
1510
|
+
checked: selectedReason === reason,
|
|
1511
|
+
onChange: (e) => setSelectedReason(e.target.value),
|
|
1512
|
+
className: "h-4 w-4 text-primary focus:ring-primary border-border-default"
|
|
1513
|
+
}
|
|
1514
|
+
),
|
|
1515
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "ml-3 text-foreground", children: reason })
|
|
1516
|
+
]
|
|
1517
|
+
},
|
|
1518
|
+
reason
|
|
1519
|
+
))
|
|
1520
|
+
] }),
|
|
1521
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex justify-end space-x-3 pt-4 border-t border-border-default mt-4", children: [
|
|
1522
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1523
|
+
Button,
|
|
1524
|
+
{
|
|
1525
|
+
onClick: onClose,
|
|
1526
|
+
variant: "secondary",
|
|
1527
|
+
size: "md",
|
|
1528
|
+
children: "Cancelar"
|
|
1529
|
+
}
|
|
1530
|
+
),
|
|
1531
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1532
|
+
Button,
|
|
1533
|
+
{
|
|
1534
|
+
onClick: () => onConfirm(selectedReason),
|
|
1535
|
+
variant: "danger",
|
|
1536
|
+
size: "md",
|
|
1537
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_tb4.TbBan, {}),
|
|
1538
|
+
children: "Confirmar Excepci\xF3n"
|
|
1539
|
+
}
|
|
1540
|
+
)
|
|
1541
|
+
] })
|
|
1542
|
+
] })
|
|
1543
|
+
}
|
|
1544
|
+
);
|
|
1545
|
+
};
|
|
1546
|
+
var MissingFingerModal = MissingBiometricModal;
|
|
1547
|
+
|
|
1548
|
+
// src/components/ui/ProgressBar.tsx
|
|
1549
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
1550
|
+
function ProgressBar({
|
|
1551
|
+
current,
|
|
1552
|
+
total,
|
|
1553
|
+
label = "Progreso del enrolamiento",
|
|
1554
|
+
className = ""
|
|
1555
|
+
}) {
|
|
1556
|
+
const progress = Math.min(100, Math.max(0, current / total * 100));
|
|
1557
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: `w-full mt-8 ${className}`, children: [
|
|
1558
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex justify-between text-xs font-semibold text-foreground/50 mb-2 uppercase tracking-wider", children: [
|
|
1559
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: label }),
|
|
1560
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
|
|
1561
|
+
current,
|
|
1562
|
+
" / ",
|
|
1563
|
+
total,
|
|
1564
|
+
" Completadas"
|
|
1565
|
+
] })
|
|
1566
|
+
] }),
|
|
1567
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "h-3 w-full bg-secondary rounded-full overflow-hidden border border-white/5", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
1568
|
+
"div",
|
|
1569
|
+
{
|
|
1570
|
+
className: "h-full bg-gradient-to-r from-primary to-purple-500 transition-all duration-700 ease-out relative",
|
|
1571
|
+
style: { width: `${progress}%` },
|
|
1572
|
+
children: [
|
|
1573
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "absolute inset-0 bg-white/20 animate-pulse" }),
|
|
1574
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "absolute right-0 top-0 bottom-0 w-[1px] bg-white/50 shadow-[0_0_10px_2px_rgba(255,255,255,0.5)]" })
|
|
1575
|
+
]
|
|
1576
|
+
}
|
|
1577
|
+
) })
|
|
1578
|
+
] });
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
// src/components/enrollment/ModuleHeader.tsx
|
|
1582
|
+
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
1583
|
+
function ModuleHeader({
|
|
1584
|
+
icon,
|
|
1585
|
+
title,
|
|
1586
|
+
subtitle,
|
|
1587
|
+
progress,
|
|
1588
|
+
children
|
|
1589
|
+
}) {
|
|
1590
|
+
const isConnected = useBiometricStore((s) => s.isConnected);
|
|
1591
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "glass rounded-sm p-6 md:p-8 border border-white/10", children: [
|
|
1592
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex flex-col md:flex-row justify-between items-start md:items-center gap-6", children: [
|
|
1593
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { children: [
|
|
1594
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("h2", { className: "text-2xl font-bold text-foreground flex items-center gap-3", children: [
|
|
1595
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "p-2 bg-primary/10 rounded-sm text-primary", children: icon }),
|
|
1596
|
+
title
|
|
1597
|
+
] }),
|
|
1598
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "text-foreground/60 mt-2 max-w-xl text-sm", children: subtitle })
|
|
1599
|
+
] }),
|
|
1600
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex flex-wrap gap-3 items-center", children: [
|
|
1601
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
1602
|
+
"div",
|
|
1603
|
+
{
|
|
1604
|
+
className: `px-3 py-1.5 rounded-sm text-[10px] font-bold uppercase tracking-wider flex items-center gap-2 border ${isConnected ? "bg-green-500/10 border-green-500/20 text-green-600 dark:text-green-400" : "bg-red-500/10 border-red-500/20 text-red-600 dark:text-red-400"}`,
|
|
1605
|
+
children: [
|
|
1606
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1607
|
+
"span",
|
|
1608
|
+
{
|
|
1609
|
+
className: `w-1.5 h-1.5 rounded-full ${isConnected ? "bg-green-500 animate-pulse" : "bg-red-500"}`
|
|
1610
|
+
}
|
|
1611
|
+
),
|
|
1612
|
+
isConnected ? "Conectado" : "Desconectado"
|
|
1613
|
+
]
|
|
1614
|
+
}
|
|
1615
|
+
),
|
|
1616
|
+
children
|
|
1617
|
+
] })
|
|
1618
|
+
] }),
|
|
1619
|
+
progress && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ProgressBar, { current: progress.current, total: progress.total })
|
|
1620
|
+
] });
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
// src/components/ui/Select.tsx
|
|
1624
|
+
var import_react4 = require("react");
|
|
1625
|
+
var import_framer_motion3 = require("framer-motion");
|
|
1626
|
+
var import_tb5 = require("react-icons/tb");
|
|
1627
|
+
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
1628
|
+
function Select({
|
|
1629
|
+
value,
|
|
1630
|
+
onChange,
|
|
1631
|
+
options,
|
|
1632
|
+
placeholder = "Seleccionar...",
|
|
1633
|
+
disabled = false,
|
|
1634
|
+
className = ""
|
|
1635
|
+
}) {
|
|
1636
|
+
const [isOpen, setIsOpen] = (0, import_react4.useState)(false);
|
|
1637
|
+
const containerRef = (0, import_react4.useRef)(null);
|
|
1638
|
+
(0, import_react4.useEffect)(() => {
|
|
1639
|
+
function handleClickOutside(event) {
|
|
1640
|
+
if (containerRef.current && !containerRef.current.contains(event.target)) {
|
|
1641
|
+
setIsOpen(false);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
1645
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
1646
|
+
}, []);
|
|
1647
|
+
const selectedOption = options.find((opt) => opt.value === value);
|
|
1648
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
|
|
1649
|
+
"div",
|
|
1650
|
+
{
|
|
1651
|
+
className: `relative w-full min-w-[200px] ${className}`,
|
|
1652
|
+
ref: containerRef,
|
|
1653
|
+
children: [
|
|
1654
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
|
|
1655
|
+
"button",
|
|
1656
|
+
{
|
|
1657
|
+
type: "button",
|
|
1658
|
+
onClick: () => !disabled && setIsOpen(!isOpen),
|
|
1659
|
+
disabled,
|
|
1660
|
+
className: `
|
|
1661
|
+
w-full flex items-center justify-between
|
|
1662
|
+
px-4 py-3 rounded-md text-sm font-medium
|
|
1663
|
+
transition-all duration-250
|
|
1664
|
+
${disabled ? "opacity-50 cursor-not-allowed bg-secondary border-border elevation-1" : isOpen ? "bg-card elevation-3 ring-2 ring-primary/50 glow-primary text-foreground" : "bg-card/50 elevation-1 hover:elevation-2 hover:-translate-y-0.5 hover:border-primary/30 text-foreground/80"}
|
|
1665
|
+
`,
|
|
1666
|
+
children: [
|
|
1667
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "flex items-center gap-2 truncate", children: [
|
|
1668
|
+
selectedOption?.icon && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "text-primary/80", children: selectedOption.icon }),
|
|
1669
|
+
selectedOption ? selectedOption.label : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "text-muted-foreground", children: placeholder })
|
|
1670
|
+
] }),
|
|
1671
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
1672
|
+
import_tb5.TbChevronDown,
|
|
1673
|
+
{
|
|
1674
|
+
className: `
|
|
1675
|
+
w-4 h-4 opacity-50 transition-transform duration-300
|
|
1676
|
+
${isOpen ? "rotate-180 text-primary" : ""}
|
|
1677
|
+
`
|
|
1678
|
+
}
|
|
1679
|
+
)
|
|
1680
|
+
]
|
|
1681
|
+
}
|
|
1682
|
+
),
|
|
1683
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_framer_motion3.AnimatePresence, { children: isOpen && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
1684
|
+
import_framer_motion3.motion.div,
|
|
1685
|
+
{
|
|
1686
|
+
initial: { opacity: 0, y: 8, scale: 0.98 },
|
|
1687
|
+
animate: { opacity: 1, y: 0, scale: 1 },
|
|
1688
|
+
exit: { opacity: 0, y: 8, scale: 0.98 },
|
|
1689
|
+
transition: { duration: 0.2, ease: "easeOut" },
|
|
1690
|
+
className: "\r\n absolute z-50 top-full left-0 right-0 mt-2\r\n p-1.5 rounded-md border border-white/10\r\n bg-card/95 backdrop-blur-2xl\r\n shadow-2xl shadow-black/20\r\n max-h-64 overflow-y-auto\r\n scrollbar-thin scrollbar-track-transparent scrollbar-thumb-border/50\r\n ",
|
|
1691
|
+
children: options.map((option) => {
|
|
1692
|
+
const isSelected = option.value === value;
|
|
1693
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
|
|
1694
|
+
"div",
|
|
1695
|
+
{
|
|
1696
|
+
onClick: () => {
|
|
1697
|
+
onChange(option.value);
|
|
1698
|
+
setIsOpen(false);
|
|
1699
|
+
},
|
|
1700
|
+
className: `
|
|
1701
|
+
flex items-center justify-between px-3 py-2.5 rounded-sm cursor-pointer
|
|
1702
|
+
text-sm transition-all duration-150
|
|
1703
|
+
${isSelected ? "bg-primary/10 text-primary font-semibold" : "text-foreground/70 hover:bg-white/5 hover:text-foreground"}
|
|
1704
|
+
`,
|
|
1705
|
+
children: [
|
|
1706
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex items-center gap-2.5", children: [
|
|
1707
|
+
option.icon && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
1708
|
+
"span",
|
|
1709
|
+
{
|
|
1710
|
+
className: isSelected ? "text-primary" : "opacity-50",
|
|
1711
|
+
children: option.icon
|
|
1712
|
+
}
|
|
1713
|
+
),
|
|
1714
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: option.label })
|
|
1715
|
+
] }),
|
|
1716
|
+
isSelected && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
1717
|
+
import_framer_motion3.motion.div,
|
|
1718
|
+
{
|
|
1719
|
+
initial: { scale: 0 },
|
|
1720
|
+
animate: { scale: 1 },
|
|
1721
|
+
transition: {
|
|
1722
|
+
type: "spring",
|
|
1723
|
+
stiffness: 300,
|
|
1724
|
+
damping: 20
|
|
1725
|
+
},
|
|
1726
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_tb5.TbCheck, { size: 16 })
|
|
1727
|
+
}
|
|
1728
|
+
)
|
|
1729
|
+
]
|
|
1730
|
+
},
|
|
1731
|
+
option.value
|
|
1732
|
+
);
|
|
1733
|
+
})
|
|
1734
|
+
}
|
|
1735
|
+
) })
|
|
1736
|
+
]
|
|
1737
|
+
}
|
|
1738
|
+
);
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// src/components/ui/QualityBadge.tsx
|
|
1742
|
+
var import_jsx_runtime9 = require("react/jsx-runtime");
|
|
1743
|
+
function QualityBadge({ quality, className = "" }) {
|
|
1744
|
+
if (quality === null || quality === void 0) return null;
|
|
1745
|
+
const baseClasses = "flex flex-col items-center justify-center w-16 h-16 rounded-full border-2 backdrop-blur-xl shadow-lg animate-in zoom-in duration-300";
|
|
1746
|
+
let colorClasses = "";
|
|
1747
|
+
if (quality >= 80) {
|
|
1748
|
+
colorClasses = "text-quality-good border-quality-good/30 bg-quality-good/10 shadow-[0_0_10px_-3px_rgba(var(--quality-good),0.3)]";
|
|
1749
|
+
} else if (quality >= 50) {
|
|
1750
|
+
colorClasses = "text-quality-ok border-quality-ok/30 bg-quality-ok/10 shadow-[0_0_10px_-3px_rgba(var(--quality-ok),0.3)]";
|
|
1751
|
+
} else {
|
|
1752
|
+
colorClasses = "text-quality-bad border-quality-bad/30 bg-quality-bad/10 shadow-[0_0_10px_-3px_rgba(var(--quality-bad),0.3)]";
|
|
1753
|
+
}
|
|
1754
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: `${baseClasses} ${colorClasses} ${className}`, children: [
|
|
1755
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "text-xl font-bold tracking-tighter leading-none", children: quality }),
|
|
1756
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "text-[8px] font-bold uppercase tracking-widest opacity-70 mt-0.5", children: "CALIDAD" })
|
|
1757
|
+
] });
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
// src/components/ui/Loader.tsx
|
|
1761
|
+
var import_tb6 = require("react-icons/tb");
|
|
1762
|
+
var import_jsx_runtime10 = require("react/jsx-runtime");
|
|
1763
|
+
function Loader({ size = 24, text, className }) {
|
|
1764
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: `flex flex-col items-center justify-center gap-2 text-primary ${className}`, children: [
|
|
1765
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_tb6.TbLoader, { size, className: "animate-spin" }),
|
|
1766
|
+
text && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "text-sm text-foreground/80", children: text })
|
|
1767
|
+
] });
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// src/components/enrollment/FingerCaptureModal.tsx
|
|
1771
|
+
var import_tb7 = require("react-icons/tb");
|
|
1772
|
+
var import_framer_motion4 = require("framer-motion");
|
|
1773
|
+
var import_jsx_runtime11 = require("react/jsx-runtime");
|
|
1774
|
+
function FingerCaptureModal({
|
|
1775
|
+
isOpen,
|
|
1776
|
+
onCancel,
|
|
1777
|
+
captureType,
|
|
1778
|
+
previewImage,
|
|
1779
|
+
previewQuality
|
|
1780
|
+
}) {
|
|
1781
|
+
const isWaiting = !previewImage;
|
|
1782
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1783
|
+
Modal,
|
|
1784
|
+
{
|
|
1785
|
+
isOpen,
|
|
1786
|
+
onClose: onCancel,
|
|
1787
|
+
title: captureType,
|
|
1788
|
+
allowClose: true,
|
|
1789
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-col md:flex-row gap-8 items-stretch p-2", children: [
|
|
1790
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "flex-1 flex justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
|
|
1791
|
+
"div",
|
|
1792
|
+
{
|
|
1793
|
+
className: "\r\n relative w-full max-w-[280px] aspect-[3/4]\r\n rounded-md overflow-hidden\r\n bg-black/5 dark:bg-white/5\r\n border border-border/50 dark:border-white/10\r\n shadow-inner flex items-center justify-center\r\n ",
|
|
1794
|
+
children: [
|
|
1795
|
+
isWaiting && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1796
|
+
import_framer_motion4.motion.div,
|
|
1797
|
+
{
|
|
1798
|
+
className: "absolute inset-x-0 h-[2px] bg-primary/80 z-10 shadow-[0_0_15px_2px_rgba(var(--primary),0.6)]",
|
|
1799
|
+
animate: { top: ["0%", "100%", "0%"] },
|
|
1800
|
+
transition: { duration: 3, repeat: Infinity, ease: "linear" }
|
|
1801
|
+
}
|
|
1802
|
+
),
|
|
1803
|
+
previewImage ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1804
|
+
import_framer_motion4.motion.img,
|
|
1805
|
+
{
|
|
1806
|
+
initial: { opacity: 0, scale: 0.95 },
|
|
1807
|
+
animate: { opacity: 1, scale: 1 },
|
|
1808
|
+
transition: { duration: 0.3 },
|
|
1809
|
+
src: previewImage,
|
|
1810
|
+
alt: "Vista previa",
|
|
1811
|
+
className: "object-contain w-full h-full p-4 rotate-180 drop-shadow-xl"
|
|
1812
|
+
}
|
|
1813
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-col items-center justify-center text-foreground/30 gap-4", children: [
|
|
1814
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "relative", children: [
|
|
1815
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_tb7.TbFingerprint, { size: 80, className: "animate-pulse" }),
|
|
1816
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1817
|
+
import_tb7.TbScan,
|
|
1818
|
+
{
|
|
1819
|
+
size: 30,
|
|
1820
|
+
className: "absolute -bottom-2 -right-2 text-primary animate-bounce"
|
|
1821
|
+
}
|
|
1822
|
+
)
|
|
1823
|
+
] }),
|
|
1824
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-col items-center gap-2", children: [
|
|
1825
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Loader, { size: 20 }),
|
|
1826
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "text-xs font-medium tracking-wide uppercase", children: "Esperando Esc\xE1ner" })
|
|
1827
|
+
] })
|
|
1828
|
+
] }),
|
|
1829
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1830
|
+
"div",
|
|
1831
|
+
{
|
|
1832
|
+
className: "absolute inset-0 pointer-events-none opacity-10",
|
|
1833
|
+
style: {
|
|
1834
|
+
backgroundImage: "radial-gradient(circle, currentColor 1px, transparent 1px)",
|
|
1835
|
+
backgroundSize: "20px 20px"
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
),
|
|
1839
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "absolute top-4 right-4", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(QualityBadge, { quality: previewQuality }) })
|
|
1840
|
+
]
|
|
1841
|
+
}
|
|
1842
|
+
) }),
|
|
1843
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex-1 flex flex-col justify-between min-h-[320px]", children: [
|
|
1844
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "space-y-6", children: [
|
|
1845
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "space-y-2", children: [
|
|
1846
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("h4", { className: "text-sm font-bold text-foreground uppercase tracking-wider flex items-center gap-2", children: [
|
|
1847
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "w-2 h-2 rounded-full bg-primary animate-pulse" }),
|
|
1848
|
+
"Estado del Dispositivo"
|
|
1849
|
+
] }),
|
|
1850
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "p-4 rounded-md bg-secondary/50 border border-border/50 text-sm text-foreground/80 leading-relaxed", children: isWaiting ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { children: "El dispositivo est\xE1 inicializando. Por favor, prepare al usuario para la captura." }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "text-quality-good font-medium", children: "\xA1Imagen capturada! Analizando calidad biom\xE9trica..." }) })
|
|
1851
|
+
] }),
|
|
1852
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "space-y-3", children: [
|
|
1853
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h4", { className: "text-xs font-semibold text-foreground/60 uppercase tracking-wide", children: "Instrucciones" }),
|
|
1854
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("ul", { className: "text-sm text-foreground/70 space-y-2 list-disc list-inside marker:text-primary", children: [
|
|
1855
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("li", { children: "Coloque el dedo firmemente en el centro." }),
|
|
1856
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("li", { children: "Aplique presi\xF3n uniforme." }),
|
|
1857
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("li", { children: "Mantenga la posici\xF3n hasta escuchar el tono." })
|
|
1858
|
+
] })
|
|
1859
|
+
] })
|
|
1860
|
+
] }),
|
|
1861
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "mt-auto pt-6", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
|
|
1862
|
+
"button",
|
|
1863
|
+
{
|
|
1864
|
+
onClick: onCancel,
|
|
1865
|
+
className: "\r\n group w-full flex items-center justify-center gap-2 px-4 py-3.5\r\n rounded-md font-semibold text-sm\r\n bg-red-500/10 text-red-600 dark:text-red-400 \r\n border border-red-500/20\r\n hover:bg-red-500 hover:text-white hover:border-red-500\r\n transition-all duration-200 shadow-sm\r\n ",
|
|
1866
|
+
children: [
|
|
1867
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1868
|
+
import_tb7.TbX,
|
|
1869
|
+
{
|
|
1870
|
+
size: 18,
|
|
1871
|
+
className: "transition-transform group-hover:rotate-90"
|
|
1872
|
+
}
|
|
1873
|
+
),
|
|
1874
|
+
"Cancelar Captura"
|
|
1875
|
+
]
|
|
1876
|
+
}
|
|
1877
|
+
) })
|
|
1878
|
+
] })
|
|
1879
|
+
] })
|
|
1880
|
+
}
|
|
1881
|
+
);
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
// src/components/enrollment/FingerEnrollModule.tsx
|
|
1885
|
+
var import_tb8 = require("react-icons/tb");
|
|
1886
|
+
|
|
1887
|
+
// src/lib/constants/fingerPositions.ts
|
|
1888
|
+
var FINGER_NAMES = {
|
|
1889
|
+
RIGHT_THUMB: "Pulgar Der.",
|
|
1890
|
+
RIGHT_INDEX: "\xCDndice Der.",
|
|
1891
|
+
RIGHT_MIDDLE: "Medio Der.",
|
|
1892
|
+
RIGHT_RING: "Anular Der.",
|
|
1893
|
+
RIGHT_LITTLE: "Me\xF1ique Der.",
|
|
1894
|
+
LEFT_THUMB: "Pulgar Izq.",
|
|
1895
|
+
LEFT_INDEX: "\xCDndice Izq.",
|
|
1896
|
+
LEFT_MIDDLE: "Medio Izq.",
|
|
1897
|
+
LEFT_RING: "Anular Izq.",
|
|
1898
|
+
LEFT_LITTLE: "Me\xF1ique Izq."
|
|
1899
|
+
};
|
|
1900
|
+
var RIGHT_FINGERS = [
|
|
1901
|
+
"RIGHT_THUMB",
|
|
1902
|
+
"RIGHT_INDEX",
|
|
1903
|
+
"RIGHT_MIDDLE",
|
|
1904
|
+
"RIGHT_RING",
|
|
1905
|
+
"RIGHT_LITTLE"
|
|
1906
|
+
];
|
|
1907
|
+
var LEFT_FINGERS = [
|
|
1908
|
+
"LEFT_THUMB",
|
|
1909
|
+
"LEFT_INDEX",
|
|
1910
|
+
"LEFT_MIDDLE",
|
|
1911
|
+
"LEFT_RING",
|
|
1912
|
+
"LEFT_LITTLE"
|
|
1913
|
+
];
|
|
1914
|
+
|
|
1915
|
+
// src/components/enrollment/FingerEnrollModule.tsx
|
|
1916
|
+
var import_jsx_runtime12 = require("react/jsx-runtime");
|
|
1917
|
+
var CAPTURE_GROUPS_4_4_2 = [
|
|
1918
|
+
{ label: "Mano Derecha (4 dedos)", backendMode: "RightSlap4" },
|
|
1919
|
+
{ label: "Mano Izquierda (4 dedos)", backendMode: "LeftSlap4" },
|
|
1920
|
+
{ label: "Pulgares (2 dedos)", backendMode: "Thumbs2" }
|
|
1921
|
+
];
|
|
1922
|
+
var CAPTURE_GROUPS_2_2 = [
|
|
1923
|
+
{ label: "\xCDndice y Medio Der.", backendMode: "RightIndexMiddle" },
|
|
1924
|
+
{ label: "Anular y Me\xF1ique Der.", backendMode: "RightRingLittle" },
|
|
1925
|
+
{ label: "\xCDndice y Medio Izq.", backendMode: "LeftIndexMiddle" },
|
|
1926
|
+
{ label: "Anular y Me\xF1ique Izq.", backendMode: "LeftRingLittle" },
|
|
1927
|
+
{ label: "Ambos Pulgares", backendMode: "Thumbs2" }
|
|
1928
|
+
];
|
|
1929
|
+
var getFingersForMode = (backendMode) => {
|
|
1930
|
+
switch (backendMode) {
|
|
1931
|
+
case "RightSlap4":
|
|
1932
|
+
return ["RIGHT_INDEX", "RIGHT_MIDDLE", "RIGHT_RING", "RIGHT_LITTLE"];
|
|
1933
|
+
case "LeftSlap4":
|
|
1934
|
+
return ["LEFT_INDEX", "LEFT_MIDDLE", "LEFT_RING", "LEFT_LITTLE"];
|
|
1935
|
+
case "Thumbs2":
|
|
1936
|
+
return ["RIGHT_THUMB", "LEFT_THUMB"];
|
|
1937
|
+
case "RightIndexMiddle":
|
|
1938
|
+
return ["RIGHT_INDEX", "RIGHT_MIDDLE"];
|
|
1939
|
+
case "RightRingLittle":
|
|
1940
|
+
return ["RIGHT_RING", "RIGHT_LITTLE"];
|
|
1941
|
+
case "LeftIndexMiddle":
|
|
1942
|
+
return ["LEFT_INDEX", "LEFT_MIDDLE"];
|
|
1943
|
+
case "LeftRingLittle":
|
|
1944
|
+
return ["LEFT_RING", "LEFT_LITTLE"];
|
|
1945
|
+
default:
|
|
1946
|
+
return [];
|
|
1947
|
+
}
|
|
1948
|
+
};
|
|
1949
|
+
function FingerEnrollModule({ className = "" }) {
|
|
1950
|
+
const {
|
|
1951
|
+
addFingerprint,
|
|
1952
|
+
addMissingFinger,
|
|
1953
|
+
removeMissingFinger,
|
|
1954
|
+
removeFingerprint,
|
|
1955
|
+
fingerprints: storeFingerprints,
|
|
1956
|
+
missingFingers: enrollmentMissingFingers
|
|
1957
|
+
} = useEnrollmentStore();
|
|
1958
|
+
const [fingerResults, setFingerResults] = (0, import_react5.useState)({});
|
|
1959
|
+
const [selectedMode, setSelectedMode] = (0, import_react5.useState)(
|
|
1960
|
+
"MODE_4_4_2" /* Mode4_4_2 */
|
|
1961
|
+
);
|
|
1962
|
+
const [isModalOpen, setIsModalOpen] = (0, import_react5.useState)(false);
|
|
1963
|
+
const isModalOpenRef = (0, import_react5.useRef)(isModalOpen);
|
|
1964
|
+
const [isProcessing, setIsProcessing] = (0, import_react5.useState)(false);
|
|
1965
|
+
const [currentCaptureInfo, setCurrentCaptureInfo] = (0, import_react5.useState)(null);
|
|
1966
|
+
const [previewImage, setPreviewImage] = (0, import_react5.useState)(null);
|
|
1967
|
+
const [previewQuality, setPreviewQuality] = (0, import_react5.useState)(null);
|
|
1968
|
+
const [fingerToMarkMissing, setFingerToMarkMissing] = (0, import_react5.useState)(
|
|
1969
|
+
null
|
|
1970
|
+
);
|
|
1971
|
+
const {
|
|
1972
|
+
isConnected,
|
|
1973
|
+
sendMessage,
|
|
1974
|
+
registerHandler,
|
|
1975
|
+
unregisterHandler,
|
|
1976
|
+
getExpectedFingers,
|
|
1977
|
+
markFingerAsMissing,
|
|
1978
|
+
restoreFinger,
|
|
1979
|
+
missingFingers,
|
|
1980
|
+
clearAllMissingFingers
|
|
1981
|
+
} = useBiometricStore();
|
|
1982
|
+
const addMessage = useUiStore((s) => s.addMessage);
|
|
1983
|
+
(0, import_react5.useEffect)(() => {
|
|
1984
|
+
const loaded = {};
|
|
1985
|
+
Object.entries(storeFingerprints).forEach(([key, item]) => {
|
|
1986
|
+
loaded[key] = {
|
|
1987
|
+
Finger: key,
|
|
1988
|
+
Quality: item.quality,
|
|
1989
|
+
Image: item.image,
|
|
1990
|
+
Error: null
|
|
1991
|
+
};
|
|
1992
|
+
});
|
|
1993
|
+
setFingerResults((prev) => ({ ...prev, ...loaded }));
|
|
1994
|
+
}, [storeFingerprints]);
|
|
1995
|
+
const allMissingFingers = (0, import_react5.useMemo)(
|
|
1996
|
+
() => ({ ...enrollmentMissingFingers, ...missingFingers }),
|
|
1997
|
+
[enrollmentMissingFingers, missingFingers]
|
|
1998
|
+
);
|
|
1999
|
+
const capturedCount = (0, import_react5.useMemo)(() => {
|
|
2000
|
+
const successCount = Object.values(fingerResults).filter(
|
|
2001
|
+
(r) => r && r.Quality >= 40 && !r.Error
|
|
2002
|
+
).length;
|
|
2003
|
+
const missingCount = Object.keys(allMissingFingers).length;
|
|
2004
|
+
return successCount + missingCount;
|
|
2005
|
+
}, [fingerResults, allMissingFingers]);
|
|
2006
|
+
const isCapturing = isProcessing || isModalOpen;
|
|
2007
|
+
const modeOptions = [
|
|
2008
|
+
{ value: "MODE_4_4_2" /* Mode4_4_2 */, label: "4\u20134\u20132 (Est\xE1ndar)", icon: "\u{1F44B}" },
|
|
2009
|
+
{ value: "MODE_2_2" /* Mode2_2 */, label: "2\u20132 (Pares)", icon: "\u270C\uFE0F" },
|
|
2010
|
+
{ value: "MODE_1_1" /* Mode1_1 */, label: "1\u20131 (Individual)", icon: "\u261D\uFE0F" }
|
|
2011
|
+
];
|
|
2012
|
+
(0, import_react5.useEffect)(() => {
|
|
2013
|
+
isModalOpenRef.current = isModalOpen;
|
|
2014
|
+
}, [isModalOpen]);
|
|
2015
|
+
(0, import_react5.useEffect)(() => {
|
|
2016
|
+
const handlePreview = (data) => {
|
|
2017
|
+
if (!isModalOpenRef.current) return;
|
|
2018
|
+
setPreviewImage(data.biometricData?.capturedImage || null);
|
|
2019
|
+
setPreviewQuality(data.biometricData?.quality || null);
|
|
2020
|
+
};
|
|
2021
|
+
const handleComplete = (data) => {
|
|
2022
|
+
setIsModalOpen(false);
|
|
2023
|
+
setIsProcessing(false);
|
|
2024
|
+
setCurrentCaptureInfo(null);
|
|
2025
|
+
setPreviewImage(null);
|
|
2026
|
+
setPreviewQuality(null);
|
|
2027
|
+
const segments = data.biometricData?.segments;
|
|
2028
|
+
if (Array.isArray(segments)) {
|
|
2029
|
+
const newRes = {};
|
|
2030
|
+
for (const seg of segments) {
|
|
2031
|
+
if (seg.finger) {
|
|
2032
|
+
newRes[seg.finger] = {
|
|
2033
|
+
Finger: seg.finger,
|
|
2034
|
+
Quality: seg.quality,
|
|
2035
|
+
Image: seg.image,
|
|
2036
|
+
Error: null
|
|
2037
|
+
};
|
|
2038
|
+
addMessage(
|
|
2039
|
+
`Huella ${FINGER_NAMES[seg.finger]} capturada (${seg.quality}).`
|
|
2040
|
+
);
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
setFingerResults((prev) => ({ ...prev, ...newRes }));
|
|
2044
|
+
import_react_hot_toast.toast.success("Captura completada");
|
|
2045
|
+
}
|
|
2046
|
+
};
|
|
2047
|
+
const handleError = (data) => {
|
|
2048
|
+
import_react_hot_toast.toast.error(data.error || data.messageType || "Error desconocido");
|
|
2049
|
+
addMessage(`Error captura: ${data.error}`);
|
|
2050
|
+
setIsModalOpen(false);
|
|
2051
|
+
setIsProcessing(false);
|
|
2052
|
+
setPreviewImage(null);
|
|
2053
|
+
setPreviewQuality(null);
|
|
2054
|
+
};
|
|
2055
|
+
const handleEnrollment = (data) => {
|
|
2056
|
+
setIsProcessing(false);
|
|
2057
|
+
const wsqs = data.biometricData?.wsqFiles || {};
|
|
2058
|
+
const count = Object.keys(wsqs).length;
|
|
2059
|
+
Object.entries(wsqs).forEach(([pos, base64]) => {
|
|
2060
|
+
const fingerInfo = fingerResults[pos];
|
|
2061
|
+
const imageToSave = fingerInfo?.Image || base64;
|
|
2062
|
+
const normalizedImage = normalizeImageForStorage(imageToSave);
|
|
2063
|
+
addFingerprint(pos, normalizedImage, fingerInfo?.Quality || 100);
|
|
2064
|
+
});
|
|
2065
|
+
Object.entries(missingFingers).forEach(([pos, reason]) => {
|
|
2066
|
+
addMissingFinger(pos, reason);
|
|
2067
|
+
});
|
|
2068
|
+
addMessage(`Procesamiento finalizado. ${count} huellas listas.`);
|
|
2069
|
+
import_react_hot_toast.toast.success("Huellas planas guardadas");
|
|
2070
|
+
};
|
|
2071
|
+
registerHandler("FINGER_PREVIEW_FRAME", handlePreview);
|
|
2072
|
+
registerHandler("FINGER_CAPTURE_COMPLETE", handleComplete);
|
|
2073
|
+
registerHandler("ENROLLMENT_COMPLETE", handleEnrollment);
|
|
2074
|
+
registerHandler("FINGER_QUALITY_LOW", handleError);
|
|
2075
|
+
registerHandler("DUPLICATE_FINGER", handleError);
|
|
2076
|
+
registerHandler("FINGER_ERROR", handleError);
|
|
2077
|
+
registerHandler("ERROR_DEVICE_BUSY", handleError);
|
|
2078
|
+
registerHandler("ERROR_DEVICE_NOT_FOUND", handleError);
|
|
2079
|
+
registerHandler("ERROR_CAPTURE_FAILED", handleError);
|
|
2080
|
+
registerHandler("ERROR", handleError);
|
|
2081
|
+
return () => {
|
|
2082
|
+
unregisterHandler("FINGER_PREVIEW_FRAME");
|
|
2083
|
+
unregisterHandler("FINGER_CAPTURE_COMPLETE");
|
|
2084
|
+
unregisterHandler("ENROLLMENT_COMPLETE");
|
|
2085
|
+
unregisterHandler("FINGER_QUALITY_LOW");
|
|
2086
|
+
unregisterHandler("DUPLICATE_FINGER");
|
|
2087
|
+
unregisterHandler("FINGER_ERROR");
|
|
2088
|
+
unregisterHandler("ERROR_DEVICE_BUSY");
|
|
2089
|
+
unregisterHandler("ERROR_DEVICE_NOT_FOUND");
|
|
2090
|
+
unregisterHandler("ERROR_CAPTURE_FAILED");
|
|
2091
|
+
unregisterHandler("ERROR");
|
|
2092
|
+
};
|
|
2093
|
+
}, [
|
|
2094
|
+
registerHandler,
|
|
2095
|
+
unregisterHandler,
|
|
2096
|
+
addMessage,
|
|
2097
|
+
addFingerprint,
|
|
2098
|
+
addMissingFinger,
|
|
2099
|
+
missingFingers,
|
|
2100
|
+
fingerResults
|
|
2101
|
+
]);
|
|
2102
|
+
const startCapture = (mode, info) => {
|
|
2103
|
+
if (!isConnected) {
|
|
2104
|
+
import_react_hot_toast.toast.error("Servicio no conectado.");
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
if (isProcessing) return;
|
|
2108
|
+
let fingersAttempting = [];
|
|
2109
|
+
if (info.type === "MODE_1_1" /* Mode1_1 */) {
|
|
2110
|
+
fingersAttempting = [info.targetFinger];
|
|
2111
|
+
} else {
|
|
2112
|
+
fingersAttempting = getFingersForMode(mode);
|
|
2113
|
+
}
|
|
2114
|
+
const fingersWithoutEnrollmentMissing = fingersAttempting.filter(
|
|
2115
|
+
(f) => !enrollmentMissingFingers[f]
|
|
2116
|
+
);
|
|
2117
|
+
const expectedFingers = getExpectedFingers(fingersWithoutEnrollmentMissing);
|
|
2118
|
+
if (expectedFingers.length === 0) {
|
|
2119
|
+
const msg = "Todos los dedos de este grupo est\xE1n marcados como omitidos/faltantes.";
|
|
2120
|
+
import_react_hot_toast.toast.error(msg);
|
|
2121
|
+
addMessage(msg);
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
2124
|
+
setIsProcessing(true);
|
|
2125
|
+
setIsModalOpen(true);
|
|
2126
|
+
setCurrentCaptureInfo(info);
|
|
2127
|
+
setPreviewImage(null);
|
|
2128
|
+
setPreviewQuality(null);
|
|
2129
|
+
sendMessage({
|
|
2130
|
+
messageType: "START_FINGER_CAPTURE",
|
|
2131
|
+
biometricData: {
|
|
2132
|
+
mode,
|
|
2133
|
+
targetFinger: info?.targetFinger || null,
|
|
2134
|
+
expectedFingers
|
|
2135
|
+
}
|
|
2136
|
+
});
|
|
2137
|
+
addMessage(`Iniciando captura: ${mode}.`);
|
|
2138
|
+
};
|
|
2139
|
+
const cancelCapture = () => {
|
|
2140
|
+
sendMessage({ messageType: "ABORT_FINGER_CAPTURE" });
|
|
2141
|
+
setIsModalOpen(false);
|
|
2142
|
+
setIsProcessing(false);
|
|
2143
|
+
setPreviewImage(null);
|
|
2144
|
+
setPreviewQuality(null);
|
|
2145
|
+
addMessage("Captura cancelada.");
|
|
2146
|
+
};
|
|
2147
|
+
const processBatch = () => {
|
|
2148
|
+
if (capturedCount < 10) {
|
|
2149
|
+
const msg = `Faltan dedos por procesar (${capturedCount}/10).`;
|
|
2150
|
+
import_react_hot_toast.toast.error(msg);
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
setIsProcessing(true);
|
|
2154
|
+
addMessage("Procesando huellas capturadas...");
|
|
2155
|
+
sendMessage({ messageType: "FINALIZE_ENROLLMENT" });
|
|
2156
|
+
};
|
|
2157
|
+
const resetAll = () => {
|
|
2158
|
+
setFingerResults({});
|
|
2159
|
+
setIsProcessing(false);
|
|
2160
|
+
clearAllMissingFingers();
|
|
2161
|
+
addMessage("M\xF3dulo reiniciado.");
|
|
2162
|
+
};
|
|
2163
|
+
const handleConfirmMissing = (reason) => {
|
|
2164
|
+
if (fingerToMarkMissing) {
|
|
2165
|
+
markFingerAsMissing(fingerToMarkMissing, reason);
|
|
2166
|
+
addMissingFinger(fingerToMarkMissing, reason);
|
|
2167
|
+
setFingerResults((prev) => {
|
|
2168
|
+
const next = { ...prev };
|
|
2169
|
+
delete next[fingerToMarkMissing];
|
|
2170
|
+
return next;
|
|
2171
|
+
});
|
|
2172
|
+
removeFingerprint(fingerToMarkMissing);
|
|
2173
|
+
addMessage(`Dedo ${fingerToMarkMissing} marcado como: ${reason}`);
|
|
2174
|
+
setFingerToMarkMissing(null);
|
|
2175
|
+
}
|
|
2176
|
+
};
|
|
2177
|
+
const renderFingerGrid = (fingers) => fingers.map((f) => {
|
|
2178
|
+
const res = fingerResults[f];
|
|
2179
|
+
const missing = allMissingFingers[f] || null;
|
|
2180
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
2181
|
+
BiometricSlot,
|
|
2182
|
+
{
|
|
2183
|
+
position: f,
|
|
2184
|
+
title: FINGER_NAMES[f].split(" ")[0],
|
|
2185
|
+
status: res ? res.Error ? "error" : "captured" : "pending",
|
|
2186
|
+
imageUrl: getImageSrcForDisplay(res?.Image, "png"),
|
|
2187
|
+
quality: res?.Quality,
|
|
2188
|
+
iconType: "finger",
|
|
2189
|
+
missingReason: missing,
|
|
2190
|
+
onCapture: () => startCapture("SingleFinger", {
|
|
2191
|
+
type: "MODE_1_1" /* Mode1_1 */,
|
|
2192
|
+
targetFinger: f,
|
|
2193
|
+
label: FINGER_NAMES[f]
|
|
2194
|
+
}),
|
|
2195
|
+
onRequestMissing: () => setFingerToMarkMissing(f),
|
|
2196
|
+
onRestore: () => {
|
|
2197
|
+
removeMissingFinger(f);
|
|
2198
|
+
if (missingFingers[f]) restoreFinger(f);
|
|
2199
|
+
},
|
|
2200
|
+
onDelete: () => {
|
|
2201
|
+
removeFingerprint(f);
|
|
2202
|
+
setFingerResults((prev) => {
|
|
2203
|
+
const next = { ...prev };
|
|
2204
|
+
delete next[f];
|
|
2205
|
+
return next;
|
|
2206
|
+
});
|
|
2207
|
+
},
|
|
2208
|
+
isDisabled: isCapturing || !isConnected
|
|
2209
|
+
},
|
|
2210
|
+
f
|
|
2211
|
+
);
|
|
2212
|
+
});
|
|
2213
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "space-y-8 animate-in fade-in duration-500", children: [
|
|
2214
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
|
|
2215
|
+
ModuleHeader,
|
|
2216
|
+
{
|
|
2217
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_tb8.TbFingerprint, { size: 24 }),
|
|
2218
|
+
title: "Huellas Planas",
|
|
2219
|
+
subtitle: "Capture las 10 huellas digitales (Slaps).",
|
|
2220
|
+
progress: { current: capturedCount, total: 10 },
|
|
2221
|
+
children: [
|
|
2222
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
2223
|
+
Button,
|
|
2224
|
+
{
|
|
2225
|
+
variant: "success",
|
|
2226
|
+
size: "sm",
|
|
2227
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_tb8.TbSend, { size: 16 }),
|
|
2228
|
+
onClick: processBatch,
|
|
2229
|
+
disabled: !isConnected || isCapturing || capturedCount < 10,
|
|
2230
|
+
isLoading: isProcessing,
|
|
2231
|
+
children: "Guardar"
|
|
2232
|
+
}
|
|
2233
|
+
),
|
|
2234
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
2235
|
+
Button,
|
|
2236
|
+
{
|
|
2237
|
+
variant: "ghost",
|
|
2238
|
+
size: "sm",
|
|
2239
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_tb8.TbReload, { size: 16 }),
|
|
2240
|
+
onClick: resetAll,
|
|
2241
|
+
disabled: isCapturing,
|
|
2242
|
+
children: "Limpiar"
|
|
2243
|
+
}
|
|
2244
|
+
)
|
|
2245
|
+
]
|
|
2246
|
+
}
|
|
2247
|
+
),
|
|
2248
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex flex-col lg:flex-row gap-4 items-stretch lg:items-center bg-secondary/30 p-4 rounded-md border border-white/5", children: [
|
|
2249
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex flex-col sm:flex-row items-start sm:items-center gap-3 flex-1", children: [
|
|
2250
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("label", { className: "text-xs font-bold uppercase tracking-wider text-foreground/50 min-w-max", children: "Modo:" }),
|
|
2251
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "w-full sm:max-w-xs", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
2252
|
+
Select,
|
|
2253
|
+
{
|
|
2254
|
+
options: modeOptions,
|
|
2255
|
+
value: selectedMode,
|
|
2256
|
+
onChange: (val) => setSelectedMode(val),
|
|
2257
|
+
disabled: isCapturing
|
|
2258
|
+
}
|
|
2259
|
+
) })
|
|
2260
|
+
] }),
|
|
2261
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex gap-3 w-full sm:w-auto flex-wrap", children: [
|
|
2262
|
+
(selectedMode === "MODE_4_4_2" /* Mode4_4_2 */ ? CAPTURE_GROUPS_4_4_2 : selectedMode === "MODE_2_2" /* Mode2_2 */ ? CAPTURE_GROUPS_2_2 : []).map((g) => /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
|
|
2263
|
+
"button",
|
|
2264
|
+
{
|
|
2265
|
+
disabled: !isConnected || isCapturing,
|
|
2266
|
+
onClick: () => startCapture(g.backendMode, {
|
|
2267
|
+
type: selectedMode,
|
|
2268
|
+
label: g.label
|
|
2269
|
+
}),
|
|
2270
|
+
className: "flex items-center gap-2 px-4 py-2.5 rounded-md bg-background border border-border/60 text-foreground/70 font-medium text-xs hover:border-primary hover:text-primary hover:bg-primary/5 active:scale-95 transition-all duration-200 disabled:opacity-50",
|
|
2271
|
+
children: [
|
|
2272
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_tb8.TbHandClick, { size: 16 }),
|
|
2273
|
+
" ",
|
|
2274
|
+
g.label
|
|
2275
|
+
]
|
|
2276
|
+
},
|
|
2277
|
+
g.label
|
|
2278
|
+
)),
|
|
2279
|
+
selectedMode === "MODE_1_1" /* Mode1_1 */ && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "text-sm text-foreground/50 italic py-2", children: "Seleccione un dedo abajo para capturar individualmente." })
|
|
2280
|
+
] })
|
|
2281
|
+
] }),
|
|
2282
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "grid grid-cols-1 xl:grid-cols-2 gap-8", children: [
|
|
2283
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "glass p-6 rounded-sm border border-white/10 relative overflow-hidden group", children: [
|
|
2284
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_tb8.TbFingerprint, { size: 120 }) }),
|
|
2285
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("h3", { className: "text-lg font-bold text-foreground mb-5 pl-3 border-l-4 border-primary", children: "Mano Derecha" }),
|
|
2286
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-4", children: renderFingerGrid(RIGHT_FINGERS) })
|
|
2287
|
+
] }),
|
|
2288
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "glass p-6 rounded-sm border border-white/10 relative overflow-hidden group", children: [
|
|
2289
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_tb8.TbFingerprint, { size: 120 }) }),
|
|
2290
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("h3", { className: "text-lg font-bold text-foreground mb-5 pl-3 border-l-4 border-secondary-foreground/50", children: "Mano Izquierda" }),
|
|
2291
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-4", children: renderFingerGrid(LEFT_FINGERS) })
|
|
2292
|
+
] })
|
|
2293
|
+
] }),
|
|
2294
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
2295
|
+
FingerCaptureModal,
|
|
2296
|
+
{
|
|
2297
|
+
isOpen: isModalOpen,
|
|
2298
|
+
onCancel: cancelCapture,
|
|
2299
|
+
captureType: currentCaptureInfo?.label || "Captura de huella",
|
|
2300
|
+
previewImage,
|
|
2301
|
+
previewQuality
|
|
2302
|
+
}
|
|
2303
|
+
),
|
|
2304
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
2305
|
+
MissingFingerModal,
|
|
2306
|
+
{
|
|
2307
|
+
isOpen: !!fingerToMarkMissing,
|
|
2308
|
+
onClose: () => setFingerToMarkMissing(null),
|
|
2309
|
+
onConfirm: handleConfirmMissing,
|
|
2310
|
+
positionName: fingerToMarkMissing ? FINGER_NAMES[fingerToMarkMissing] : ""
|
|
2311
|
+
}
|
|
2312
|
+
)
|
|
2313
|
+
] });
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
// src/components/enrollment/FaceEnrollModule.tsx
|
|
2317
|
+
var import_react7 = require("react");
|
|
2318
|
+
var import_react_hot_toast3 = require("react-hot-toast");
|
|
2319
|
+
|
|
2320
|
+
// src/components/enrollment/FaceCameraModal.tsx
|
|
2321
|
+
var import_react6 = require("react");
|
|
2322
|
+
var import_react_hot_toast2 = require("react-hot-toast");
|
|
2323
|
+
var import_tb9 = require("react-icons/tb");
|
|
2324
|
+
var import_jsx_runtime13 = require("react/jsx-runtime");
|
|
2325
|
+
function FaceCameraModal({
|
|
2326
|
+
isOpen,
|
|
2327
|
+
onClose,
|
|
2328
|
+
onCaptured
|
|
2329
|
+
}) {
|
|
2330
|
+
const videoRef = (0, import_react6.useRef)(null);
|
|
2331
|
+
const canvasRef = (0, import_react6.useRef)(null);
|
|
2332
|
+
const [isCameraLoading, setIsCameraLoading] = (0, import_react6.useState)(true);
|
|
2333
|
+
const [isValidating, setIsValidating] = (0, import_react6.useState)(false);
|
|
2334
|
+
const [validationError, setValidationError] = (0, import_react6.useState)(null);
|
|
2335
|
+
const [tempImage, setTempImage] = (0, import_react6.useState)(null);
|
|
2336
|
+
const { sendMessage, registerHandler, unregisterHandler } = useBiometricStore();
|
|
2337
|
+
const addMessage = useUiStore((s) => s.addMessage);
|
|
2338
|
+
const startCamera = (0, import_react6.useCallback)(async () => {
|
|
2339
|
+
setIsCameraLoading(true);
|
|
2340
|
+
setValidationError(null);
|
|
2341
|
+
try {
|
|
2342
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
2343
|
+
video: {
|
|
2344
|
+
width: { ideal: 1920 },
|
|
2345
|
+
height: { ideal: 1080 },
|
|
2346
|
+
facingMode: "user"
|
|
2347
|
+
}
|
|
2348
|
+
});
|
|
2349
|
+
if (videoRef.current) {
|
|
2350
|
+
videoRef.current.srcObject = stream;
|
|
2351
|
+
videoRef.current.onloadedmetadata = () => setIsCameraLoading(false);
|
|
2352
|
+
}
|
|
2353
|
+
} catch (err) {
|
|
2354
|
+
const msg = "No se pudo acceder a la c\xE1mara. Verifique permisos.";
|
|
2355
|
+
import_react_hot_toast2.toast.error(msg);
|
|
2356
|
+
setValidationError(msg);
|
|
2357
|
+
setIsCameraLoading(false);
|
|
2358
|
+
}
|
|
2359
|
+
}, []);
|
|
2360
|
+
const stopCamera = (0, import_react6.useCallback)(() => {
|
|
2361
|
+
if (videoRef.current?.srcObject) {
|
|
2362
|
+
videoRef.current.srcObject.getTracks().forEach((t) => t.stop());
|
|
2363
|
+
videoRef.current.srcObject = null;
|
|
2364
|
+
}
|
|
2365
|
+
}, []);
|
|
2366
|
+
(0, import_react6.useEffect)(() => {
|
|
2367
|
+
if (isOpen) {
|
|
2368
|
+
const timer = setTimeout(() => {
|
|
2369
|
+
startCamera();
|
|
2370
|
+
}, 0);
|
|
2371
|
+
return () => clearTimeout(timer);
|
|
2372
|
+
} else {
|
|
2373
|
+
stopCamera();
|
|
2374
|
+
}
|
|
2375
|
+
return () => stopCamera();
|
|
2376
|
+
}, [isOpen, startCamera, stopCamera]);
|
|
2377
|
+
const captureAndValidate = () => {
|
|
2378
|
+
const video = videoRef.current;
|
|
2379
|
+
const canvas = canvasRef.current;
|
|
2380
|
+
if (!video || !canvas) return;
|
|
2381
|
+
setValidationError(null);
|
|
2382
|
+
canvas.width = video.videoWidth;
|
|
2383
|
+
canvas.height = video.videoHeight;
|
|
2384
|
+
const ctx = canvas.getContext("2d");
|
|
2385
|
+
ctx?.drawImage(video, 0, 0);
|
|
2386
|
+
const base64 = canvas.toDataURL("image/jpeg", 0.95).split(",")[1];
|
|
2387
|
+
setTempImage(base64);
|
|
2388
|
+
setIsValidating(true);
|
|
2389
|
+
sendMessage({
|
|
2390
|
+
messageType: "VALIDATE_FACE_IMAGE",
|
|
2391
|
+
Payload: base64
|
|
2392
|
+
});
|
|
2393
|
+
};
|
|
2394
|
+
(0, import_react6.useEffect)(() => {
|
|
2395
|
+
const onValidationResult = (msg) => {
|
|
2396
|
+
if (!isValidating) return;
|
|
2397
|
+
setIsValidating(false);
|
|
2398
|
+
const { isValid, message, qualityScore } = msg.payload;
|
|
2399
|
+
if (isValid) {
|
|
2400
|
+
import_react_hot_toast2.toast.success(`Foto aceptada (Calidad: ${qualityScore})`);
|
|
2401
|
+
addMessage(`\u2705 Foto aceptada. Calidad: ${qualityScore}`);
|
|
2402
|
+
setValidationError(null);
|
|
2403
|
+
if (tempImage) onCaptured(tempImage, qualityScore || 0);
|
|
2404
|
+
} else {
|
|
2405
|
+
setValidationError(message || "La imagen no cumple los est\xE1ndares.");
|
|
2406
|
+
addMessage(`\u274C Foto rechazada: ${message}`);
|
|
2407
|
+
setTempImage(null);
|
|
2408
|
+
}
|
|
2409
|
+
};
|
|
2410
|
+
registerHandler("FACE_VALIDATION_RESULT", onValidationResult);
|
|
2411
|
+
return () => unregisterHandler("FACE_VALIDATION_RESULT");
|
|
2412
|
+
}, [
|
|
2413
|
+
isValidating,
|
|
2414
|
+
tempImage,
|
|
2415
|
+
onCaptured,
|
|
2416
|
+
registerHandler,
|
|
2417
|
+
unregisterHandler,
|
|
2418
|
+
addMessage
|
|
2419
|
+
]);
|
|
2420
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
2421
|
+
Modal,
|
|
2422
|
+
{
|
|
2423
|
+
isOpen,
|
|
2424
|
+
onClose,
|
|
2425
|
+
title: "Captura de Rostro",
|
|
2426
|
+
allowClose: !isValidating,
|
|
2427
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex flex-col items-center w-full max-w-4xl mx-auto", children: [
|
|
2428
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
|
|
2429
|
+
"div",
|
|
2430
|
+
{
|
|
2431
|
+
className: `relative w-full aspect-video bg-black rounded-md overflow-hidden shadow-2xl border-2 transition-colors duration-300 ${validationError ? "border-red-500/50" : "border-white/10"}`,
|
|
2432
|
+
children: [
|
|
2433
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
2434
|
+
"video",
|
|
2435
|
+
{
|
|
2436
|
+
ref: videoRef,
|
|
2437
|
+
autoPlay: true,
|
|
2438
|
+
playsInline: true,
|
|
2439
|
+
muted: true,
|
|
2440
|
+
className: `w-full h-full object-cover transform scale-x-[-1] ${isValidating ? "opacity-50 blur-sm" : ""}`
|
|
2441
|
+
}
|
|
2442
|
+
),
|
|
2443
|
+
(isCameraLoading || isValidating) && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "absolute inset-0 flex flex-col items-center justify-center z-20 bg-black/40 backdrop-blur-sm", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
2444
|
+
Loader,
|
|
2445
|
+
{
|
|
2446
|
+
text: isValidating ? "Validando..." : "Iniciando c\xE1mara...",
|
|
2447
|
+
size: 50
|
|
2448
|
+
}
|
|
2449
|
+
) }),
|
|
2450
|
+
!isCameraLoading && !isValidating && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "absolute inset-0 pointer-events-none opacity-60", children: [
|
|
2451
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "absolute inset-0 m-auto w-[30%] h-[50%] border-2 border-dashed border-white/70 rounded-[40%]" }),
|
|
2452
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-4 h-4 text-white/80", children: "+" })
|
|
2453
|
+
] })
|
|
2454
|
+
]
|
|
2455
|
+
}
|
|
2456
|
+
),
|
|
2457
|
+
validationError && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "w-full mt-4 p-3 bg-red-500/10 border border-red-500/20 rounded-md flex items-center justify-center gap-3 animate-in fade-in slide-in-from-bottom-2", children: [
|
|
2458
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "p-2 bg-red-500/20 rounded-full text-red-500", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_tb9.TbAlertTriangle, { size: 20 }) }),
|
|
2459
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { className: "text-red-500 font-medium text-sm text-center", children: validationError })
|
|
2460
|
+
] }),
|
|
2461
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "w-full mt-6 flex justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
2462
|
+
"button",
|
|
2463
|
+
{
|
|
2464
|
+
onClick: captureAndValidate,
|
|
2465
|
+
disabled: isCameraLoading || isValidating,
|
|
2466
|
+
className: `flex items-center gap-3 px-12 py-4 rounded-full font-bold text-lg shadow-xl transition-all ${isCameraLoading || isValidating ? "bg-secondary text-foreground/50 cursor-wait" : "bg-primary text-white hover:scale-105 active:scale-95 shadow-primary/25"}`,
|
|
2467
|
+
children: isValidating ? "Analizando..." : /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_jsx_runtime13.Fragment, { children: [
|
|
2468
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_tb9.TbCamera, { size: 26 }),
|
|
2469
|
+
" ",
|
|
2470
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { children: "CAPTURAR" })
|
|
2471
|
+
] })
|
|
2472
|
+
}
|
|
2473
|
+
) }),
|
|
2474
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("canvas", { ref: canvasRef, className: "hidden" })
|
|
2475
|
+
] })
|
|
2476
|
+
}
|
|
2477
|
+
);
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
// src/components/enrollment/FaceEnrollModule.tsx
|
|
2481
|
+
var import_tb10 = require("react-icons/tb");
|
|
2482
|
+
var import_jsx_runtime14 = require("react/jsx-runtime");
|
|
2483
|
+
var INITIAL_SLOTS = Array(5).fill(null);
|
|
2484
|
+
function FaceEnrollModule({ className = "" }) {
|
|
2485
|
+
const {
|
|
2486
|
+
faceCaptures,
|
|
2487
|
+
setFaceCaptures,
|
|
2488
|
+
removeFace,
|
|
2489
|
+
addMissingFace,
|
|
2490
|
+
removeMissingFace,
|
|
2491
|
+
missingFaces
|
|
2492
|
+
} = useEnrollmentStore();
|
|
2493
|
+
const [slots, setSlots] = (0, import_react7.useState)(INITIAL_SLOTS);
|
|
2494
|
+
const [faceToMarkMissing, setFaceToMarkMissing] = (0, import_react7.useState)(
|
|
2495
|
+
null
|
|
2496
|
+
);
|
|
2497
|
+
const [finalTemplate, setFinalTemplate] = (0, import_react7.useState)(null);
|
|
2498
|
+
const [isModalOpen, setIsModalOpen] = (0, import_react7.useState)(false);
|
|
2499
|
+
const [activeSlot, setActiveSlot] = (0, import_react7.useState)(0);
|
|
2500
|
+
const [isProcessing, setIsProcessing] = (0, import_react7.useState)(false);
|
|
2501
|
+
const [jp2Images, setJp2Images] = (0, import_react7.useState)([]);
|
|
2502
|
+
const { isConnected, sendMessage, registerHandler, unregisterHandler } = useBiometricStore();
|
|
2503
|
+
const addMessage = useUiStore((s) => s.addMessage);
|
|
2504
|
+
(0, import_react7.useEffect)(() => {
|
|
2505
|
+
if (faceCaptures && faceCaptures.length > 0) {
|
|
2506
|
+
const newSlots = [...faceCaptures];
|
|
2507
|
+
while (newSlots.length < 5) newSlots.push(null);
|
|
2508
|
+
setSlots(newSlots);
|
|
2509
|
+
}
|
|
2510
|
+
}, [faceCaptures]);
|
|
2511
|
+
(0, import_react7.useEffect)(() => {
|
|
2512
|
+
const onComplete = (msg) => {
|
|
2513
|
+
setIsProcessing(false);
|
|
2514
|
+
const tmpl = msg.biometricData?.finalTemplate;
|
|
2515
|
+
const jp2List = msg.biometricData?.jp2Base64Images || [];
|
|
2516
|
+
setFinalTemplate(tmpl);
|
|
2517
|
+
setJp2Images(jp2List);
|
|
2518
|
+
const formattedFaces = jp2List.map((img) => ({
|
|
2519
|
+
image: normalizeImageForStorage(img),
|
|
2520
|
+
quality: 100
|
|
2521
|
+
}));
|
|
2522
|
+
setFaceCaptures(formattedFaces);
|
|
2523
|
+
addMessage("Plantilla facial generada exitosamente.");
|
|
2524
|
+
import_react_hot_toast3.toast.success("Enrolamiento facial completado");
|
|
2525
|
+
};
|
|
2526
|
+
const onError = (msg) => {
|
|
2527
|
+
setIsProcessing(false);
|
|
2528
|
+
const err = msg.error || "Error de validaci\xF3n";
|
|
2529
|
+
import_react_hot_toast3.toast.error(err, { duration: 5e3 });
|
|
2530
|
+
addMessage("Error: " + err);
|
|
2531
|
+
};
|
|
2532
|
+
registerHandler("FACE_ENROLLMENT_COMPLETE", onComplete);
|
|
2533
|
+
registerHandler("ERROR", onError);
|
|
2534
|
+
return () => {
|
|
2535
|
+
unregisterHandler("FACE_ENROLLMENT_COMPLETE");
|
|
2536
|
+
unregisterHandler("ERROR");
|
|
2537
|
+
};
|
|
2538
|
+
}, [registerHandler, unregisterHandler, addMessage, setFaceCaptures]);
|
|
2539
|
+
const capturedCount = (0, import_react7.useMemo)(() => {
|
|
2540
|
+
const captured = slots.filter((s) => s !== null).length;
|
|
2541
|
+
const missing = Object.keys(missingFaces).length;
|
|
2542
|
+
return captured + missing;
|
|
2543
|
+
}, [slots, missingFaces]);
|
|
2544
|
+
const openSlot = (idx) => {
|
|
2545
|
+
if (!isConnected) return import_react_hot_toast3.toast.error("Sistema desconectado.");
|
|
2546
|
+
setActiveSlot(idx);
|
|
2547
|
+
setIsModalOpen(true);
|
|
2548
|
+
};
|
|
2549
|
+
const handleCaptured = (img, quality) => {
|
|
2550
|
+
const next = [...slots];
|
|
2551
|
+
next[activeSlot] = {
|
|
2552
|
+
image: normalizeImageForStorage(img),
|
|
2553
|
+
quality
|
|
2554
|
+
};
|
|
2555
|
+
setSlots(next);
|
|
2556
|
+
setFaceCaptures(next);
|
|
2557
|
+
setIsModalOpen(false);
|
|
2558
|
+
};
|
|
2559
|
+
const handleDeleteFace = (index) => {
|
|
2560
|
+
const next = [...slots];
|
|
2561
|
+
next[index] = null;
|
|
2562
|
+
setSlots(next);
|
|
2563
|
+
removeFace(index);
|
|
2564
|
+
import_react_hot_toast3.toast.success(`\xC1ngulo ${index + 1} eliminado`);
|
|
2565
|
+
};
|
|
2566
|
+
const handleConfirmMissingFace = (reason) => {
|
|
2567
|
+
if (faceToMarkMissing !== null) {
|
|
2568
|
+
const next = [...slots];
|
|
2569
|
+
next[faceToMarkMissing] = null;
|
|
2570
|
+
setSlots(next);
|
|
2571
|
+
removeFace(faceToMarkMissing);
|
|
2572
|
+
addMissingFace(faceToMarkMissing, reason);
|
|
2573
|
+
setFaceToMarkMissing(null);
|
|
2574
|
+
}
|
|
2575
|
+
};
|
|
2576
|
+
const finalize = () => {
|
|
2577
|
+
const validSlots = slots.filter((s) => s !== null);
|
|
2578
|
+
const missingCount = Object.keys(missingFaces).length;
|
|
2579
|
+
const totalAccountedFor = validSlots.length + missingCount;
|
|
2580
|
+
const imgs = validSlots.map((s) => s.image);
|
|
2581
|
+
if (totalAccountedFor < 5) {
|
|
2582
|
+
return import_react_hot_toast3.toast.error(
|
|
2583
|
+
`Se requieren 5 \xE1ngulos (${validSlots.length} capturados, ${missingCount} omitidos).`
|
|
2584
|
+
);
|
|
2585
|
+
}
|
|
2586
|
+
if (imgs.length === 0) {
|
|
2587
|
+
return import_react_hot_toast3.toast.error("Capture al menos un \xE1ngulo facial.");
|
|
2588
|
+
}
|
|
2589
|
+
setIsProcessing(true);
|
|
2590
|
+
sendMessage({
|
|
2591
|
+
messageType: "FINALIZE_FACE_ENROLLMENT",
|
|
2592
|
+
biometricData: { faceImages: imgs }
|
|
2593
|
+
});
|
|
2594
|
+
};
|
|
2595
|
+
const downloadPackage = () => {
|
|
2596
|
+
if (jp2Images.length === 0)
|
|
2597
|
+
return import_react_hot_toast3.toast.error("No hay datos para descargar");
|
|
2598
|
+
const payload = { faceIsoImages: jp2Images, template: finalTemplate };
|
|
2599
|
+
const blob = new Blob([JSON.stringify(payload, null, 2)], {
|
|
2600
|
+
type: "application/json"
|
|
2601
|
+
});
|
|
2602
|
+
const url = URL.createObjectURL(blob);
|
|
2603
|
+
const a = document.createElement("a");
|
|
2604
|
+
a.href = url;
|
|
2605
|
+
a.download = `FACE_ENROLL_${(/* @__PURE__ */ new Date()).getTime()}.json`;
|
|
2606
|
+
a.click();
|
|
2607
|
+
};
|
|
2608
|
+
const resetAll = () => {
|
|
2609
|
+
setSlots(INITIAL_SLOTS);
|
|
2610
|
+
setFinalTemplate(null);
|
|
2611
|
+
setFaceCaptures(Array(5).fill(null));
|
|
2612
|
+
setJp2Images([]);
|
|
2613
|
+
setIsProcessing(false);
|
|
2614
|
+
};
|
|
2615
|
+
const isBusy = isProcessing || isModalOpen;
|
|
2616
|
+
return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "space-y-8 animate-in fade-in duration-500", children: [
|
|
2617
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
|
|
2618
|
+
ModuleHeader,
|
|
2619
|
+
{
|
|
2620
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_tb10.TbFaceId, { size: 24 }),
|
|
2621
|
+
title: "Enrolamiento Facial",
|
|
2622
|
+
subtitle: "Capture 5 \xE1ngulos de alta definici\xF3n para el registro facial.",
|
|
2623
|
+
progress: { current: capturedCount, total: 5 },
|
|
2624
|
+
children: [
|
|
2625
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
2626
|
+
Button,
|
|
2627
|
+
{
|
|
2628
|
+
variant: "success",
|
|
2629
|
+
size: "sm",
|
|
2630
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_tb10.TbSend, { size: 16 }),
|
|
2631
|
+
onClick: finalize,
|
|
2632
|
+
disabled: !isConnected || isBusy || capturedCount < 5,
|
|
2633
|
+
isLoading: isProcessing,
|
|
2634
|
+
children: "Guardar"
|
|
2635
|
+
}
|
|
2636
|
+
),
|
|
2637
|
+
finalTemplate && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
2638
|
+
Button,
|
|
2639
|
+
{
|
|
2640
|
+
variant: "secondary",
|
|
2641
|
+
size: "sm",
|
|
2642
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_tb10.TbDownload, { size: 16 }),
|
|
2643
|
+
onClick: downloadPackage,
|
|
2644
|
+
children: "Exportar"
|
|
2645
|
+
}
|
|
2646
|
+
),
|
|
2647
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
2648
|
+
Button,
|
|
2649
|
+
{
|
|
2650
|
+
variant: "ghost",
|
|
2651
|
+
size: "sm",
|
|
2652
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_tb10.TbReload, { size: 16 }),
|
|
2653
|
+
onClick: resetAll,
|
|
2654
|
+
disabled: isBusy,
|
|
2655
|
+
children: "Limpiar"
|
|
2656
|
+
}
|
|
2657
|
+
)
|
|
2658
|
+
]
|
|
2659
|
+
}
|
|
2660
|
+
),
|
|
2661
|
+
finalTemplate && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "glass p-8 rounded-sm text-center w-full relative overflow-hidden border border-green-500/20", children: [
|
|
2662
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "absolute inset-0 bg-gradient-to-b from-quality-good/10 to-transparent pointer-events-none" }),
|
|
2663
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_tb10.TbFaceId, { className: "mx-auto text-green-500 mb-4", size: 40 }),
|
|
2664
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("h3", { className: "text-xl font-bold text-foreground mb-2", children: "Biometr\xEDa Generada Correctamente" }),
|
|
2665
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "flex justify-center gap-3 mt-6", children: [
|
|
2666
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
2667
|
+
Button,
|
|
2668
|
+
{
|
|
2669
|
+
variant: "success",
|
|
2670
|
+
size: "md",
|
|
2671
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_tb10.TbDownload, { size: 18 }),
|
|
2672
|
+
onClick: downloadPackage,
|
|
2673
|
+
children: "Exportar JSON"
|
|
2674
|
+
}
|
|
2675
|
+
),
|
|
2676
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
2677
|
+
Button,
|
|
2678
|
+
{
|
|
2679
|
+
variant: "ghost",
|
|
2680
|
+
size: "md",
|
|
2681
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_tb10.TbReload, { size: 18 }),
|
|
2682
|
+
onClick: resetAll,
|
|
2683
|
+
children: "Nuevo Tr\xE1mite"
|
|
2684
|
+
}
|
|
2685
|
+
)
|
|
2686
|
+
] })
|
|
2687
|
+
] }),
|
|
2688
|
+
!finalTemplate && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "grid grid-cols-2 md:grid-cols-5 gap-4", children: slots.map((s, i) => {
|
|
2689
|
+
const isMissing = !!missingFaces[i];
|
|
2690
|
+
return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
2691
|
+
BiometricSlot,
|
|
2692
|
+
{
|
|
2693
|
+
title: `\xC1ngulo ${i + 1}`,
|
|
2694
|
+
position: `${i + 1}`,
|
|
2695
|
+
status: isMissing ? "pending" : s ? "captured" : "pending",
|
|
2696
|
+
imageUrl: s ? getImageSrcForDisplay(s.image, "jpeg") : null,
|
|
2697
|
+
quality: s?.quality,
|
|
2698
|
+
iconType: "face",
|
|
2699
|
+
onCapture: () => openSlot(i),
|
|
2700
|
+
onDelete: () => handleDeleteFace(i),
|
|
2701
|
+
onRequestMissing: () => setFaceToMarkMissing(i),
|
|
2702
|
+
onRestore: () => removeMissingFace(i),
|
|
2703
|
+
missingReason: missingFaces[i] || null,
|
|
2704
|
+
isDisabled: !isConnected || isBusy
|
|
2705
|
+
},
|
|
2706
|
+
i
|
|
2707
|
+
);
|
|
2708
|
+
}) }),
|
|
2709
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
2710
|
+
MissingBiometricModal,
|
|
2711
|
+
{
|
|
2712
|
+
isOpen: faceToMarkMissing !== null,
|
|
2713
|
+
onClose: () => setFaceToMarkMissing(null),
|
|
2714
|
+
onConfirm: handleConfirmMissingFace,
|
|
2715
|
+
positionName: faceToMarkMissing !== null ? `\xC1ngulo ${faceToMarkMissing + 1}` : "",
|
|
2716
|
+
biometricType: "face"
|
|
2717
|
+
}
|
|
2718
|
+
),
|
|
2719
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
2720
|
+
FaceCameraModal,
|
|
2721
|
+
{
|
|
2722
|
+
isOpen: isModalOpen,
|
|
2723
|
+
onClose: () => setIsModalOpen(false),
|
|
2724
|
+
onCaptured: handleCaptured
|
|
2725
|
+
}
|
|
2726
|
+
)
|
|
2727
|
+
] });
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
// src/components/enrollment/PalmEnrollModule.tsx
|
|
2731
|
+
var import_react8 = require("react");
|
|
2732
|
+
var import_react_hot_toast4 = require("react-hot-toast");
|
|
2733
|
+
|
|
2734
|
+
// src/components/enrollment/PalmCaptureModal.tsx
|
|
2735
|
+
var import_tb11 = require("react-icons/tb");
|
|
2736
|
+
var import_framer_motion5 = require("framer-motion");
|
|
2737
|
+
var import_jsx_runtime15 = require("react/jsx-runtime");
|
|
2738
|
+
function PalmCaptureModal({
|
|
2739
|
+
isOpen,
|
|
2740
|
+
onCancel,
|
|
2741
|
+
onRetry,
|
|
2742
|
+
captureType,
|
|
2743
|
+
previewImage,
|
|
2744
|
+
statusMessage,
|
|
2745
|
+
errorState
|
|
2746
|
+
}) {
|
|
2747
|
+
const isWaiting = !previewImage && !errorState;
|
|
2748
|
+
let statusStyles = "bg-primary/10 border-primary/20 text-primary";
|
|
2749
|
+
let statusIcon = /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Loader, { size: 18, className: "animate-spin" });
|
|
2750
|
+
if (errorState) {
|
|
2751
|
+
statusStyles = "bg-red-500/10 border-red-500/20 text-red-600 dark:text-red-400";
|
|
2752
|
+
statusIcon = /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_tb11.TbAlertTriangle, { size: 20 });
|
|
2753
|
+
} else if (statusMessage.toLowerCase().includes("\xE9xito") || statusMessage.toLowerCase().includes("capturada")) {
|
|
2754
|
+
statusStyles = "bg-green-500/10 border-green-500/20 text-green-600 dark:text-green-400";
|
|
2755
|
+
statusIcon = /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_tb11.TbCheck, { size: 20 });
|
|
2756
|
+
}
|
|
2757
|
+
return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
2758
|
+
Modal,
|
|
2759
|
+
{
|
|
2760
|
+
isOpen,
|
|
2761
|
+
onClose: onCancel,
|
|
2762
|
+
title: captureType,
|
|
2763
|
+
allowClose: true,
|
|
2764
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex flex-col md:flex-row gap-8 items-stretch p-2", children: [
|
|
2765
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "flex-1 flex justify-center bg-card/50 rounded-md p-4 border border-border/50 shadow-sm", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
|
|
2766
|
+
"div",
|
|
2767
|
+
{
|
|
2768
|
+
className: "\r\n relative w-full max-w-[320px] aspect-[4/5]\r\n rounded-md overflow-hidden\r\n bg-zinc-950 border border-white/10\r\n shadow-inner flex items-center justify-center\r\n ",
|
|
2769
|
+
children: [
|
|
2770
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
2771
|
+
"div",
|
|
2772
|
+
{
|
|
2773
|
+
className: "absolute inset-0 pointer-events-none opacity-20",
|
|
2774
|
+
style: {
|
|
2775
|
+
backgroundImage: "linear-gradient(0deg, transparent 24%, #444 25%, #444 26%, transparent 27%, transparent 74%, #444 75%, #444 76%, transparent 77%, transparent), linear-gradient(90deg, transparent 24%, #444 25%, #444 26%, transparent 27%, transparent 74%, #444 75%, #444 76%, transparent 77%, transparent)",
|
|
2776
|
+
backgroundSize: "40px 40px"
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
),
|
|
2780
|
+
isWaiting && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
2781
|
+
import_framer_motion5.motion.div,
|
|
2782
|
+
{
|
|
2783
|
+
className: "absolute inset-x-0 h-[2px] bg-primary z-10 shadow-[0_0_15px_2px_rgba(var(--primary),0.8)]",
|
|
2784
|
+
animate: { top: ["0%", "100%", "0%"] },
|
|
2785
|
+
transition: { duration: 4, repeat: Infinity, ease: "linear" }
|
|
2786
|
+
}
|
|
2787
|
+
),
|
|
2788
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "relative z-1 w-full h-full flex items-center justify-center p-4", children: previewImage ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
2789
|
+
"img",
|
|
2790
|
+
{
|
|
2791
|
+
src: previewImage,
|
|
2792
|
+
alt: "Vista previa",
|
|
2793
|
+
className: "object-contain w-full h-full"
|
|
2794
|
+
}
|
|
2795
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex flex-col items-center justify-center text-white/20 gap-4", children: [
|
|
2796
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "relative", children: [
|
|
2797
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_tb11.TbHandStop, { size: 90, className: "opacity-60" }),
|
|
2798
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
2799
|
+
import_tb11.TbScan,
|
|
2800
|
+
{
|
|
2801
|
+
size: 30,
|
|
2802
|
+
className: "absolute -bottom-2 -right-2 text-primary animate-pulse"
|
|
2803
|
+
}
|
|
2804
|
+
)
|
|
2805
|
+
] }),
|
|
2806
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-[10px] font-bold tracking-[0.25em] uppercase opacity-60 text-center", children: "Esperando Esc\xE1ner" })
|
|
2807
|
+
] }) })
|
|
2808
|
+
]
|
|
2809
|
+
}
|
|
2810
|
+
) }),
|
|
2811
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex-1 flex flex-col justify-between gap-6 min-h-[340px]", children: [
|
|
2812
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "space-y-6", children: [
|
|
2813
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "space-y-2", children: [
|
|
2814
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("h4", { className: "text-xs font-bold text-muted-foreground uppercase tracking-wider", children: "Estado del Dispositivo" }),
|
|
2815
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
|
|
2816
|
+
"div",
|
|
2817
|
+
{
|
|
2818
|
+
className: `
|
|
2819
|
+
p-4 rounded-md border flex items-start gap-3 shadow-sm
|
|
2820
|
+
transition-colors duration-200 ${statusStyles}
|
|
2821
|
+
`,
|
|
2822
|
+
children: [
|
|
2823
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "mt-0.5 shrink-0", children: statusIcon }),
|
|
2824
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-sm font-medium leading-snug", children: statusMessage })
|
|
2825
|
+
]
|
|
2826
|
+
}
|
|
2827
|
+
)
|
|
2828
|
+
] }),
|
|
2829
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "space-y-3", children: [
|
|
2830
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("h4", { className: "text-xs font-semibold text-muted-foreground uppercase tracking-wide flex items-center gap-2", children: [
|
|
2831
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_tb11.TbScan, { className: "text-primary" }),
|
|
2832
|
+
" Instrucciones"
|
|
2833
|
+
] }),
|
|
2834
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "bg-secondary/30 border border-border rounded-md p-4", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("ul", { className: "text-sm text-foreground/80 space-y-3", children: [
|
|
2835
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("li", { className: "flex gap-3", children: [
|
|
2836
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "w-1.5 h-1.5 rounded-full bg-primary mt-2 shrink-0" }),
|
|
2837
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { children: "Retire anillos y accesorios de la mano." })
|
|
2838
|
+
] }),
|
|
2839
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("li", { className: "flex gap-3", children: [
|
|
2840
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "w-1.5 h-1.5 rounded-full bg-primary mt-2 shrink-0" }),
|
|
2841
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { children: "Mantenga la palma plana contra la superficie." })
|
|
2842
|
+
] }),
|
|
2843
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("li", { className: "flex gap-3", children: [
|
|
2844
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "w-1.5 h-1.5 rounded-full bg-primary mt-2 shrink-0" }),
|
|
2845
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { children: "Junte los dedos firmemente." })
|
|
2846
|
+
] })
|
|
2847
|
+
] }) })
|
|
2848
|
+
] })
|
|
2849
|
+
] }),
|
|
2850
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "flex gap-3 pt-4 border-t border-border", children: errorState ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
|
|
2851
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
2852
|
+
"button",
|
|
2853
|
+
{
|
|
2854
|
+
onClick: onCancel,
|
|
2855
|
+
className: "flex-1 px-4 py-3 rounded-md font-medium text-sm border border-border hover:bg-secondary transition-colors",
|
|
2856
|
+
children: "Cancelar"
|
|
2857
|
+
}
|
|
2858
|
+
),
|
|
2859
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
|
|
2860
|
+
"button",
|
|
2861
|
+
{
|
|
2862
|
+
onClick: onRetry,
|
|
2863
|
+
className: "flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-md font-medium text-sm bg-primary text-primary-foreground hover:bg-primary/90 transition-all shadow-lg shadow-primary/20",
|
|
2864
|
+
children: [
|
|
2865
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_tb11.TbReload, { size: 18 }),
|
|
2866
|
+
" Reintentar"
|
|
2867
|
+
]
|
|
2868
|
+
}
|
|
2869
|
+
)
|
|
2870
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
|
|
2871
|
+
"button",
|
|
2872
|
+
{
|
|
2873
|
+
onClick: onCancel,
|
|
2874
|
+
className: "\r\n w-full flex items-center justify-center gap-2 px-4 py-3\r\n rounded-md font-semibold text-sm\r\n bg-secondary hover:bg-red-500/10 \r\n text-foreground hover:text-red-600\r\n border border-transparent hover:border-red-500/20\r\n transition-all duration-200\r\n ",
|
|
2875
|
+
children: [
|
|
2876
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_tb11.TbX, { size: 18 }),
|
|
2877
|
+
"Cancelar Captura"
|
|
2878
|
+
]
|
|
2879
|
+
}
|
|
2880
|
+
) })
|
|
2881
|
+
] })
|
|
2882
|
+
] })
|
|
2883
|
+
}
|
|
2884
|
+
);
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
// src/components/enrollment/PalmEnrollModule.tsx
|
|
2888
|
+
var import_tb12 = require("react-icons/tb");
|
|
2889
|
+
var import_jsx_runtime16 = require("react/jsx-runtime");
|
|
2890
|
+
var PALM_NAMES = {
|
|
2891
|
+
UPPERPALMLEFT: "Superior Izq.",
|
|
2892
|
+
LOWERPALMLEFT: "Inferior Izq.",
|
|
2893
|
+
WRITERSPALMLEFT: "Escritor Izq.",
|
|
2894
|
+
UPPERPALMRIGHT: "Superior Der.",
|
|
2895
|
+
LOWERPALMRIGHT: "Inferior Der.",
|
|
2896
|
+
WRITERSPALMRIGHT: "Escritor Der."
|
|
2897
|
+
};
|
|
2898
|
+
var LEFT_PALMS = ["UPPERPALMLEFT", "LOWERPALMLEFT", "WRITERSPALMLEFT"];
|
|
2899
|
+
var RIGHT_PALMS = ["UPPERPALMRIGHT", "LOWERPALMRIGHT", "WRITERSPALMRIGHT"];
|
|
2900
|
+
function PalmEnrollModule({ className = "" }) {
|
|
2901
|
+
const {
|
|
2902
|
+
addPalm,
|
|
2903
|
+
removePalm,
|
|
2904
|
+
addMissingPalm,
|
|
2905
|
+
removeMissingPalm,
|
|
2906
|
+
missingPalms,
|
|
2907
|
+
palms: storePalms
|
|
2908
|
+
} = useEnrollmentStore();
|
|
2909
|
+
const [palmResults, setPalmResults] = (0, import_react8.useState)({});
|
|
2910
|
+
const [finalTemplate, setFinalTemplate] = (0, import_react8.useState)(null);
|
|
2911
|
+
const [palmWsqs, setPalmWsqs] = (0, import_react8.useState)({});
|
|
2912
|
+
const [isModalOpen, setIsModalOpen] = (0, import_react8.useState)(false);
|
|
2913
|
+
const isModalOpenRef = (0, import_react8.useRef)(isModalOpen);
|
|
2914
|
+
const [isProcessing, setIsProcessing] = (0, import_react8.useState)(false);
|
|
2915
|
+
const [currentPalm, setCurrentPalm] = (0, import_react8.useState)(null);
|
|
2916
|
+
const [previewImage, setPreviewImage] = (0, import_react8.useState)(null);
|
|
2917
|
+
const [modalStatus, setModalStatus] = (0, import_react8.useState)("Esperando...");
|
|
2918
|
+
const [modalError, setModalError] = (0, import_react8.useState)(false);
|
|
2919
|
+
const [pendingPalm, setPendingPalm] = (0, import_react8.useState)(null);
|
|
2920
|
+
const [showReplaceConfirm, setShowReplaceConfirm] = (0, import_react8.useState)(false);
|
|
2921
|
+
const [palmToMarkMissing, setPalmToMarkMissing] = (0, import_react8.useState)(
|
|
2922
|
+
null
|
|
2923
|
+
);
|
|
2924
|
+
const { isConnected, sendMessage, registerHandler, unregisterHandler } = useBiometricStore();
|
|
2925
|
+
const addMessage = useUiStore((s) => s.addMessage);
|
|
2926
|
+
(0, import_react8.useEffect)(() => {
|
|
2927
|
+
const loaded = {};
|
|
2928
|
+
Object.entries(storePalms).forEach(([key, item]) => {
|
|
2929
|
+
loaded[key] = {
|
|
2930
|
+
finger: key,
|
|
2931
|
+
quality: item.quality,
|
|
2932
|
+
image: item.image,
|
|
2933
|
+
isCaptured: true,
|
|
2934
|
+
error: null
|
|
2935
|
+
};
|
|
2936
|
+
});
|
|
2937
|
+
setPalmResults((prev) => ({ ...prev, ...loaded }));
|
|
2938
|
+
}, [storePalms]);
|
|
2939
|
+
const capturedCount = (0, import_react8.useMemo)(() => {
|
|
2940
|
+
const captured = Object.values(palmResults).filter(
|
|
2941
|
+
(p) => p?.isCaptured
|
|
2942
|
+
).length;
|
|
2943
|
+
const missing = Object.keys(missingPalms).length;
|
|
2944
|
+
return captured + missing;
|
|
2945
|
+
}, [palmResults, missingPalms]);
|
|
2946
|
+
const isCapturing = isProcessing || isModalOpen;
|
|
2947
|
+
(0, import_react8.useEffect)(() => {
|
|
2948
|
+
isModalOpenRef.current = isModalOpen;
|
|
2949
|
+
}, [isModalOpen]);
|
|
2950
|
+
(0, import_react8.useEffect)(() => {
|
|
2951
|
+
const onPreview = (d) => {
|
|
2952
|
+
if (!isModalOpenRef.current) return;
|
|
2953
|
+
setPreviewImage(d.payload);
|
|
2954
|
+
};
|
|
2955
|
+
const onStatus = (d) => {
|
|
2956
|
+
if (!isModalOpenRef.current) return;
|
|
2957
|
+
setModalStatus(d.payload.message);
|
|
2958
|
+
};
|
|
2959
|
+
const onComplete = (d) => {
|
|
2960
|
+
const seg = d.biometricData?.segments?.[0];
|
|
2961
|
+
if (seg && seg.finger === currentPalm) {
|
|
2962
|
+
setPalmResults((prev) => ({
|
|
2963
|
+
...prev,
|
|
2964
|
+
[seg.finger]: { ...seg, isCaptured: true }
|
|
2965
|
+
}));
|
|
2966
|
+
}
|
|
2967
|
+
import_react_hot_toast4.toast.success("Palma capturada");
|
|
2968
|
+
setIsProcessing(false);
|
|
2969
|
+
setTimeout(() => setIsModalOpen(false), 500);
|
|
2970
|
+
};
|
|
2971
|
+
const onError = (d) => {
|
|
2972
|
+
const msg = d.error || "Error durante la captura";
|
|
2973
|
+
import_react_hot_toast4.toast.error(msg);
|
|
2974
|
+
setModalError(true);
|
|
2975
|
+
setModalStatus(msg);
|
|
2976
|
+
setIsProcessing(false);
|
|
2977
|
+
};
|
|
2978
|
+
const onTemplate = (d) => {
|
|
2979
|
+
const tmpl = d.biometricData?.finalTemplate || "";
|
|
2980
|
+
const wsqFiles = d.biometricData?.wsqFiles || {};
|
|
2981
|
+
setPalmWsqs(wsqFiles);
|
|
2982
|
+
setFinalTemplate(tmpl);
|
|
2983
|
+
Object.entries(wsqFiles).forEach(([pos, base64]) => {
|
|
2984
|
+
const cached = palmResults[pos];
|
|
2985
|
+
const imageToSave = cached?.image || base64;
|
|
2986
|
+
addPalm(pos, imageToSave, cached?.quality || 100);
|
|
2987
|
+
});
|
|
2988
|
+
addMessage("Plantilla de palmas generada exitosamente.");
|
|
2989
|
+
import_react_hot_toast4.toast.success("Plantilla generada");
|
|
2990
|
+
};
|
|
2991
|
+
registerHandler("PALM_PREVIEW_FRAME", onPreview);
|
|
2992
|
+
registerHandler("PALM_CAPTURE_STATUS", onStatus);
|
|
2993
|
+
registerHandler("PALM_CAPTURE_COMPLETE", onComplete);
|
|
2994
|
+
registerHandler("PALM_TEMPLATE_GENERATED", onTemplate);
|
|
2995
|
+
registerHandler("PALM_ERROR", onError);
|
|
2996
|
+
registerHandler("ERROR", onError);
|
|
2997
|
+
return () => {
|
|
2998
|
+
unregisterHandler("PALM_PREVIEW_FRAME");
|
|
2999
|
+
unregisterHandler("PALM_CAPTURE_STATUS");
|
|
3000
|
+
unregisterHandler("PALM_CAPTURE_COMPLETE");
|
|
3001
|
+
unregisterHandler("PALM_TEMPLATE_GENERATED");
|
|
3002
|
+
unregisterHandler("PALM_ERROR");
|
|
3003
|
+
unregisterHandler("ERROR");
|
|
3004
|
+
};
|
|
3005
|
+
}, [
|
|
3006
|
+
currentPalm,
|
|
3007
|
+
registerHandler,
|
|
3008
|
+
unregisterHandler,
|
|
3009
|
+
addMessage,
|
|
3010
|
+
addPalm,
|
|
3011
|
+
palmResults
|
|
3012
|
+
]);
|
|
3013
|
+
const openCapture = (pos) => {
|
|
3014
|
+
setPreviewImage(null);
|
|
3015
|
+
setModalError(false);
|
|
3016
|
+
setModalStatus("Iniciando esc\xE1ner...");
|
|
3017
|
+
setCurrentPalm(pos);
|
|
3018
|
+
setIsModalOpen(true);
|
|
3019
|
+
setIsProcessing(true);
|
|
3020
|
+
sendMessage({
|
|
3021
|
+
messageType: "START_PALM_CAPTURE",
|
|
3022
|
+
biometricData: { targetFinger: pos }
|
|
3023
|
+
});
|
|
3024
|
+
};
|
|
3025
|
+
const startCapture = (pos) => {
|
|
3026
|
+
if (!isConnected || isCapturing) return;
|
|
3027
|
+
if (palmResults[pos]?.isCaptured) {
|
|
3028
|
+
setPendingPalm(pos);
|
|
3029
|
+
setShowReplaceConfirm(true);
|
|
3030
|
+
return;
|
|
3031
|
+
}
|
|
3032
|
+
openCapture(pos);
|
|
3033
|
+
};
|
|
3034
|
+
const retryCapture = () => {
|
|
3035
|
+
if (!currentPalm) return;
|
|
3036
|
+
openCapture(currentPalm);
|
|
3037
|
+
};
|
|
3038
|
+
const cancelCapture = () => {
|
|
3039
|
+
sendMessage({ messageType: "ABORT_PALM_CAPTURE" });
|
|
3040
|
+
setIsModalOpen(false);
|
|
3041
|
+
setIsProcessing(false);
|
|
3042
|
+
setModalError(false);
|
|
3043
|
+
setPreviewImage(null);
|
|
3044
|
+
setCurrentPalm(null);
|
|
3045
|
+
};
|
|
3046
|
+
const handleDeletePalm = (pos) => {
|
|
3047
|
+
setPalmResults((prev) => {
|
|
3048
|
+
const next = { ...prev };
|
|
3049
|
+
delete next[pos];
|
|
3050
|
+
return next;
|
|
3051
|
+
});
|
|
3052
|
+
removePalm(pos);
|
|
3053
|
+
import_react_hot_toast4.toast.success(`Palma ${PALM_NAMES[pos]} eliminada`);
|
|
3054
|
+
};
|
|
3055
|
+
const handleConfirmMissingPalm = (reason) => {
|
|
3056
|
+
if (palmToMarkMissing) {
|
|
3057
|
+
setPalmResults((prev) => {
|
|
3058
|
+
const next = { ...prev };
|
|
3059
|
+
delete next[palmToMarkMissing];
|
|
3060
|
+
return next;
|
|
3061
|
+
});
|
|
3062
|
+
removePalm(palmToMarkMissing);
|
|
3063
|
+
addMissingPalm(palmToMarkMissing, reason);
|
|
3064
|
+
setPalmToMarkMissing(null);
|
|
3065
|
+
}
|
|
3066
|
+
};
|
|
3067
|
+
const finalize = () => {
|
|
3068
|
+
if (capturedCount === 0)
|
|
3069
|
+
return import_react_hot_toast4.toast.error("Capture al menos una palma.");
|
|
3070
|
+
setIsProcessing(true);
|
|
3071
|
+
sendMessage({ messageType: "FINALIZE_PALM_ENROLLMENT" });
|
|
3072
|
+
};
|
|
3073
|
+
const resetState = () => {
|
|
3074
|
+
setPalmResults({});
|
|
3075
|
+
setFinalTemplate(null);
|
|
3076
|
+
setIsProcessing(false);
|
|
3077
|
+
};
|
|
3078
|
+
const downloadTemplate = () => {
|
|
3079
|
+
if (!finalTemplate) return;
|
|
3080
|
+
const payload = {
|
|
3081
|
+
template: finalTemplate,
|
|
3082
|
+
wsqImages: palmWsqs
|
|
3083
|
+
};
|
|
3084
|
+
const blob = new Blob([JSON.stringify(payload, null, 2)], {
|
|
3085
|
+
type: "application/json"
|
|
3086
|
+
});
|
|
3087
|
+
const url = URL.createObjectURL(blob);
|
|
3088
|
+
const a = document.createElement("a");
|
|
3089
|
+
a.href = url;
|
|
3090
|
+
a.download = "paquete_palmas_abis.json";
|
|
3091
|
+
a.click();
|
|
3092
|
+
URL.revokeObjectURL(url);
|
|
3093
|
+
};
|
|
3094
|
+
const renderPalmGrid = (palms) => palms.map((pos) => {
|
|
3095
|
+
const r = palmResults[pos];
|
|
3096
|
+
const isMissing = !!missingPalms[pos];
|
|
3097
|
+
const status = isMissing ? "pending" : r ? r.error ? "error" : "captured" : "pending";
|
|
3098
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
3099
|
+
BiometricSlot,
|
|
3100
|
+
{
|
|
3101
|
+
position: pos,
|
|
3102
|
+
title: PALM_NAMES[pos],
|
|
3103
|
+
status,
|
|
3104
|
+
imageUrl: getImageSrcForDisplay(r?.image, "png"),
|
|
3105
|
+
quality: r?.quality,
|
|
3106
|
+
iconType: "palm",
|
|
3107
|
+
onCapture: () => startCapture(pos),
|
|
3108
|
+
onDelete: () => handleDeletePalm(pos),
|
|
3109
|
+
onRequestMissing: () => setPalmToMarkMissing(pos),
|
|
3110
|
+
onRestore: () => removeMissingPalm(pos),
|
|
3111
|
+
missingReason: missingPalms[pos] || null,
|
|
3112
|
+
isDisabled: !isConnected || isCapturing
|
|
3113
|
+
},
|
|
3114
|
+
pos
|
|
3115
|
+
);
|
|
3116
|
+
});
|
|
3117
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "space-y-8 animate-in fade-in duration-500", children: [
|
|
3118
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
|
|
3119
|
+
ModuleHeader,
|
|
3120
|
+
{
|
|
3121
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_tb12.TbHandStop, { size: 24 }),
|
|
3122
|
+
title: "Enrolamiento de Palmas",
|
|
3123
|
+
subtitle: "Capture las palmas superiores, inferiores y de escritor de ambas manos.",
|
|
3124
|
+
progress: { current: capturedCount, total: 6 },
|
|
3125
|
+
children: [
|
|
3126
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
3127
|
+
Button,
|
|
3128
|
+
{
|
|
3129
|
+
variant: "success",
|
|
3130
|
+
size: "sm",
|
|
3131
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_tb12.TbSend, { size: 16 }),
|
|
3132
|
+
onClick: finalize,
|
|
3133
|
+
disabled: !isConnected || isCapturing || capturedCount === 0,
|
|
3134
|
+
isLoading: isProcessing,
|
|
3135
|
+
children: "Guardar"
|
|
3136
|
+
}
|
|
3137
|
+
),
|
|
3138
|
+
finalTemplate && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
3139
|
+
Button,
|
|
3140
|
+
{
|
|
3141
|
+
variant: "secondary",
|
|
3142
|
+
size: "sm",
|
|
3143
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_tb12.TbDownload, { size: 16 }),
|
|
3144
|
+
onClick: downloadTemplate,
|
|
3145
|
+
children: "Exportar"
|
|
3146
|
+
}
|
|
3147
|
+
),
|
|
3148
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
3149
|
+
Button,
|
|
3150
|
+
{
|
|
3151
|
+
variant: "ghost",
|
|
3152
|
+
size: "sm",
|
|
3153
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_tb12.TbReload, { size: 16 }),
|
|
3154
|
+
onClick: resetState,
|
|
3155
|
+
disabled: isCapturing,
|
|
3156
|
+
children: "Limpiar"
|
|
3157
|
+
}
|
|
3158
|
+
)
|
|
3159
|
+
]
|
|
3160
|
+
}
|
|
3161
|
+
),
|
|
3162
|
+
finalTemplate && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "glass p-8 rounded-sm text-center w-full relative overflow-hidden border border-green-500/20", children: [
|
|
3163
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "absolute inset-0 bg-gradient-to-b from-quality-good/10 to-transparent pointer-events-none" }),
|
|
3164
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "text-foreground/60 mb-6 text-base", children: "Paquete ABIS generado exitosamente" }),
|
|
3165
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex flex-col sm:flex-row justify-center gap-3", children: [
|
|
3166
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
3167
|
+
Button,
|
|
3168
|
+
{
|
|
3169
|
+
variant: "success",
|
|
3170
|
+
size: "md",
|
|
3171
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_tb12.TbDownload, { size: 18 }),
|
|
3172
|
+
onClick: downloadTemplate,
|
|
3173
|
+
children: "Descargar Paquete ABIS"
|
|
3174
|
+
}
|
|
3175
|
+
),
|
|
3176
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
3177
|
+
Button,
|
|
3178
|
+
{
|
|
3179
|
+
variant: "ghost",
|
|
3180
|
+
size: "md",
|
|
3181
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_tb12.TbReload, { size: 18 }),
|
|
3182
|
+
onClick: resetState,
|
|
3183
|
+
children: "Nuevo Tr\xE1mite"
|
|
3184
|
+
}
|
|
3185
|
+
)
|
|
3186
|
+
] })
|
|
3187
|
+
] }),
|
|
3188
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "grid grid-cols-1 xl:grid-cols-2 gap-8", children: [
|
|
3189
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "glass p-6 rounded-sm border border-white/10 relative overflow-hidden group", children: [
|
|
3190
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_tb12.TbHandStop, { size: 120 }) }),
|
|
3191
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("h3", { className: "text-lg font-bold text-foreground mb-5 pl-3 border-l-4 border-primary", children: "Mano Derecha" }),
|
|
3192
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "grid grid-cols-1 sm:grid-cols-3 gap-4", children: renderPalmGrid(RIGHT_PALMS) })
|
|
3193
|
+
] }),
|
|
3194
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "glass p-6 rounded-sm border border-white/10 relative overflow-hidden group", children: [
|
|
3195
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_tb12.TbHandStop, { size: 120, className: "-scale-x-100" }) }),
|
|
3196
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("h3", { className: "text-lg font-bold text-foreground mb-5 pl-3 border-l-4 border-secondary-foreground/50", children: "Mano Izquierda" }),
|
|
3197
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "grid grid-cols-1 sm:grid-cols-3 gap-4", children: renderPalmGrid(LEFT_PALMS) })
|
|
3198
|
+
] })
|
|
3199
|
+
] }),
|
|
3200
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
3201
|
+
PalmCaptureModal,
|
|
3202
|
+
{
|
|
3203
|
+
isOpen: isModalOpen,
|
|
3204
|
+
onCancel: cancelCapture,
|
|
3205
|
+
onRetry: retryCapture,
|
|
3206
|
+
previewImage,
|
|
3207
|
+
statusMessage: modalStatus,
|
|
3208
|
+
errorState: modalError,
|
|
3209
|
+
captureType: currentPalm ? PALM_NAMES[currentPalm] : "Captura de Palma"
|
|
3210
|
+
}
|
|
3211
|
+
),
|
|
3212
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
3213
|
+
Modal,
|
|
3214
|
+
{
|
|
3215
|
+
isOpen: showReplaceConfirm,
|
|
3216
|
+
onClose: () => setShowReplaceConfirm(false),
|
|
3217
|
+
title: "\xBFReemplazar Captura?",
|
|
3218
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "text-center p-4", children: [
|
|
3219
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("p", { className: "text-foreground/80 mb-6 text-sm leading-relaxed", children: [
|
|
3220
|
+
"Ya existe una captura guardada para",
|
|
3221
|
+
" ",
|
|
3222
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "font-bold text-foreground", children: pendingPalm && PALM_NAMES[pendingPalm] }),
|
|
3223
|
+
". \xBFDesea capturar nuevamente y sobrescribir la anterior?"
|
|
3224
|
+
] }),
|
|
3225
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex gap-3 justify-center", children: [
|
|
3226
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
3227
|
+
Button,
|
|
3228
|
+
{
|
|
3229
|
+
variant: "secondary",
|
|
3230
|
+
size: "md",
|
|
3231
|
+
onClick: () => setShowReplaceConfirm(false),
|
|
3232
|
+
children: "Cancelar"
|
|
3233
|
+
}
|
|
3234
|
+
),
|
|
3235
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
3236
|
+
Button,
|
|
3237
|
+
{
|
|
3238
|
+
variant: "primary",
|
|
3239
|
+
size: "md",
|
|
3240
|
+
onClick: () => {
|
|
3241
|
+
if (!pendingPalm) return;
|
|
3242
|
+
setShowReplaceConfirm(false);
|
|
3243
|
+
openCapture(pendingPalm);
|
|
3244
|
+
},
|
|
3245
|
+
children: "Confirmar Reemplazo"
|
|
3246
|
+
}
|
|
3247
|
+
)
|
|
3248
|
+
] })
|
|
3249
|
+
] })
|
|
3250
|
+
}
|
|
3251
|
+
),
|
|
3252
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
3253
|
+
MissingBiometricModal,
|
|
3254
|
+
{
|
|
3255
|
+
isOpen: !!palmToMarkMissing,
|
|
3256
|
+
onClose: () => setPalmToMarkMissing(null),
|
|
3257
|
+
onConfirm: handleConfirmMissingPalm,
|
|
3258
|
+
positionName: palmToMarkMissing ? PALM_NAMES[palmToMarkMissing] : "",
|
|
3259
|
+
biometricType: "palm"
|
|
3260
|
+
}
|
|
3261
|
+
)
|
|
3262
|
+
] });
|
|
3263
|
+
}
|
|
3264
|
+
|
|
3265
|
+
// src/components/enrollment/IrisEnrollModule.tsx
|
|
3266
|
+
var import_react10 = require("react");
|
|
3267
|
+
var import_react_hot_toast5 = require("react-hot-toast");
|
|
3268
|
+
|
|
3269
|
+
// src/components/enrollment/IrisCameraModal.tsx
|
|
3270
|
+
var import_react9 = require("react");
|
|
3271
|
+
var import_tb13 = require("react-icons/tb");
|
|
3272
|
+
var import_jsx_runtime17 = require("react/jsx-runtime");
|
|
3273
|
+
function IrisCameraModal({
|
|
3274
|
+
isOpen,
|
|
3275
|
+
onClose,
|
|
3276
|
+
onStartCapture,
|
|
3277
|
+
onAbortCapture,
|
|
3278
|
+
isCapturing,
|
|
3279
|
+
statusMessage,
|
|
3280
|
+
previewRight,
|
|
3281
|
+
previewLeft,
|
|
3282
|
+
distance,
|
|
3283
|
+
cameraStatus
|
|
3284
|
+
}) {
|
|
3285
|
+
const imgRef = (0, import_react9.useRef)(null);
|
|
3286
|
+
const lastBlobUrl = (0, import_react9.useRef)(null);
|
|
3287
|
+
(0, import_react9.useEffect)(() => {
|
|
3288
|
+
const base64 = previewRight || previewLeft;
|
|
3289
|
+
if (imgRef.current && base64) {
|
|
3290
|
+
try {
|
|
3291
|
+
const byteString = atob(base64.split(",")[1]);
|
|
3292
|
+
const arrayBuffer = new ArrayBuffer(byteString.length);
|
|
3293
|
+
const intArray = new Uint8Array(arrayBuffer);
|
|
3294
|
+
for (let i = 0; i < byteString.length; i++) {
|
|
3295
|
+
intArray[i] = byteString.charCodeAt(i);
|
|
3296
|
+
}
|
|
3297
|
+
const blob = new Blob([intArray], { type: "image/bmp" });
|
|
3298
|
+
const url = URL.createObjectURL(blob);
|
|
3299
|
+
imgRef.current.src = url;
|
|
3300
|
+
if (lastBlobUrl.current) URL.revokeObjectURL(lastBlobUrl.current);
|
|
3301
|
+
lastBlobUrl.current = url;
|
|
3302
|
+
} catch (e) {
|
|
3303
|
+
console.warn("\u26A0\uFE0F Error cargando frame BMP:", e);
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
}, [previewRight, previewLeft]);
|
|
3307
|
+
(0, import_react9.useEffect)(() => {
|
|
3308
|
+
if (!isOpen && lastBlobUrl.current) {
|
|
3309
|
+
URL.revokeObjectURL(lastBlobUrl.current);
|
|
3310
|
+
lastBlobUrl.current = null;
|
|
3311
|
+
}
|
|
3312
|
+
}, [isOpen]);
|
|
3313
|
+
let statusColor = "text-foreground/70";
|
|
3314
|
+
let statusBg = "bg-secondary/50 border-border";
|
|
3315
|
+
if (statusMessage.toLowerCase().includes("error")) {
|
|
3316
|
+
statusColor = "text-red-500";
|
|
3317
|
+
statusBg = "bg-red-500/10 border-red-500/20";
|
|
3318
|
+
} else if (statusMessage.toLowerCase().includes("lista") || statusMessage.toLowerCase().includes("completado")) {
|
|
3319
|
+
statusColor = "text-green-500";
|
|
3320
|
+
statusBg = "bg-green-500/10 border-green-500/20";
|
|
3321
|
+
}
|
|
3322
|
+
const isDistanceOptimal = distance !== null && distance >= 100 && distance <= 120;
|
|
3323
|
+
const distanceColor = isDistanceOptimal ? "text-green-500" : "text-yellow-500";
|
|
3324
|
+
const hasPreview = !!(previewRight || previewLeft);
|
|
3325
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
3326
|
+
Modal,
|
|
3327
|
+
{
|
|
3328
|
+
isOpen,
|
|
3329
|
+
onClose,
|
|
3330
|
+
title: "Captura de Iris",
|
|
3331
|
+
allowClose: !isCapturing,
|
|
3332
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex flex-col md:flex-row gap-6 items-stretch", children: [
|
|
3333
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "flex-1 flex justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "relative w-full max-w-[340px] aspect-[4/3] rounded-md p-1 bg-muted/20 border border-border shadow-sm", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "w-full h-full rounded-md overflow-hidden bg-zinc-950 relative flex items-center justify-center border border-white/5 shadow-inner group", children: [
|
|
3334
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
3335
|
+
"div",
|
|
3336
|
+
{
|
|
3337
|
+
className: "absolute inset-0 opacity-20 pointer-events-none",
|
|
3338
|
+
style: {
|
|
3339
|
+
backgroundImage: "linear-gradient(#333 1px, transparent 1px), linear-gradient(90deg, #333 1px, transparent 1px)",
|
|
3340
|
+
backgroundSize: "20px 20px"
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
),
|
|
3344
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
3345
|
+
"img",
|
|
3346
|
+
{
|
|
3347
|
+
ref: imgRef,
|
|
3348
|
+
alt: "Vista previa",
|
|
3349
|
+
className: `
|
|
3350
|
+
relative z-10 object-contain w-full h-full transition-opacity duration-200
|
|
3351
|
+
${hasPreview ? "opacity-100" : "opacity-0"}
|
|
3352
|
+
`
|
|
3353
|
+
}
|
|
3354
|
+
),
|
|
3355
|
+
!hasPreview && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "absolute inset-0 flex flex-col items-center justify-center text-white/20 gap-4 z-0", children: [
|
|
3356
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "relative", children: [
|
|
3357
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_tb13.TbEye, { size: 80, className: "opacity-50" }),
|
|
3358
|
+
isCapturing && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "absolute inset-0 flex items-center justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Loader, { size: 30 }) })
|
|
3359
|
+
] }),
|
|
3360
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { className: "text-[10px] font-bold tracking-[0.2em] uppercase opacity-60", children: isCapturing ? "Iniciando Sensor..." : "C\xE1mara en espera" })
|
|
3361
|
+
] })
|
|
3362
|
+
] }) }) }),
|
|
3363
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex-1 flex flex-col justify-between gap-6 min-h-[320px] py-1", children: [
|
|
3364
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "space-y-5", children: [
|
|
3365
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "grid grid-cols-2 gap-3", children: [
|
|
3366
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "bg-card border border-border rounded-md p-3 flex flex-col gap-1", children: [
|
|
3367
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "text-[10px] uppercase font-bold text-muted-foreground flex items-center gap-1", children: [
|
|
3368
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_tb13.TbRuler, {}),
|
|
3369
|
+
" Distancia"
|
|
3370
|
+
] }),
|
|
3371
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
|
|
3372
|
+
"span",
|
|
3373
|
+
{
|
|
3374
|
+
className: `text-lg font-mono font-bold ${distanceColor}`,
|
|
3375
|
+
children: [
|
|
3376
|
+
distance ?? "--",
|
|
3377
|
+
" ",
|
|
3378
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-xs text-muted-foreground", children: "mm" })
|
|
3379
|
+
]
|
|
3380
|
+
}
|
|
3381
|
+
)
|
|
3382
|
+
] }),
|
|
3383
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "bg-card border border-border rounded-md p-3 flex flex-col gap-1", children: [
|
|
3384
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "text-[10px] uppercase font-bold text-muted-foreground flex items-center gap-1", children: [
|
|
3385
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_tb13.TbActivity, {}),
|
|
3386
|
+
" Estado"
|
|
3387
|
+
] }),
|
|
3388
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
3389
|
+
"span",
|
|
3390
|
+
{
|
|
3391
|
+
className: "text-sm font-medium truncate",
|
|
3392
|
+
title: cameraStatus,
|
|
3393
|
+
children: cameraStatus
|
|
3394
|
+
}
|
|
3395
|
+
)
|
|
3396
|
+
] })
|
|
3397
|
+
] }),
|
|
3398
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
3399
|
+
"div",
|
|
3400
|
+
{
|
|
3401
|
+
className: `p-4 rounded-md border ${statusBg} transition-colors duration-300`,
|
|
3402
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex items-start gap-3", children: [
|
|
3403
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
3404
|
+
import_tb13.TbScan,
|
|
3405
|
+
{
|
|
3406
|
+
className: `mt-0.5 shrink-0 ${statusColor}`,
|
|
3407
|
+
size: 18
|
|
3408
|
+
}
|
|
3409
|
+
),
|
|
3410
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
3411
|
+
"p",
|
|
3412
|
+
{
|
|
3413
|
+
className: `text-sm font-medium leading-snug ${statusColor}`,
|
|
3414
|
+
children: statusMessage
|
|
3415
|
+
}
|
|
3416
|
+
)
|
|
3417
|
+
] })
|
|
3418
|
+
}
|
|
3419
|
+
)
|
|
3420
|
+
] }),
|
|
3421
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "space-y-3 pt-4 border-t border-border", children: !isCapturing ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex gap-3", children: [
|
|
3422
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
3423
|
+
"button",
|
|
3424
|
+
{
|
|
3425
|
+
onClick: onClose,
|
|
3426
|
+
className: "flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-md font-semibold text-sm border border-border hover:bg-secondary transition-colors",
|
|
3427
|
+
children: "Cerrar"
|
|
3428
|
+
}
|
|
3429
|
+
),
|
|
3430
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
|
|
3431
|
+
"button",
|
|
3432
|
+
{
|
|
3433
|
+
onClick: onStartCapture,
|
|
3434
|
+
className: "flex-[2] flex items-center justify-center gap-2 px-4 py-3 rounded-md font-semibold text-sm bg-primary text-white hover:bg-primary/90 shadow-lg shadow-primary/20 transition-all",
|
|
3435
|
+
children: [
|
|
3436
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_tb13.TbPlayerPlay, { size: 18 }),
|
|
3437
|
+
" Iniciar Captura"
|
|
3438
|
+
]
|
|
3439
|
+
}
|
|
3440
|
+
)
|
|
3441
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
|
|
3442
|
+
"button",
|
|
3443
|
+
{
|
|
3444
|
+
onClick: onAbortCapture,
|
|
3445
|
+
className: "w-full flex items-center justify-center gap-2 px-4 py-3 rounded-md font-semibold text-sm bg-red-500/10 text-red-600 border border-red-500/20 hover:bg-red-500 hover:text-white transition-all",
|
|
3446
|
+
children: [
|
|
3447
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_tb13.TbPlayerStop, { size: 18 }),
|
|
3448
|
+
" Detener / Cancelar"
|
|
3449
|
+
]
|
|
3450
|
+
}
|
|
3451
|
+
) })
|
|
3452
|
+
] })
|
|
3453
|
+
] })
|
|
3454
|
+
}
|
|
3455
|
+
);
|
|
3456
|
+
}
|
|
3457
|
+
|
|
3458
|
+
// src/components/enrollment/IrisEnrollModule.tsx
|
|
3459
|
+
var import_tb14 = require("react-icons/tb");
|
|
3460
|
+
var import_jsx_runtime18 = require("react/jsx-runtime");
|
|
3461
|
+
var IRIS_NAMES = {
|
|
3462
|
+
right: "Ojo Derecho",
|
|
3463
|
+
left: "Ojo Izquierdo"
|
|
3464
|
+
};
|
|
3465
|
+
function IrisEnrollModule({ className = "" }) {
|
|
3466
|
+
const {
|
|
3467
|
+
setIrises,
|
|
3468
|
+
removeIris,
|
|
3469
|
+
addMissingIris,
|
|
3470
|
+
removeMissingIris,
|
|
3471
|
+
missingIrises,
|
|
3472
|
+
irises: storeIrises
|
|
3473
|
+
} = useEnrollmentStore();
|
|
3474
|
+
const [finalTemplate, setFinalTemplate] = (0, import_react10.useState)(null);
|
|
3475
|
+
const [finalQuality, setFinalQuality] = (0, import_react10.useState)(null);
|
|
3476
|
+
const [jp2IrisImages, setJp2IrisImages] = (0, import_react10.useState)([]);
|
|
3477
|
+
const [rightIrisImage, setRightIrisImage] = (0, import_react10.useState)(null);
|
|
3478
|
+
const [leftIrisImage, setLeftIrisImage] = (0, import_react10.useState)(null);
|
|
3479
|
+
const [irisQualities, setIrisQualities] = (0, import_react10.useState)({
|
|
3480
|
+
right: null,
|
|
3481
|
+
left: null
|
|
3482
|
+
});
|
|
3483
|
+
const [irisMode, setIrisMode] = (0, import_react10.useState)("both");
|
|
3484
|
+
const [isModalOpen, setIsModalOpen] = (0, import_react10.useState)(false);
|
|
3485
|
+
const isModalOpenRef = (0, import_react10.useRef)(isModalOpen);
|
|
3486
|
+
const [isCapturing, setIsCapturing] = (0, import_react10.useState)(false);
|
|
3487
|
+
const [modalStatus, setModalStatus] = (0, import_react10.useState)("Esperando...");
|
|
3488
|
+
const [previewRight, setPreviewRight] = (0, import_react10.useState)(null);
|
|
3489
|
+
const [previewLeft, setPreviewLeft] = (0, import_react10.useState)(null);
|
|
3490
|
+
const [distance, setDistance] = (0, import_react10.useState)(null);
|
|
3491
|
+
const [cameraStatus, setCameraStatus] = (0, import_react10.useState)("Desconocido");
|
|
3492
|
+
const [showReplaceConfirm, setShowReplaceConfirm] = (0, import_react10.useState)(false);
|
|
3493
|
+
const [irisToMarkMissing, setIrisToMarkMissing] = (0, import_react10.useState)(
|
|
3494
|
+
null
|
|
3495
|
+
);
|
|
3496
|
+
const { isConnected, sendMessage, registerHandler, unregisterHandler } = useBiometricStore();
|
|
3497
|
+
const addMessage = useUiStore((s) => s.addMessage);
|
|
3498
|
+
(0, import_react10.useEffect)(() => {
|
|
3499
|
+
if (storeIrises.right && !rightIrisImage) {
|
|
3500
|
+
const srcRight = getImageSrcForDisplay(storeIrises.right.image) || null;
|
|
3501
|
+
setRightIrisImage(srcRight);
|
|
3502
|
+
setIrisQualities((prev) => ({
|
|
3503
|
+
...prev,
|
|
3504
|
+
right: storeIrises.right.quality
|
|
3505
|
+
}));
|
|
3506
|
+
}
|
|
3507
|
+
if (storeIrises.left && !leftIrisImage) {
|
|
3508
|
+
const srcLeft = getImageSrcForDisplay(storeIrises.left.image) || null;
|
|
3509
|
+
setLeftIrisImage(srcLeft);
|
|
3510
|
+
setIrisQualities((prev) => ({
|
|
3511
|
+
...prev,
|
|
3512
|
+
left: storeIrises.left.quality
|
|
3513
|
+
}));
|
|
3514
|
+
}
|
|
3515
|
+
}, [storeIrises]);
|
|
3516
|
+
const capturedCount = (0, import_react10.useMemo)(() => {
|
|
3517
|
+
let count = 0;
|
|
3518
|
+
if (rightIrisImage) count++;
|
|
3519
|
+
if (leftIrisImage) count++;
|
|
3520
|
+
count += Object.keys(missingIrises).length;
|
|
3521
|
+
return count;
|
|
3522
|
+
}, [rightIrisImage, leftIrisImage, missingIrises]);
|
|
3523
|
+
const modeOptions = [
|
|
3524
|
+
{ value: "both", label: "Ambos Ojos (Dual)", icon: "\u{1F440}" },
|
|
3525
|
+
{ value: "right", label: "Solo Derecho", icon: "\u{1F441}" },
|
|
3526
|
+
{ value: "left", label: "Solo Izquierdo", icon: "\u{1F441}" }
|
|
3527
|
+
];
|
|
3528
|
+
const parseCameraStatus = (code) => {
|
|
3529
|
+
switch (Number(code)) {
|
|
3530
|
+
case 2:
|
|
3531
|
+
return "C\xE1mara lista";
|
|
3532
|
+
case 4:
|
|
3533
|
+
return "Capturando";
|
|
3534
|
+
case 5:
|
|
3535
|
+
return "Completado";
|
|
3536
|
+
default:
|
|
3537
|
+
return "Estado desconocido";
|
|
3538
|
+
}
|
|
3539
|
+
};
|
|
3540
|
+
(0, import_react10.useEffect)(() => {
|
|
3541
|
+
isModalOpenRef.current = isModalOpen;
|
|
3542
|
+
}, [isModalOpen]);
|
|
3543
|
+
(0, import_react10.useEffect)(() => {
|
|
3544
|
+
const handlePreviewFrame = (msg) => {
|
|
3545
|
+
if (!isModalOpenRef.current) return;
|
|
3546
|
+
const { irisRightBase64, irisLeftBase64 } = msg.biometricData || {};
|
|
3547
|
+
if (irisRightBase64)
|
|
3548
|
+
setPreviewRight(getImageSrcForDisplay(irisRightBase64) || null);
|
|
3549
|
+
if (irisLeftBase64)
|
|
3550
|
+
setPreviewLeft(getImageSrcForDisplay(irisLeftBase64) || null);
|
|
3551
|
+
};
|
|
3552
|
+
const handleComplete = (msg) => {
|
|
3553
|
+
const d = msg.biometricData || {};
|
|
3554
|
+
if (d.irisRightBase64)
|
|
3555
|
+
setRightIrisImage(getImageSrcForDisplay(d.irisRightBase64) || null);
|
|
3556
|
+
if (d.irisLeftBase64)
|
|
3557
|
+
setLeftIrisImage(getImageSrcForDisplay(d.irisLeftBase64) || null);
|
|
3558
|
+
setFinalTemplate(d.finalTemplate || null);
|
|
3559
|
+
setFinalQuality(d.quality || null);
|
|
3560
|
+
const jp2List = {
|
|
3561
|
+
right: d.irisRightBase64 || null,
|
|
3562
|
+
left: d.irisLeftBase64 || null
|
|
3563
|
+
};
|
|
3564
|
+
setJp2IrisImages(jp2List);
|
|
3565
|
+
const rightData = d.irisRightBase64 ? {
|
|
3566
|
+
image: normalizeImageForStorage(d.irisRightBase64),
|
|
3567
|
+
quality: d.irisRightQuality || 100
|
|
3568
|
+
} : void 0;
|
|
3569
|
+
const leftData = d.irisLeftBase64 ? {
|
|
3570
|
+
image: normalizeImageForStorage(d.irisLeftBase64),
|
|
3571
|
+
quality: d.irisLeftQuality || 100
|
|
3572
|
+
} : void 0;
|
|
3573
|
+
setIrises(rightData, leftData);
|
|
3574
|
+
setIrisQualities({
|
|
3575
|
+
right: d.irisRightQuality || null,
|
|
3576
|
+
left: d.irisLeftQuality || null
|
|
3577
|
+
});
|
|
3578
|
+
setIsCapturing(false);
|
|
3579
|
+
setIsModalOpen(false);
|
|
3580
|
+
setPreviewRight(null);
|
|
3581
|
+
setPreviewLeft(null);
|
|
3582
|
+
import_react_hot_toast5.toast.success("Plantilla de iris generada");
|
|
3583
|
+
};
|
|
3584
|
+
const handleStatusUpdate = (msg) => {
|
|
3585
|
+
if (!isModalOpenRef.current) return;
|
|
3586
|
+
const d = msg.biometricData || {};
|
|
3587
|
+
if (d.distance !== void 0) setDistance(d.distance);
|
|
3588
|
+
if (d.status !== void 0) {
|
|
3589
|
+
const st = parseCameraStatus(d.status);
|
|
3590
|
+
setCameraStatus(st);
|
|
3591
|
+
setModalStatus("Captura en proceso...");
|
|
3592
|
+
}
|
|
3593
|
+
};
|
|
3594
|
+
const handleError = (msg) => {
|
|
3595
|
+
const err = msg.error || "Error desconocido";
|
|
3596
|
+
import_react_hot_toast5.toast.error(err);
|
|
3597
|
+
setIsCapturing(false);
|
|
3598
|
+
setModalStatus("Error: " + err);
|
|
3599
|
+
};
|
|
3600
|
+
registerHandler("IRIS_PREVIEW_FRAME", handlePreviewFrame);
|
|
3601
|
+
registerHandler("IRIS_ENROLLMENT_COMPLETE", handleComplete);
|
|
3602
|
+
registerHandler("IRIS_STATUS_UPDATE", handleStatusUpdate);
|
|
3603
|
+
registerHandler("IRIS_ERROR", handleError);
|
|
3604
|
+
return () => {
|
|
3605
|
+
unregisterHandler("IRIS_PREVIEW_FRAME");
|
|
3606
|
+
unregisterHandler("IRIS_ENROLLMENT_COMPLETE");
|
|
3607
|
+
unregisterHandler("IRIS_STATUS_UPDATE");
|
|
3608
|
+
unregisterHandler("IRIS_ERROR");
|
|
3609
|
+
};
|
|
3610
|
+
}, [registerHandler, unregisterHandler, setIrises]);
|
|
3611
|
+
const startCapture = () => {
|
|
3612
|
+
if (!isConnected) return import_react_hot_toast5.toast.error("Servicio no conectado.");
|
|
3613
|
+
setFinalTemplate(null);
|
|
3614
|
+
setFinalQuality(null);
|
|
3615
|
+
setRightIrisImage(null);
|
|
3616
|
+
setLeftIrisImage(null);
|
|
3617
|
+
setIrisQualities({ right: null, left: null });
|
|
3618
|
+
setPreviewRight(null);
|
|
3619
|
+
setPreviewLeft(null);
|
|
3620
|
+
setIsModalOpen(true);
|
|
3621
|
+
setModalStatus("Listo para iniciar");
|
|
3622
|
+
setDistance(0);
|
|
3623
|
+
sendMessage({
|
|
3624
|
+
messageType: "START_IRIS_PREVIEW",
|
|
3625
|
+
biometricData: { mode: irisMode }
|
|
3626
|
+
});
|
|
3627
|
+
};
|
|
3628
|
+
const openModal = () => {
|
|
3629
|
+
const hasExistingData = irisMode === "both" && (rightIrisImage || leftIrisImage) || irisMode === "right" && rightIrisImage || irisMode === "left" && leftIrisImage;
|
|
3630
|
+
if (hasExistingData) {
|
|
3631
|
+
setShowReplaceConfirm(true);
|
|
3632
|
+
} else {
|
|
3633
|
+
startCapture();
|
|
3634
|
+
}
|
|
3635
|
+
};
|
|
3636
|
+
const handleStartCapture = () => {
|
|
3637
|
+
setIsCapturing(true);
|
|
3638
|
+
setModalStatus("Capturando... Mantenga la mirada fija.");
|
|
3639
|
+
sendMessage({
|
|
3640
|
+
messageType: "START_IRIS_CAPTURE",
|
|
3641
|
+
biometricData: { mode: irisMode }
|
|
3642
|
+
});
|
|
3643
|
+
};
|
|
3644
|
+
const handleAbortCapture = () => {
|
|
3645
|
+
setIsCapturing(false);
|
|
3646
|
+
setModalStatus("Cancelado por el usuario.");
|
|
3647
|
+
sendMessage({ messageType: "ABORT_IRIS_CAPTURE" });
|
|
3648
|
+
};
|
|
3649
|
+
const closeModal = () => {
|
|
3650
|
+
if (isCapturing) handleAbortCapture();
|
|
3651
|
+
setIsModalOpen(false);
|
|
3652
|
+
};
|
|
3653
|
+
const downloadTemplate = () => {
|
|
3654
|
+
if (!finalTemplate) return;
|
|
3655
|
+
const payload = {
|
|
3656
|
+
template: finalTemplate,
|
|
3657
|
+
irisIsoImages: jp2IrisImages
|
|
3658
|
+
};
|
|
3659
|
+
const blob = new Blob([JSON.stringify(payload, null, 2)], {
|
|
3660
|
+
type: "application/json"
|
|
3661
|
+
});
|
|
3662
|
+
const url = URL.createObjectURL(blob);
|
|
3663
|
+
const a = document.createElement("a");
|
|
3664
|
+
a.href = url;
|
|
3665
|
+
a.download = "paquete_iris_abis.json";
|
|
3666
|
+
a.click();
|
|
3667
|
+
URL.revokeObjectURL(url);
|
|
3668
|
+
addMessage("Paquete de iris descargado correctamente.");
|
|
3669
|
+
};
|
|
3670
|
+
const resetState = () => {
|
|
3671
|
+
setFinalTemplate(null);
|
|
3672
|
+
setFinalQuality(null);
|
|
3673
|
+
setRightIrisImage(null);
|
|
3674
|
+
setLeftIrisImage(null);
|
|
3675
|
+
setIrisQualities({ right: null, left: null });
|
|
3676
|
+
};
|
|
3677
|
+
const handleDeleteIris = (side) => {
|
|
3678
|
+
if (side === "right") {
|
|
3679
|
+
setRightIrisImage(null);
|
|
3680
|
+
setIrisQualities((prev) => ({ ...prev, right: null }));
|
|
3681
|
+
} else {
|
|
3682
|
+
setLeftIrisImage(null);
|
|
3683
|
+
setIrisQualities((prev) => ({ ...prev, left: null }));
|
|
3684
|
+
}
|
|
3685
|
+
removeIris(side);
|
|
3686
|
+
import_react_hot_toast5.toast.success(`Iris ${IRIS_NAMES[side]} eliminado`);
|
|
3687
|
+
};
|
|
3688
|
+
const handleConfirmMissingIris = (reason) => {
|
|
3689
|
+
if (irisToMarkMissing) {
|
|
3690
|
+
const side = irisToMarkMissing;
|
|
3691
|
+
handleDeleteIris(side);
|
|
3692
|
+
addMissingIris(irisToMarkMissing, reason);
|
|
3693
|
+
setIrisToMarkMissing(null);
|
|
3694
|
+
}
|
|
3695
|
+
};
|
|
3696
|
+
const isBusy = isCapturing || isModalOpen;
|
|
3697
|
+
return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "space-y-8 animate-in fade-in duration-500", children: [
|
|
3698
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
|
|
3699
|
+
ModuleHeader,
|
|
3700
|
+
{
|
|
3701
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_tb14.TbEye, { size: 24 }),
|
|
3702
|
+
title: "Enrolamiento de Iris",
|
|
3703
|
+
subtitle: "Utilice el esc\xE1ner binocular para capturar el patr\xF3n del iris.",
|
|
3704
|
+
progress: { current: capturedCount, total: 2 },
|
|
3705
|
+
children: [
|
|
3706
|
+
finalTemplate && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
3707
|
+
Button,
|
|
3708
|
+
{
|
|
3709
|
+
variant: "secondary",
|
|
3710
|
+
size: "sm",
|
|
3711
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_tb14.TbDownload, { size: 16 }),
|
|
3712
|
+
onClick: downloadTemplate,
|
|
3713
|
+
children: "Exportar"
|
|
3714
|
+
}
|
|
3715
|
+
),
|
|
3716
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
3717
|
+
Button,
|
|
3718
|
+
{
|
|
3719
|
+
variant: "ghost",
|
|
3720
|
+
size: "sm",
|
|
3721
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_tb14.TbReload, { size: 16 }),
|
|
3722
|
+
onClick: resetState,
|
|
3723
|
+
disabled: isBusy,
|
|
3724
|
+
children: "Limpiar"
|
|
3725
|
+
}
|
|
3726
|
+
)
|
|
3727
|
+
]
|
|
3728
|
+
}
|
|
3729
|
+
),
|
|
3730
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex flex-col sm:flex-row gap-4 items-end bg-secondary/30 p-4 rounded-md border border-white/5", children: [
|
|
3731
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex-1 w-full sm:w-auto", children: [
|
|
3732
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("label", { className: "text-xs font-bold text-foreground/50 uppercase tracking-wider mb-2 block", children: "Modo de Captura" }),
|
|
3733
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "w-full sm:max-w-xs", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
3734
|
+
Select,
|
|
3735
|
+
{
|
|
3736
|
+
options: modeOptions,
|
|
3737
|
+
value: irisMode,
|
|
3738
|
+
onChange: setIrisMode,
|
|
3739
|
+
disabled: isBusy || !isConnected
|
|
3740
|
+
}
|
|
3741
|
+
) })
|
|
3742
|
+
] }),
|
|
3743
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
3744
|
+
Button,
|
|
3745
|
+
{
|
|
3746
|
+
variant: "success",
|
|
3747
|
+
size: "md",
|
|
3748
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_tb14.TbScanEye, { size: 20 }),
|
|
3749
|
+
onClick: openModal,
|
|
3750
|
+
disabled: !isConnected || isBusy,
|
|
3751
|
+
children: "Iniciar Esc\xE1ner"
|
|
3752
|
+
}
|
|
3753
|
+
)
|
|
3754
|
+
] }),
|
|
3755
|
+
finalTemplate && /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "glass p-8 rounded-sm text-center w-full relative overflow-hidden border border-green-500/20", children: [
|
|
3756
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "absolute inset-0 bg-gradient-to-b from-quality-good/10 to-transparent pointer-events-none" }),
|
|
3757
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "text-foreground/60 mb-6 text-base", children: "Paquete ABIS generado correctamente." }),
|
|
3758
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex flex-col sm:flex-row justify-center gap-3", children: [
|
|
3759
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
3760
|
+
Button,
|
|
3761
|
+
{
|
|
3762
|
+
variant: "success",
|
|
3763
|
+
size: "md",
|
|
3764
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_tb14.TbDownload, { size: 18 }),
|
|
3765
|
+
onClick: downloadTemplate,
|
|
3766
|
+
children: "Descargar Paquete ABIS"
|
|
3767
|
+
}
|
|
3768
|
+
),
|
|
3769
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
3770
|
+
Button,
|
|
3771
|
+
{
|
|
3772
|
+
variant: "ghost",
|
|
3773
|
+
size: "md",
|
|
3774
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_tb14.TbReload, { size: 18 }),
|
|
3775
|
+
onClick: resetState,
|
|
3776
|
+
children: "Nuevo Tr\xE1mite"
|
|
3777
|
+
}
|
|
3778
|
+
)
|
|
3779
|
+
] })
|
|
3780
|
+
] }),
|
|
3781
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-8", children: ["right", "left"].map((side) => {
|
|
3782
|
+
const image = side === "right" ? rightIrisImage : leftIrisImage;
|
|
3783
|
+
const quality = irisQualities[side];
|
|
3784
|
+
const missing = missingIrises[side] || null;
|
|
3785
|
+
return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
3786
|
+
BiometricSlot,
|
|
3787
|
+
{
|
|
3788
|
+
position: side,
|
|
3789
|
+
title: IRIS_NAMES[side],
|
|
3790
|
+
status: image ? "captured" : "pending",
|
|
3791
|
+
imageUrl: image,
|
|
3792
|
+
quality,
|
|
3793
|
+
iconType: "iris",
|
|
3794
|
+
missingReason: missing,
|
|
3795
|
+
onCapture: openModal,
|
|
3796
|
+
onDelete: () => handleDeleteIris(side),
|
|
3797
|
+
onRequestMissing: () => setIrisToMarkMissing(side),
|
|
3798
|
+
onRestore: () => removeMissingIris(side),
|
|
3799
|
+
isDisabled: !isConnected || isBusy,
|
|
3800
|
+
className: "min-h-[260px]"
|
|
3801
|
+
},
|
|
3802
|
+
side
|
|
3803
|
+
);
|
|
3804
|
+
}) }),
|
|
3805
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
3806
|
+
IrisCameraModal,
|
|
3807
|
+
{
|
|
3808
|
+
isOpen: isModalOpen,
|
|
3809
|
+
onClose: closeModal,
|
|
3810
|
+
onStartCapture: handleStartCapture,
|
|
3811
|
+
onAbortCapture: handleAbortCapture,
|
|
3812
|
+
isCapturing,
|
|
3813
|
+
statusMessage: modalStatus,
|
|
3814
|
+
previewRight,
|
|
3815
|
+
previewLeft,
|
|
3816
|
+
distance,
|
|
3817
|
+
cameraStatus
|
|
3818
|
+
}
|
|
3819
|
+
),
|
|
3820
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
3821
|
+
MissingBiometricModal,
|
|
3822
|
+
{
|
|
3823
|
+
isOpen: !!irisToMarkMissing,
|
|
3824
|
+
onClose: () => setIrisToMarkMissing(null),
|
|
3825
|
+
onConfirm: handleConfirmMissingIris,
|
|
3826
|
+
positionName: irisToMarkMissing ? IRIS_NAMES[irisToMarkMissing] : "",
|
|
3827
|
+
biometricType: "iris"
|
|
3828
|
+
}
|
|
3829
|
+
),
|
|
3830
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
3831
|
+
Modal,
|
|
3832
|
+
{
|
|
3833
|
+
isOpen: showReplaceConfirm,
|
|
3834
|
+
onClose: () => setShowReplaceConfirm(false),
|
|
3835
|
+
title: "\xBFReemplazar Captura?",
|
|
3836
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "text-center p-4", children: [
|
|
3837
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "text-foreground/80 mb-6 text-sm leading-relaxed", children: "Ya existe una captura de iris para este modo. \xBFDesea capturar nuevamente y sobrescribir la anterior?" }),
|
|
3838
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex gap-3 justify-center", children: [
|
|
3839
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
3840
|
+
Button,
|
|
3841
|
+
{
|
|
3842
|
+
variant: "secondary",
|
|
3843
|
+
size: "md",
|
|
3844
|
+
onClick: () => setShowReplaceConfirm(false),
|
|
3845
|
+
children: "Cancelar"
|
|
3846
|
+
}
|
|
3847
|
+
),
|
|
3848
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
3849
|
+
Button,
|
|
3850
|
+
{
|
|
3851
|
+
variant: "primary",
|
|
3852
|
+
size: "md",
|
|
3853
|
+
onClick: () => {
|
|
3854
|
+
setShowReplaceConfirm(false);
|
|
3855
|
+
startCapture();
|
|
3856
|
+
},
|
|
3857
|
+
children: "Confirmar Reemplazo"
|
|
3858
|
+
}
|
|
3859
|
+
)
|
|
3860
|
+
] })
|
|
3861
|
+
] })
|
|
3862
|
+
}
|
|
3863
|
+
)
|
|
3864
|
+
] });
|
|
3865
|
+
}
|
|
3866
|
+
|
|
3867
|
+
// src/components/enrollment/FingerRollEnrollModule.tsx
|
|
3868
|
+
var import_react11 = require("react");
|
|
3869
|
+
var import_react_hot_toast6 = require("react-hot-toast");
|
|
3870
|
+
|
|
3871
|
+
// src/components/enrollment/FingerRollCaptureModal.tsx
|
|
3872
|
+
var import_tb15 = require("react-icons/tb");
|
|
3873
|
+
var import_framer_motion6 = require("framer-motion");
|
|
3874
|
+
var import_jsx_runtime19 = require("react/jsx-runtime");
|
|
3875
|
+
function FingerRollCaptureModal({
|
|
3876
|
+
isOpen,
|
|
3877
|
+
onCancel,
|
|
3878
|
+
captureType,
|
|
3879
|
+
previewImage,
|
|
3880
|
+
previewQuality,
|
|
3881
|
+
rollStatus
|
|
3882
|
+
}) {
|
|
3883
|
+
let statusStyles = "bg-secondary/50 border-border/50 text-foreground/80";
|
|
3884
|
+
let statusIcon = /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(Loader, { size: 18, className: "text-foreground/50 animate-spin" });
|
|
3885
|
+
const statusLower = rollStatus.toLowerCase();
|
|
3886
|
+
if (statusLower.includes("rolar") || statusLower.includes("detectado") || statusLower.includes("coloque")) {
|
|
3887
|
+
statusStyles = "bg-primary/10 border-primary/20 text-primary";
|
|
3888
|
+
statusIcon = /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_tb15.TbRotateClockwise, { size: 20, className: "animate-pulse" });
|
|
3889
|
+
} else if (statusLower.includes("completo") || statusLower.includes("\xE9xito") || statusLower.includes("ok")) {
|
|
3890
|
+
statusStyles = "bg-quality-good/10 border-quality-good/20 text-quality-good";
|
|
3891
|
+
statusIcon = /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_tb15.TbCheck, { size: 20 });
|
|
3892
|
+
} else if (statusLower.includes("error") || statusLower.includes("falla") || statusLower.includes("mala")) {
|
|
3893
|
+
statusStyles = "bg-quality-bad/10 border-quality-bad/20 text-quality-bad";
|
|
3894
|
+
statusIcon = /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_tb15.TbX, { size: 20 });
|
|
3895
|
+
}
|
|
3896
|
+
return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
3897
|
+
Modal,
|
|
3898
|
+
{
|
|
3899
|
+
isOpen,
|
|
3900
|
+
onClose: onCancel,
|
|
3901
|
+
title: captureType,
|
|
3902
|
+
allowClose: true,
|
|
3903
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex flex-col md:flex-row gap-8 items-stretch p-2", children: [
|
|
3904
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "flex-1 flex justify-center bg-card/50 rounded-md p-4 border border-white/5", children: /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
|
|
3905
|
+
"div",
|
|
3906
|
+
{
|
|
3907
|
+
className: "\r\n relative w-full max-w-[280px] aspect-[3/4]\r\n rounded-md overflow-hidden\r\n bg-black/5 dark:bg-white/5\r\n border border-border/50 dark:border-white/10\r\n shadow-inner flex items-center justify-center\r\n ",
|
|
3908
|
+
children: [
|
|
3909
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
3910
|
+
import_framer_motion6.motion.div,
|
|
3911
|
+
{
|
|
3912
|
+
className: "absolute inset-y-0 w-[2px] bg-primary/80 z-10 shadow-[0_0_20px_2px_rgba(var(--primary),0.6)]",
|
|
3913
|
+
animate: { left: ["5%", "95%", "5%"] },
|
|
3914
|
+
transition: { duration: 4, repeat: Infinity, ease: "easeInOut" }
|
|
3915
|
+
}
|
|
3916
|
+
),
|
|
3917
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
3918
|
+
"div",
|
|
3919
|
+
{
|
|
3920
|
+
className: "absolute inset-0 pointer-events-none opacity-[0.08] z-0",
|
|
3921
|
+
style: {
|
|
3922
|
+
backgroundImage: "linear-gradient(0deg, transparent 24%, currentColor 25%, currentColor 26%, transparent 27%, transparent 74%, currentColor 75%, currentColor 76%, transparent 77%, transparent), linear-gradient(90deg, transparent 24%, currentColor 25%, currentColor 26%, transparent 27%, transparent 74%, currentColor 75%, currentColor 76%, transparent 77%, transparent)",
|
|
3923
|
+
backgroundSize: "20px 20px",
|
|
3924
|
+
color: "currentColor"
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3927
|
+
),
|
|
3928
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "relative z-1 w-full h-full flex items-center justify-center", children: previewImage ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
3929
|
+
import_framer_motion6.motion.img,
|
|
3930
|
+
{
|
|
3931
|
+
initial: { opacity: 0, scale: 0.95 },
|
|
3932
|
+
animate: { opacity: 1, scale: 1 },
|
|
3933
|
+
src: previewImage,
|
|
3934
|
+
alt: "Huella rolada",
|
|
3935
|
+
className: "object-contain w-full h-full p-4 rotate-180 drop-shadow-lg"
|
|
3936
|
+
}
|
|
3937
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex flex-col items-center justify-center text-foreground/30 gap-3", children: [
|
|
3938
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "relative", children: [
|
|
3939
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_tb15.TbFingerprint, { size: 70, className: "opacity-80" }),
|
|
3940
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
3941
|
+
import_framer_motion6.motion.div,
|
|
3942
|
+
{
|
|
3943
|
+
className: "absolute -right-2 -bottom-2 text-primary",
|
|
3944
|
+
animate: { rotate: 360 },
|
|
3945
|
+
transition: {
|
|
3946
|
+
duration: 8,
|
|
3947
|
+
repeat: Infinity,
|
|
3948
|
+
ease: "linear"
|
|
3949
|
+
},
|
|
3950
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_tb15.TbRotateClockwise, { size: 28 })
|
|
3951
|
+
}
|
|
3952
|
+
)
|
|
3953
|
+
] }),
|
|
3954
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("p", { className: "text-[10px] font-bold tracking-widest uppercase opacity-70", children: "Esperando inicio" })
|
|
3955
|
+
] }) }),
|
|
3956
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "absolute top-3 right-3 z-20", children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(QualityBadge, { quality: previewQuality }) })
|
|
3957
|
+
]
|
|
3958
|
+
}
|
|
3959
|
+
) }),
|
|
3960
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex-1 flex flex-col justify-between gap-6 min-h-[300px]", children: [
|
|
3961
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "space-y-4", children: [
|
|
3962
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { children: [
|
|
3963
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("h4", { className: "text-xs font-bold text-foreground/50 uppercase tracking-wider mb-2", children: "Estado del Proceso" }),
|
|
3964
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
|
|
3965
|
+
"div",
|
|
3966
|
+
{
|
|
3967
|
+
className: `
|
|
3968
|
+
p-4 rounded-md border flex items-start gap-3 shadow-sm
|
|
3969
|
+
transition-all duration-300 ${statusStyles}
|
|
3970
|
+
`,
|
|
3971
|
+
children: [
|
|
3972
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "mt-0.5 shrink-0", children: statusIcon }),
|
|
3973
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("p", { className: "text-sm font-medium leading-snug", children: rollStatus })
|
|
3974
|
+
]
|
|
3975
|
+
}
|
|
3976
|
+
)
|
|
3977
|
+
] }),
|
|
3978
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "space-y-2", children: [
|
|
3979
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("h4", { className: "text-xs font-semibold text-foreground/60 uppercase tracking-wide flex items-center gap-1", children: [
|
|
3980
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_tb15.TbScan, { className: "mb-0.5" }),
|
|
3981
|
+
" Gu\xEDa de Rolado"
|
|
3982
|
+
] }),
|
|
3983
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "bg-card border border-border/60 rounded-md p-3 space-y-2", children: [
|
|
3984
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex gap-3 items-center text-sm text-foreground/70", children: [
|
|
3985
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "w-6 h-6 rounded-full bg-secondary flex items-center justify-center text-xs font-bold shrink-0", children: "1" }),
|
|
3986
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("span", { className: "leading-tight", children: [
|
|
3987
|
+
"Apoye el ",
|
|
3988
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("strong", { children: "borde lateral" }),
|
|
3989
|
+
" de la u\xF1a."
|
|
3990
|
+
] })
|
|
3991
|
+
] }),
|
|
3992
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "h-px w-8 bg-border mx-auto" }),
|
|
3993
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex gap-3 items-center text-sm text-foreground/70", children: [
|
|
3994
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "w-6 h-6 rounded-full bg-secondary flex items-center justify-center text-xs font-bold shrink-0", children: "2" }),
|
|
3995
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "leading-tight", children: "Gire suavemente hacia el lado opuesto." })
|
|
3996
|
+
] })
|
|
3997
|
+
] })
|
|
3998
|
+
] })
|
|
3999
|
+
] }),
|
|
4000
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "pt-4 border-t border-border/40", children: /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
|
|
4001
|
+
"button",
|
|
4002
|
+
{
|
|
4003
|
+
onClick: onCancel,
|
|
4004
|
+
className: "\r\n group w-full flex items-center justify-center gap-2 px-4 py-3.5\r\n rounded-md font-semibold text-sm\r\n bg-red-500/10 text-red-600 dark:text-red-400 \r\n border border-red-500/20\r\n hover:bg-red-500 hover:text-white hover:border-red-500\r\n transition-all duration-200 shadow-sm\r\n ",
|
|
4005
|
+
children: [
|
|
4006
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_tb15.TbX, { size: 18 }),
|
|
4007
|
+
"Abortar Captura"
|
|
4008
|
+
]
|
|
4009
|
+
}
|
|
4010
|
+
) })
|
|
4011
|
+
] })
|
|
4012
|
+
] })
|
|
4013
|
+
}
|
|
4014
|
+
);
|
|
4015
|
+
}
|
|
4016
|
+
|
|
4017
|
+
// src/components/enrollment/FingerRollEnrollModule.tsx
|
|
4018
|
+
var import_tb16 = require("react-icons/tb");
|
|
4019
|
+
var import_jsx_runtime20 = require("react/jsx-runtime");
|
|
4020
|
+
function FingerRollEnrollModule({ className = "" }) {
|
|
4021
|
+
const {
|
|
4022
|
+
addRolledFingerprint,
|
|
4023
|
+
addMissingFinger,
|
|
4024
|
+
removeMissingFinger,
|
|
4025
|
+
removeRolledFingerprint,
|
|
4026
|
+
rolledFingerprints: storeRolled,
|
|
4027
|
+
missingFingers: enrollmentMissingFingers
|
|
4028
|
+
} = useEnrollmentStore();
|
|
4029
|
+
const [fingerResults, setFingerResults] = (0, import_react11.useState)({});
|
|
4030
|
+
const [isModalOpen, setIsModalOpen] = (0, import_react11.useState)(false);
|
|
4031
|
+
const isModalOpenRef = (0, import_react11.useRef)(isModalOpen);
|
|
4032
|
+
const [isProcessing, setIsProcessing] = (0, import_react11.useState)(false);
|
|
4033
|
+
const [currentFinger, setCurrentFinger] = (0, import_react11.useState)(null);
|
|
4034
|
+
const [previewImage, setPreviewImage] = (0, import_react11.useState)(null);
|
|
4035
|
+
const [previewQuality, setPreviewQuality] = (0, import_react11.useState)(null);
|
|
4036
|
+
const [rollStatus, setRollStatus] = (0, import_react11.useState)("Coloque el dedo...");
|
|
4037
|
+
const [fingerToMarkMissing, setFingerToMarkMissing] = (0, import_react11.useState)(
|
|
4038
|
+
null
|
|
4039
|
+
);
|
|
4040
|
+
const {
|
|
4041
|
+
isConnected,
|
|
4042
|
+
sendMessage,
|
|
4043
|
+
registerHandler,
|
|
4044
|
+
unregisterHandler,
|
|
4045
|
+
missingFingers,
|
|
4046
|
+
markFingerAsMissing,
|
|
4047
|
+
restoreFinger,
|
|
4048
|
+
clearAllMissingFingers
|
|
4049
|
+
} = useBiometricStore();
|
|
4050
|
+
const addMessage = useUiStore((s) => s.addMessage);
|
|
4051
|
+
(0, import_react11.useEffect)(() => {
|
|
4052
|
+
const loaded = {};
|
|
4053
|
+
Object.entries(storeRolled).forEach(([key, item]) => {
|
|
4054
|
+
loaded[key] = {
|
|
4055
|
+
Finger: key,
|
|
4056
|
+
Quality: item.quality,
|
|
4057
|
+
Image: item.image,
|
|
4058
|
+
Error: null
|
|
4059
|
+
};
|
|
4060
|
+
});
|
|
4061
|
+
setFingerResults((prev) => ({ ...prev, ...loaded }));
|
|
4062
|
+
}, [storeRolled]);
|
|
4063
|
+
const allMissingFingers = (0, import_react11.useMemo)(
|
|
4064
|
+
() => ({ ...enrollmentMissingFingers, ...missingFingers }),
|
|
4065
|
+
[enrollmentMissingFingers, missingFingers]
|
|
4066
|
+
);
|
|
4067
|
+
const capturedCount = (0, import_react11.useMemo)(() => {
|
|
4068
|
+
const success = Object.values(fingerResults).filter(
|
|
4069
|
+
(r) => r && r.Quality >= 40
|
|
4070
|
+
).length;
|
|
4071
|
+
const missing = Object.keys(allMissingFingers).length;
|
|
4072
|
+
return success + missing;
|
|
4073
|
+
}, [fingerResults, allMissingFingers]);
|
|
4074
|
+
const isCapturing = isProcessing || isModalOpen;
|
|
4075
|
+
(0, import_react11.useEffect)(() => {
|
|
4076
|
+
isModalOpenRef.current = isModalOpen;
|
|
4077
|
+
}, [isModalOpen]);
|
|
4078
|
+
(0, import_react11.useEffect)(() => {
|
|
4079
|
+
const handlePreview = (d) => {
|
|
4080
|
+
if (!isModalOpenRef.current) return;
|
|
4081
|
+
setPreviewImage(d.biometricData?.capturedImage || null);
|
|
4082
|
+
setPreviewQuality(d.biometricData?.quality || null);
|
|
4083
|
+
};
|
|
4084
|
+
const handleRollStart = (d) => {
|
|
4085
|
+
setRollStatus(d.payload || "Dedo detectado. Comience a rolar...");
|
|
4086
|
+
};
|
|
4087
|
+
const handleRollComplete = (d) => {
|
|
4088
|
+
setRollStatus(d.payload || "Rolado completo. Procesando...");
|
|
4089
|
+
};
|
|
4090
|
+
const handleCaptureComplete = (d) => {
|
|
4091
|
+
setIsProcessing(false);
|
|
4092
|
+
setIsModalOpen(false);
|
|
4093
|
+
const seg = d.biometricData?.segments?.[0];
|
|
4094
|
+
if (!seg || seg.finger !== currentFinger) {
|
|
4095
|
+
import_react_hot_toast6.toast.error("No se recibi\xF3 el segmento esperado.");
|
|
4096
|
+
return;
|
|
4097
|
+
}
|
|
4098
|
+
setFingerResults((prev) => ({
|
|
4099
|
+
...prev,
|
|
4100
|
+
[seg.finger]: {
|
|
4101
|
+
Finger: seg.finger,
|
|
4102
|
+
Quality: seg.quality,
|
|
4103
|
+
Image: seg.image,
|
|
4104
|
+
Error: null
|
|
4105
|
+
}
|
|
4106
|
+
}));
|
|
4107
|
+
if (seg.image) {
|
|
4108
|
+
addRolledFingerprint(seg.finger, seg.image, seg.quality || 100);
|
|
4109
|
+
}
|
|
4110
|
+
import_react_hot_toast6.toast.success(`Huella rolada capturada (${seg.quality}%)`);
|
|
4111
|
+
setCurrentFinger(null);
|
|
4112
|
+
setPreviewImage(null);
|
|
4113
|
+
setPreviewQuality(null);
|
|
4114
|
+
setRollStatus("Coloque el dedo...");
|
|
4115
|
+
};
|
|
4116
|
+
const handleError = (d) => {
|
|
4117
|
+
const msg = d.error || d.messageType || "Error desconocido";
|
|
4118
|
+
import_react_hot_toast6.toast.error(msg);
|
|
4119
|
+
setIsModalOpen(false);
|
|
4120
|
+
setIsProcessing(false);
|
|
4121
|
+
setCurrentFinger(null);
|
|
4122
|
+
setPreviewImage(null);
|
|
4123
|
+
setPreviewQuality(null);
|
|
4124
|
+
};
|
|
4125
|
+
const handleEnrollmentComplete = (d) => {
|
|
4126
|
+
const wsqs = d.biometricData?.wsqFiles;
|
|
4127
|
+
if (wsqs) {
|
|
4128
|
+
Object.entries(wsqs).forEach(([pos, base64]) => {
|
|
4129
|
+
const localResult = fingerResults[pos];
|
|
4130
|
+
const imageToStore = localResult?.Image || base64;
|
|
4131
|
+
const quality = localResult?.Quality || 100;
|
|
4132
|
+
addRolledFingerprint(pos, imageToStore, quality);
|
|
4133
|
+
});
|
|
4134
|
+
addMessage("Huellas roladas guardadas en memoria.");
|
|
4135
|
+
}
|
|
4136
|
+
import_react_hot_toast6.toast.success("Procesamiento finalizado");
|
|
4137
|
+
setIsProcessing(false);
|
|
4138
|
+
};
|
|
4139
|
+
registerHandler("FINGER_PREVIEW_FRAME", handlePreview);
|
|
4140
|
+
registerHandler("FINGER_ROLL_START_SIGNAL", handleRollStart);
|
|
4141
|
+
registerHandler("FINGER_ROLL_COMPLETE_SIGNAL", handleRollComplete);
|
|
4142
|
+
registerHandler("FINGER_CAPTURE_COMPLETE", handleCaptureComplete);
|
|
4143
|
+
registerHandler("ENROLLMENT_COMPLETE", handleEnrollmentComplete);
|
|
4144
|
+
registerHandler("ERROR", handleError);
|
|
4145
|
+
registerHandler("FINGER_ERROR", handleError);
|
|
4146
|
+
registerHandler("DUPLICATE_FINGER", handleError);
|
|
4147
|
+
registerHandler("FINGER_QUALITY_LOW", handleError);
|
|
4148
|
+
return () => {
|
|
4149
|
+
unregisterHandler("FINGER_PREVIEW_FRAME");
|
|
4150
|
+
unregisterHandler("FINGER_ROLL_START_SIGNAL");
|
|
4151
|
+
unregisterHandler("FINGER_ROLL_COMPLETE_SIGNAL");
|
|
4152
|
+
unregisterHandler("FINGER_CAPTURE_COMPLETE");
|
|
4153
|
+
unregisterHandler("ENROLLMENT_COMPLETE");
|
|
4154
|
+
unregisterHandler("ERROR");
|
|
4155
|
+
unregisterHandler("FINGER_ERROR");
|
|
4156
|
+
unregisterHandler("DUPLICATE_FINGER");
|
|
4157
|
+
unregisterHandler("FINGER_QUALITY_LOW");
|
|
4158
|
+
};
|
|
4159
|
+
}, [
|
|
4160
|
+
registerHandler,
|
|
4161
|
+
unregisterHandler,
|
|
4162
|
+
currentFinger,
|
|
4163
|
+
addMessage,
|
|
4164
|
+
addRolledFingerprint,
|
|
4165
|
+
fingerResults
|
|
4166
|
+
]);
|
|
4167
|
+
const startRollCapture = (finger) => {
|
|
4168
|
+
if (!isConnected) return import_react_hot_toast6.toast.error("Servicio no conectado.");
|
|
4169
|
+
if (allMissingFingers[finger]) {
|
|
4170
|
+
import_react_hot_toast6.toast.error(`El dedo ${FINGER_NAMES[finger]} est\xE1 marcado como omitido.`);
|
|
4171
|
+
return;
|
|
4172
|
+
}
|
|
4173
|
+
setIsProcessing(true);
|
|
4174
|
+
setIsModalOpen(true);
|
|
4175
|
+
setCurrentFinger(finger);
|
|
4176
|
+
setPreviewImage(null);
|
|
4177
|
+
setPreviewQuality(null);
|
|
4178
|
+
setRollStatus(`Coloque ${FINGER_NAMES[finger]}...`);
|
|
4179
|
+
addMessage(`Iniciando captura rolada: ${FINGER_NAMES[finger]}`);
|
|
4180
|
+
sendMessage({
|
|
4181
|
+
messageType: "START_FINGER_ROLL_CAPTURE",
|
|
4182
|
+
biometricData: {
|
|
4183
|
+
targetFinger: finger,
|
|
4184
|
+
expectedFingers: [finger]
|
|
4185
|
+
}
|
|
4186
|
+
});
|
|
4187
|
+
};
|
|
4188
|
+
const cancelCapture = () => {
|
|
4189
|
+
sendMessage({ messageType: "ABORT_FINGER_ROLL_CAPTURE" });
|
|
4190
|
+
setIsModalOpen(false);
|
|
4191
|
+
setIsProcessing(false);
|
|
4192
|
+
setCurrentFinger(null);
|
|
4193
|
+
setPreviewImage(null);
|
|
4194
|
+
setPreviewQuality(null);
|
|
4195
|
+
};
|
|
4196
|
+
const processBatch = () => {
|
|
4197
|
+
if (capturedCount < 10) {
|
|
4198
|
+
import_react_hot_toast6.toast.error(`Faltan huellas por procesar (${capturedCount}/10).`);
|
|
4199
|
+
return;
|
|
4200
|
+
}
|
|
4201
|
+
setIsProcessing(true);
|
|
4202
|
+
sendMessage({ messageType: "FINALIZE_ENROLLMENT" });
|
|
4203
|
+
};
|
|
4204
|
+
const resetState = () => {
|
|
4205
|
+
setFingerResults({});
|
|
4206
|
+
setIsProcessing(false);
|
|
4207
|
+
clearAllMissingFingers();
|
|
4208
|
+
addMessage("M\xF3dulo reiniciado.");
|
|
4209
|
+
};
|
|
4210
|
+
const handleConfirmMissing = (reason) => {
|
|
4211
|
+
if (fingerToMarkMissing) {
|
|
4212
|
+
markFingerAsMissing(fingerToMarkMissing, reason);
|
|
4213
|
+
addMissingFinger(fingerToMarkMissing, reason);
|
|
4214
|
+
setFingerResults((prev) => {
|
|
4215
|
+
const next = { ...prev };
|
|
4216
|
+
delete next[fingerToMarkMissing];
|
|
4217
|
+
return next;
|
|
4218
|
+
});
|
|
4219
|
+
removeRolledFingerprint(fingerToMarkMissing);
|
|
4220
|
+
addMessage(`Dedo ${fingerToMarkMissing} marcado como: ${reason}`);
|
|
4221
|
+
setFingerToMarkMissing(null);
|
|
4222
|
+
}
|
|
4223
|
+
};
|
|
4224
|
+
const renderFingerGrid = (fingers) => fingers.map((finger) => {
|
|
4225
|
+
const res = fingerResults[finger];
|
|
4226
|
+
const missing = allMissingFingers[finger] || null;
|
|
4227
|
+
return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
|
|
4228
|
+
BiometricSlot,
|
|
4229
|
+
{
|
|
4230
|
+
position: finger,
|
|
4231
|
+
title: FINGER_NAMES[finger].split(" ")[0],
|
|
4232
|
+
status: res ? res.Error ? "error" : "captured" : "pending",
|
|
4233
|
+
iconType: "finger",
|
|
4234
|
+
imageUrl: getImageSrcForDisplay(res?.Image, "png"),
|
|
4235
|
+
quality: res?.Quality,
|
|
4236
|
+
missingReason: missing,
|
|
4237
|
+
onCapture: () => startRollCapture(finger),
|
|
4238
|
+
onRequestMissing: () => setFingerToMarkMissing(finger),
|
|
4239
|
+
onRestore: () => {
|
|
4240
|
+
removeMissingFinger(finger);
|
|
4241
|
+
if (missingFingers[finger]) restoreFinger(finger);
|
|
4242
|
+
},
|
|
4243
|
+
onDelete: () => {
|
|
4244
|
+
removeRolledFingerprint(finger);
|
|
4245
|
+
setFingerResults((prev) => {
|
|
4246
|
+
const next = { ...prev };
|
|
4247
|
+
delete next[finger];
|
|
4248
|
+
return next;
|
|
4249
|
+
});
|
|
4250
|
+
},
|
|
4251
|
+
isDisabled: !isConnected || isCapturing
|
|
4252
|
+
},
|
|
4253
|
+
finger
|
|
4254
|
+
);
|
|
4255
|
+
});
|
|
4256
|
+
return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "space-y-8 animate-in fade-in duration-500", children: [
|
|
4257
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
|
|
4258
|
+
ModuleHeader,
|
|
4259
|
+
{
|
|
4260
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_tb16.TbFingerprint, { size: 24 }),
|
|
4261
|
+
title: "Huellas Roladas",
|
|
4262
|
+
subtitle: "Capture las huellas rodando el dedo de un lado a otro.",
|
|
4263
|
+
progress: { current: capturedCount, total: 10 },
|
|
4264
|
+
children: [
|
|
4265
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
|
|
4266
|
+
Button,
|
|
4267
|
+
{
|
|
4268
|
+
variant: "success",
|
|
4269
|
+
size: "sm",
|
|
4270
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_tb16.TbSend, { size: 16 }),
|
|
4271
|
+
onClick: processBatch,
|
|
4272
|
+
disabled: !isConnected || isCapturing || capturedCount < 10,
|
|
4273
|
+
isLoading: isProcessing,
|
|
4274
|
+
children: "Guardar"
|
|
4275
|
+
}
|
|
4276
|
+
),
|
|
4277
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
|
|
4278
|
+
Button,
|
|
4279
|
+
{
|
|
4280
|
+
variant: "ghost",
|
|
4281
|
+
size: "sm",
|
|
4282
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_tb16.TbReload, { size: 16 }),
|
|
4283
|
+
onClick: resetState,
|
|
4284
|
+
disabled: isCapturing,
|
|
4285
|
+
children: "Limpiar"
|
|
4286
|
+
}
|
|
4287
|
+
)
|
|
4288
|
+
]
|
|
4289
|
+
}
|
|
4290
|
+
),
|
|
4291
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "grid grid-cols-1 xl:grid-cols-2 gap-8", children: [
|
|
4292
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "glass p-6 rounded-sm border border-white/10 relative overflow-hidden group", children: [
|
|
4293
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity", children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_tb16.TbFingerprint, { size: 120 }) }),
|
|
4294
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsx)("h3", { className: "text-lg font-bold text-foreground mb-5 pl-3 border-l-4 border-primary", children: "Mano Derecha" }),
|
|
4295
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-4", children: renderFingerGrid(RIGHT_FINGERS) })
|
|
4296
|
+
] }),
|
|
4297
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "glass p-6 rounded-sm border border-white/10 relative overflow-hidden group", children: [
|
|
4298
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity", children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_tb16.TbFingerprint, { size: 120 }) }),
|
|
4299
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsx)("h3", { className: "text-lg font-bold text-foreground mb-5 pl-3 border-l-4 border-secondary-foreground/50", children: "Mano Izquierda" }),
|
|
4300
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-4", children: renderFingerGrid(LEFT_FINGERS) })
|
|
4301
|
+
] })
|
|
4302
|
+
] }),
|
|
4303
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
|
|
4304
|
+
FingerRollCaptureModal,
|
|
4305
|
+
{
|
|
4306
|
+
isOpen: isModalOpen,
|
|
4307
|
+
onCancel: cancelCapture,
|
|
4308
|
+
captureType: currentFinger ? `${FINGER_NAMES[currentFinger]} (Rolado)` : "Captura Rolada",
|
|
4309
|
+
previewImage,
|
|
4310
|
+
previewQuality,
|
|
4311
|
+
rollStatus
|
|
4312
|
+
}
|
|
4313
|
+
),
|
|
4314
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
|
|
4315
|
+
MissingFingerModal,
|
|
4316
|
+
{
|
|
4317
|
+
isOpen: !!fingerToMarkMissing,
|
|
4318
|
+
onClose: () => setFingerToMarkMissing(null),
|
|
4319
|
+
onConfirm: handleConfirmMissing,
|
|
4320
|
+
positionName: fingerToMarkMissing ? FINGER_NAMES[fingerToMarkMissing] : ""
|
|
4321
|
+
}
|
|
4322
|
+
)
|
|
4323
|
+
] });
|
|
4324
|
+
}
|
|
4325
|
+
|
|
4326
|
+
// src/components/ui/Card.tsx
|
|
4327
|
+
var import_jsx_runtime21 = require("react/jsx-runtime");
|
|
4328
|
+
function Card({
|
|
4329
|
+
variant = "default",
|
|
4330
|
+
padding = "md",
|
|
4331
|
+
rounded = "3xl",
|
|
4332
|
+
className = "",
|
|
4333
|
+
children
|
|
4334
|
+
}) {
|
|
4335
|
+
const variantClasses = {
|
|
4336
|
+
subtle: "bg-card/50 elevation-1 hover:elevation-2 hover:-translate-y-1",
|
|
4337
|
+
default: "bg-card/60 elevation-2 hover:elevation-3 hover:-translate-y-1",
|
|
4338
|
+
medium: "bg-card/70 elevation-3 hover:elevation-4 hover:-translate-y-1",
|
|
4339
|
+
strong: "bg-card/80 elevation-4",
|
|
4340
|
+
modal: "bg-card/90 elevation-5",
|
|
4341
|
+
// Mantener las antiguas para compatibilidad
|
|
4342
|
+
glass: "glass",
|
|
4343
|
+
"glass-subtle": "glass-subtle",
|
|
4344
|
+
"glass-strong": "glass-strong",
|
|
4345
|
+
outlined: "bg-transparent border-2 border-border-emphasis elevation-1"
|
|
4346
|
+
};
|
|
4347
|
+
const paddingClasses = {
|
|
4348
|
+
none: "p-0",
|
|
4349
|
+
sm: "p-4",
|
|
4350
|
+
md: "p-6 md:p-8",
|
|
4351
|
+
lg: "p-8 md:p-12"
|
|
4352
|
+
};
|
|
4353
|
+
const roundedClasses = {
|
|
4354
|
+
lg: "rounded-sm",
|
|
4355
|
+
xl: "rounded-md",
|
|
4356
|
+
"2xl": "rounded-md",
|
|
4357
|
+
"3xl": "rounded-md"
|
|
4358
|
+
};
|
|
4359
|
+
return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: `
|
|
4360
|
+
${variantClasses[variant]}
|
|
4361
|
+
${paddingClasses[padding]}
|
|
4362
|
+
${roundedClasses[rounded]}
|
|
4363
|
+
transition-all duration-300
|
|
4364
|
+
${className}
|
|
4365
|
+
`, children });
|
|
4366
|
+
}
|
|
4367
|
+
|
|
4368
|
+
// src/components/ui/Input.tsx
|
|
4369
|
+
var import_jsx_runtime22 = require("react/jsx-runtime");
|
|
4370
|
+
function Input({
|
|
4371
|
+
label,
|
|
4372
|
+
icon,
|
|
4373
|
+
error,
|
|
4374
|
+
className = "",
|
|
4375
|
+
...props
|
|
4376
|
+
}) {
|
|
4377
|
+
return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "space-y-2 w-full", children: [
|
|
4378
|
+
label && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("label", { className: "block text-xs font-semibold uppercase tracking-wider text-foreground/50 ml-1", children: label }),
|
|
4379
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "relative", children: [
|
|
4380
|
+
icon && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { className: "absolute left-4 top-1/2 -translate-y-1/2 text-foreground/40", children: icon }),
|
|
4381
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
|
|
4382
|
+
"input",
|
|
4383
|
+
{
|
|
4384
|
+
className: `
|
|
4385
|
+
w-full bg-secondary/50 border border-border-default
|
|
4386
|
+
rounded-md py-3 px-4
|
|
4387
|
+
${icon ? "pl-11" : ""}
|
|
4388
|
+
|
|
4389
|
+
elevation-1
|
|
4390
|
+
hover:elevation-2 hover:-translate-y-0.5
|
|
4391
|
+
focus:elevation-3 focus:ring-2 focus:ring-primary/50 focus:glow-primary focus:-translate-y-1
|
|
4392
|
+
|
|
4393
|
+
outline-none
|
|
4394
|
+
transition-all duration-250
|
|
4395
|
+
text-foreground placeholder:text-foreground/30
|
|
4396
|
+
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:translate-y-0
|
|
4397
|
+
|
|
4398
|
+
${error ? "border-danger/50 focus:ring-danger/50 focus:glow-danger" : ""}
|
|
4399
|
+
${className}
|
|
4400
|
+
`,
|
|
4401
|
+
...props
|
|
4402
|
+
}
|
|
4403
|
+
)
|
|
4404
|
+
] }),
|
|
4405
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("p", { className: "text-xs text-danger ml-1", children: error })
|
|
4406
|
+
] });
|
|
4407
|
+
}
|
|
4408
|
+
|
|
4409
|
+
// src/components/ui/StatusChip.tsx
|
|
4410
|
+
var import_tb17 = require("react-icons/tb");
|
|
4411
|
+
var import_jsx_runtime23 = require("react/jsx-runtime");
|
|
4412
|
+
function StatusChip({
|
|
4413
|
+
status,
|
|
4414
|
+
label,
|
|
4415
|
+
icon,
|
|
4416
|
+
isLoading = false,
|
|
4417
|
+
size = "md",
|
|
4418
|
+
showDot = true
|
|
4419
|
+
}) {
|
|
4420
|
+
const statusClasses = {
|
|
4421
|
+
success: `
|
|
4422
|
+
bg-success/10 border-success/20 text-success
|
|
4423
|
+
elevation-1 glow-success
|
|
4424
|
+
hover:scale-105 hover:elevation-2
|
|
4425
|
+
`,
|
|
4426
|
+
warning: `
|
|
4427
|
+
bg-warning/10 border-warning/20 text-warning
|
|
4428
|
+
elevation-1 glow-warning
|
|
4429
|
+
hover:scale-105 hover:elevation-2
|
|
4430
|
+
animate-pulse
|
|
4431
|
+
`,
|
|
4432
|
+
danger: `
|
|
4433
|
+
bg-danger/10 border-danger/20 text-danger
|
|
4434
|
+
elevation-1 glow-danger
|
|
4435
|
+
hover:scale-105 hover:elevation-2
|
|
4436
|
+
`,
|
|
4437
|
+
info: `
|
|
4438
|
+
bg-info/10 border-info/20 text-info
|
|
4439
|
+
elevation-1
|
|
4440
|
+
shadow-[0_0_12px_rgba(59,130,246,0.2)]
|
|
4441
|
+
hover:scale-105 hover:elevation-2
|
|
4442
|
+
`,
|
|
4443
|
+
neutral: `
|
|
4444
|
+
bg-secondary/50 border-border-default text-foreground/60
|
|
4445
|
+
elevation-1
|
|
4446
|
+
hover:scale-105 hover:elevation-2
|
|
4447
|
+
`
|
|
4448
|
+
};
|
|
4449
|
+
const sizeClasses = {
|
|
4450
|
+
sm: "px-2 py-1 text-[10px] rounded-custom-sm",
|
|
4451
|
+
md: "px-3 py-1.5 text-xs rounded-custom-md"
|
|
4452
|
+
};
|
|
4453
|
+
return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: `
|
|
4454
|
+
inline-flex items-center gap-2 border font-semibold uppercase tracking-wider
|
|
4455
|
+
transition-all duration-250
|
|
4456
|
+
${statusClasses[status]}
|
|
4457
|
+
${sizeClasses[size]}
|
|
4458
|
+
`, children: [
|
|
4459
|
+
isLoading ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_tb17.TbLoader, { className: "animate-spin" }) : icon,
|
|
4460
|
+
/* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { children: label }),
|
|
4461
|
+
showDot && !isLoading && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { className: `w-1.5 h-1.5 rounded-full bg-current ${status === "success" ? "shadow-[0_0_8px_currentColor] animate-pulse" : ""}` })
|
|
4462
|
+
] });
|
|
4463
|
+
}
|
|
4464
|
+
|
|
4465
|
+
// src/components/ui/Tabs.tsx
|
|
4466
|
+
var React3 = __toESM(require("react"));
|
|
4467
|
+
var import_jsx_runtime24 = require("react/jsx-runtime");
|
|
4468
|
+
function cn(...classes) {
|
|
4469
|
+
return classes.filter(Boolean).join(" ");
|
|
4470
|
+
}
|
|
4471
|
+
var TabsContext = React3.createContext(null);
|
|
4472
|
+
function Tabs({
|
|
4473
|
+
defaultValue,
|
|
4474
|
+
value,
|
|
4475
|
+
// Agregado para soportar modo controlado si se necesita
|
|
4476
|
+
className,
|
|
4477
|
+
children,
|
|
4478
|
+
onValueChange
|
|
4479
|
+
}) {
|
|
4480
|
+
const [internalTab, setInternalTab] = React3.useState(defaultValue || "");
|
|
4481
|
+
const activeTab = value !== void 0 ? value : internalTab;
|
|
4482
|
+
const handleTabChange = (val) => {
|
|
4483
|
+
setInternalTab(val);
|
|
4484
|
+
if (onValueChange) onValueChange(val);
|
|
4485
|
+
};
|
|
4486
|
+
return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(TabsContext.Provider, { value: { activeTab, setActiveTab: handleTabChange }, children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { className: cn("w-full", className), children }) });
|
|
4487
|
+
}
|
|
4488
|
+
function TabsList({
|
|
4489
|
+
className,
|
|
4490
|
+
children
|
|
4491
|
+
}) {
|
|
4492
|
+
return (
|
|
4493
|
+
// Quitamos 'inline-flex' y 'h-12' fijo para permitir responsive real
|
|
4494
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
4495
|
+
"div",
|
|
4496
|
+
{
|
|
4497
|
+
className: cn(
|
|
4498
|
+
"flex items-center justify-start w-full overflow-x-auto scrollbar-hide",
|
|
4499
|
+
className
|
|
4500
|
+
),
|
|
4501
|
+
children
|
|
4502
|
+
}
|
|
4503
|
+
)
|
|
4504
|
+
);
|
|
4505
|
+
}
|
|
4506
|
+
function TabsTrigger({
|
|
4507
|
+
value,
|
|
4508
|
+
children,
|
|
4509
|
+
className,
|
|
4510
|
+
disabled
|
|
4511
|
+
}) {
|
|
4512
|
+
const context = React3.useContext(TabsContext);
|
|
4513
|
+
if (!context) throw new Error("TabsTrigger must be used within Tabs");
|
|
4514
|
+
const isActive = context.activeTab === value;
|
|
4515
|
+
return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
4516
|
+
"button",
|
|
4517
|
+
{
|
|
4518
|
+
onClick: () => !disabled && context.setActiveTab(value),
|
|
4519
|
+
disabled,
|
|
4520
|
+
"data-state": isActive ? "active" : "inactive",
|
|
4521
|
+
className: cn(
|
|
4522
|
+
"inline-flex items-center justify-center whitespace-nowrap px-4 py-2 text-sm font-medium transition-all ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
4523
|
+
className
|
|
4524
|
+
),
|
|
4525
|
+
children
|
|
4526
|
+
}
|
|
4527
|
+
);
|
|
4528
|
+
}
|
|
4529
|
+
function TabsContent({
|
|
4530
|
+
value,
|
|
4531
|
+
children,
|
|
4532
|
+
className
|
|
4533
|
+
}) {
|
|
4534
|
+
const context = React3.useContext(TabsContext);
|
|
4535
|
+
if (!context) throw new Error("TabsContent must be used within Tabs");
|
|
4536
|
+
if (context.activeTab !== value) return null;
|
|
4537
|
+
return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
4538
|
+
"div",
|
|
4539
|
+
{
|
|
4540
|
+
className: cn(
|
|
4541
|
+
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 animate-in fade-in zoom-in-95 duration-200",
|
|
4542
|
+
className
|
|
4543
|
+
),
|
|
4544
|
+
children
|
|
4545
|
+
}
|
|
4546
|
+
);
|
|
4547
|
+
}
|
|
4548
|
+
|
|
4549
|
+
// src/components/ui/TableWrapper.tsx
|
|
4550
|
+
var import_jsx_runtime25 = require("react/jsx-runtime");
|
|
4551
|
+
function cn2(...classes) {
|
|
4552
|
+
return classes.filter(Boolean).join(" ");
|
|
4553
|
+
}
|
|
4554
|
+
var TableWrapper = ({
|
|
4555
|
+
children,
|
|
4556
|
+
className,
|
|
4557
|
+
variant = "default"
|
|
4558
|
+
}) => {
|
|
4559
|
+
const elevationClass = variant === "strong" ? "elevation-3" : "elevation-2";
|
|
4560
|
+
return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: cn2(
|
|
4561
|
+
"rounded-md overflow-hidden",
|
|
4562
|
+
elevationClass,
|
|
4563
|
+
className
|
|
4564
|
+
), children });
|
|
4565
|
+
};
|
|
4566
|
+
var TableHeader = ({ children, className }) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("thead", { className: cn2(
|
|
4567
|
+
"elevation-3",
|
|
4568
|
+
"bg-card/80",
|
|
4569
|
+
className
|
|
4570
|
+
), children });
|
|
4571
|
+
var TableRow = ({ children, onClick, className }) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
|
|
4572
|
+
"tr",
|
|
4573
|
+
{
|
|
4574
|
+
onClick,
|
|
4575
|
+
className: cn2(
|
|
4576
|
+
"bg-card/40",
|
|
4577
|
+
"elevation-1",
|
|
4578
|
+
"hover:elevation-2",
|
|
4579
|
+
"hover:-translate-y-0.5",
|
|
4580
|
+
"hover:bg-card/60",
|
|
4581
|
+
"transition-all duration-250",
|
|
4582
|
+
onClick && "cursor-pointer",
|
|
4583
|
+
className
|
|
4584
|
+
),
|
|
4585
|
+
children
|
|
4586
|
+
}
|
|
4587
|
+
);
|
|
4588
|
+
|
|
4589
|
+
// src/lib/api.ts
|
|
4590
|
+
var import_axios = __toESM(require("axios"));
|
|
4591
|
+
|
|
4592
|
+
// src/lib/stores/authStore.ts
|
|
4593
|
+
var import_zustand4 = require("zustand");
|
|
4594
|
+
var import_js_cookie = __toESM(require("js-cookie"));
|
|
4595
|
+
var useAuthStore = (0, import_zustand4.create)((set) => ({
|
|
4596
|
+
token: null,
|
|
4597
|
+
user: null,
|
|
4598
|
+
login: (token, user) => {
|
|
4599
|
+
import_js_cookie.default.set("auth-token", token, {
|
|
4600
|
+
expires: 7,
|
|
4601
|
+
sameSite: "strict",
|
|
4602
|
+
secure: typeof window !== "undefined" && window.location.protocol === "https:"
|
|
4603
|
+
});
|
|
4604
|
+
localStorage.setItem("auth-user", JSON.stringify(user));
|
|
4605
|
+
set({ token, user });
|
|
4606
|
+
},
|
|
4607
|
+
logout: () => {
|
|
4608
|
+
import_js_cookie.default.remove("auth-token");
|
|
4609
|
+
localStorage.removeItem("auth-user");
|
|
4610
|
+
set({ token: null, user: null });
|
|
4611
|
+
},
|
|
4612
|
+
initializeAuth: () => {
|
|
4613
|
+
const token = import_js_cookie.default.get("auth-token") || null;
|
|
4614
|
+
const userStr = localStorage.getItem("auth-user");
|
|
4615
|
+
const user = userStr ? JSON.parse(userStr) : null;
|
|
4616
|
+
set({ token, user });
|
|
4617
|
+
}
|
|
4618
|
+
}));
|
|
4619
|
+
|
|
4620
|
+
// src/lib/api.ts
|
|
4621
|
+
var _apiBaseUrl = "http://localhost:8080/api/v1";
|
|
4622
|
+
function setApiBaseUrl(url) {
|
|
4623
|
+
_apiBaseUrl = url;
|
|
4624
|
+
api.defaults.baseURL = url;
|
|
4625
|
+
}
|
|
4626
|
+
var api = import_axios.default.create({
|
|
4627
|
+
baseURL: _apiBaseUrl,
|
|
4628
|
+
headers: { "Content-Type": "application/json" }
|
|
4629
|
+
});
|
|
4630
|
+
api.interceptors.request.use((config) => {
|
|
4631
|
+
const token = useAuthStore.getState().token;
|
|
4632
|
+
if (token) {
|
|
4633
|
+
config.headers["x-access-token"] = token;
|
|
4634
|
+
}
|
|
4635
|
+
return config;
|
|
4636
|
+
});
|
|
4637
|
+
var api_default = api;
|
|
4638
|
+
|
|
4639
|
+
// src/lib/services/biometricService.ts
|
|
4640
|
+
var AuthService = {
|
|
4641
|
+
login: async (credentials) => {
|
|
4642
|
+
const { data } = await api_default.post("/auth/login", credentials);
|
|
4643
|
+
return data;
|
|
4644
|
+
}
|
|
4645
|
+
};
|
|
4646
|
+
var BiometricService = {
|
|
4647
|
+
enrollApplicant: async (payload) => {
|
|
4648
|
+
const { data } = await api_default.post("/biometric/enrollment", payload);
|
|
4649
|
+
return data;
|
|
4650
|
+
},
|
|
4651
|
+
updateApplicant: async (externalId, payload) => {
|
|
4652
|
+
const { data } = await api_default.put(`/biometric/update/${externalId}`, payload);
|
|
4653
|
+
return data;
|
|
4654
|
+
},
|
|
4655
|
+
getApplicant: async (externalId) => {
|
|
4656
|
+
const { data } = await api_default.get(`/biometric/${externalId}`);
|
|
4657
|
+
return data;
|
|
4658
|
+
},
|
|
4659
|
+
verifyIdentity: async (externalId, biometricData) => {
|
|
4660
|
+
const { data } = await api_default.post(`/biometric/${externalId}/verify`, biometricData);
|
|
4661
|
+
return data;
|
|
4662
|
+
},
|
|
4663
|
+
identifyProbe: async (biometricData) => {
|
|
4664
|
+
const { data } = await api_default.post(`/biometric/identify`, biometricData);
|
|
4665
|
+
return data;
|
|
4666
|
+
}
|
|
4667
|
+
};
|
|
4668
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
4669
|
+
0 && (module.exports = {
|
|
4670
|
+
AuthService,
|
|
4671
|
+
BiometricProvider,
|
|
4672
|
+
BiometricService,
|
|
4673
|
+
BiometricSlot,
|
|
4674
|
+
Button,
|
|
4675
|
+
Card,
|
|
4676
|
+
FINGER_NAMES,
|
|
4677
|
+
FaceCameraModal,
|
|
4678
|
+
FaceEnrollModule,
|
|
4679
|
+
FingerCaptureModal,
|
|
4680
|
+
FingerEnrollModule,
|
|
4681
|
+
FingerRollCaptureModal,
|
|
4682
|
+
FingerRollEnrollModule,
|
|
4683
|
+
FingerprintImpressionType,
|
|
4684
|
+
FingerprintPosition,
|
|
4685
|
+
Input,
|
|
4686
|
+
IrisCameraModal,
|
|
4687
|
+
IrisEnrollModule,
|
|
4688
|
+
IrisPosition,
|
|
4689
|
+
LEFT_FINGERS,
|
|
4690
|
+
Loader,
|
|
4691
|
+
MissingBiometricModal,
|
|
4692
|
+
MissingFingerModal,
|
|
4693
|
+
Modal,
|
|
4694
|
+
ModuleHeader,
|
|
4695
|
+
PalmCaptureModal,
|
|
4696
|
+
PalmEnrollModule,
|
|
4697
|
+
ProgressBar,
|
|
4698
|
+
QualityBadge,
|
|
4699
|
+
RIGHT_FINGERS,
|
|
4700
|
+
Select,
|
|
4701
|
+
StatusChip,
|
|
4702
|
+
TableHeader,
|
|
4703
|
+
TableRow,
|
|
4704
|
+
TableWrapper,
|
|
4705
|
+
Tabs,
|
|
4706
|
+
TabsContent,
|
|
4707
|
+
TabsList,
|
|
4708
|
+
TabsTrigger,
|
|
4709
|
+
api,
|
|
4710
|
+
base64toBlob,
|
|
4711
|
+
canvasToBase64,
|
|
4712
|
+
detectImageFormat,
|
|
4713
|
+
downloadImageAsBase64,
|
|
4714
|
+
getImageSrcForDisplay,
|
|
4715
|
+
isRemoteImage,
|
|
4716
|
+
isValidBase64,
|
|
4717
|
+
normalizeImageForStorage,
|
|
4718
|
+
setApiBaseUrl,
|
|
4719
|
+
useAuthStore,
|
|
4720
|
+
useBiometricConfig,
|
|
4721
|
+
useBiometricStore,
|
|
4722
|
+
useEnrollmentStore,
|
|
4723
|
+
useUiStore
|
|
4724
|
+
});
|
|
4725
|
+
//# sourceMappingURL=index.js.map
|