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