{"id":"cs-1782349967055-e6p7oos9","status":"done","createdAt":1782349967055,"finishedAt":1782350049921,"meta":{"projectName":"ALimpiezas","projectPath":"C:\\xampp\\htdocs\\ALimpiezas","messagePreview":"Debes elegir un archivo, una foto de galería o hacer una foto.\nEn modo OCR/IA el documento es obligatorio.\nUsa «Elegir a","phaseId":null},"events":[{"event":"status","data":{"text":"Modo IDE / sesión — completo (80 herramientas, uso selectivo)"},"at":1782349967059},{"event":"status","data":{"text":"Agente"},"at":1782349967061},{"event":"status","data":{"text":"Modo revisión: snapshot local por archivo"},"at":1782349967061},{"event":"status","data":{"text":"Contexto del IDE cargado"},"at":1782349967061},{"event":"agent","data":{"agentId":"agent-18f3257a-043c-4892-a88b-6efdf18acae9","projectPath":"C:\\xampp\\htdocs\\ALimpiezas"},"at":1782349967061},{"event":"changes","data":{"sessionId":"cs-1782349967058-c8w8dj","pending":0},"at":1782349967061},{"event":"status","data":{"text":"Procesando…"},"at":1782349967123},{"event":"text","data":{"chunk":"Continúo el hilo: reviso los archivos móvil ya identificados para corregir la validación tras subir y añadir soporte de varios archivos.\n"},"at":1782350049921},{"event":"tool_activity","data":{"activity":{"id":"tool_c72a44b1-8405-4d9c-a377-fd1fc62018a","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Dinamic\\Assets\\JS\\mobile.js","detail":"","at":"2026-06-25T01:12:49.867Z"}},"at":1782349969867},{"event":"tool_activity","data":{"activity":{"id":"tool_ab2032ec-ac7e-4350-ade8-2b3329dd8cc","name":"glob","status":"running","summary":"glob: ","detail":"","at":"2026-06-25T01:12:49.967Z"}},"at":1782349969967},{"event":"tool_activity","data":{"activity":{"id":"tool_c72a44b1-8405-4d9c-a377-fd1fc62018a","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Dinamic\\Assets\\JS\\mobile.js","detail":"","at":"2026-06-25T01:12:50.021Z"}},"at":1782349970021},{"event":"tool_activity","data":{"activity":{"id":"tool_7116e752-6ad5-448c-93bb-a7601dae4d3","name":"grep","status":"running","summary":"grep: C:\\xampp\\htdocs\\ALimpiezas","detail":"","at":"2026-06-25T01:12:50.111Z"}},"at":1782349970111},{"event":"tool_activity","data":{"activity":{"id":"tool_ab2032ec-ac7e-4350-ade8-2b3329dd8cc","name":"glob","status":"completed","summary":"glob: ","detail":"","at":"2026-06-25T01:12:50.140Z"}},"at":1782349970140},{"event":"tool_activity","data":{"activity":{"id":"tool_7116e752-6ad5-448c-93bb-a7601dae4d3","name":"grep","status":"completed","summary":"grep: C:\\xampp\\htdocs\\ALimpiezas","detail":"","at":"2026-06-25T01:12:50.289Z"}},"at":1782349970289},{"event":"tool_activity","data":{"activity":{"id":"tool_64691860-373a-4562-9f58-affe032c4e6","name":"read","status":"running","summary":"read: …ampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig","detail":"","at":"2026-06-25T01:12:51.342Z"}},"at":1782349971342},{"event":"tool_activity","data":{"activity":{"id":"tool_8dc7a98a-c247-4f50-b17f-7fc50f42433","name":"read","status":"running","summary":"read: …ampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php","detail":"","at":"2026-06-25T01:12:51.343Z"}},"at":1782349971343},{"event":"tool_activity","data":{"activity":{"id":"tool_d6449a32-8ac2-4146-9f36-d03a9d5d229","name":"glob","status":"running","summary":"glob: ","detail":"","at":"2026-06-25T01:12:51.345Z"}},"at":1782349971345},{"event":"tool_activity","data":{"activity":{"id":"tool_64691860-373a-4562-9f58-affe032c4e6","name":"read","status":"completed","summary":"read: …ampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig","detail":"","at":"2026-06-25T01:12:51.453Z"}},"at":1782349971453},{"event":"tool_activity","data":{"activity":{"id":"tool_8dc7a98a-c247-4f50-b17f-7fc50f42433","name":"read","status":"completed","summary":"read: …ampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php","detail":"","at":"2026-06-25T01:12:51.455Z"}},"at":1782349971455},{"event":"tool_activity","data":{"activity":{"id":"tool_d6449a32-8ac2-4146-9f36-d03a9d5d229","name":"glob","status":"completed","summary":"glob: ","detail":"","at":"2026-06-25T01:12:51.519Z"}},"at":1782349971519},{"event":"tool_activity","data":{"activity":{"id":"tool_b6059766-367d-495e-a52a-8515c2b4ad9","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Dinamic\\View\\MobileExpenses.html.twig","detail":"","at":"2026-06-25T01:12:57.080Z"}},"at":1782349977080},{"event":"tool_activity","data":{"activity":{"id":"tool_defca2d9-bda0-4de8-a7e1-a788005d03f","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php","detail":"","at":"2026-06-25T01:12:57.081Z"}},"at":1782349977081},{"event":"tool_activity","data":{"activity":{"id":"tool_073bbd10-d2d5-40e5-b4a2-883357b14ba","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\sw.js","detail":"","at":"2026-06-25T01:12:57.081Z"}},"at":1782349977081},{"event":"tool_activity","data":{"activity":{"id":"tool_84d9ceb8-b7e1-46a7-b9b7-a2b4075b9b6","name":"read","status":"running","summary":"read: …htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\Master\\MobileLayout.html.twig","detail":"","at":"2026-06-25T01:12:57.082Z"}},"at":1782349977082},{"event":"tool_activity","data":{"activity":{"id":"tool_b6059766-367d-495e-a52a-8515c2b4ad9","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Dinamic\\View\\MobileExpenses.html.twig","detail":"","at":"2026-06-25T01:12:57.285Z"}},"at":1782349977285},{"event":"tool_activity","data":{"activity":{"id":"tool_84d9ceb8-b7e1-46a7-b9b7-a2b4075b9b6","name":"read","status":"completed","summary":"read: …htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\Master\\MobileLayout.html.twig","detail":"","at":"2026-06-25T01:12:57.286Z"}},"at":1782349977286},{"event":"tool_activity","data":{"activity":{"id":"tool_073bbd10-d2d5-40e5-b4a2-883357b14ba","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\sw.js","detail":"","at":"2026-06-25T01:12:57.287Z"}},"at":1782349977287},{"event":"tool_activity","data":{"activity":{"id":"tool_defca2d9-bda0-4de8-a7e1-a788005d03f","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php","detail":"","at":"2026-06-25T01:12:57.327Z"}},"at":1782349977327},{"event":"tool_activity","data":{"activity":{"id":"tool_bff30292-c2f1-4306-a466-93a2d8b94c2","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js","detail":"","at":"2026-06-25T01:13:12.361Z"}},"at":1782349992361},{"event":"tool_activity","data":{"activity":{"id":"tool_139e21ca-7c24-46c9-96c3-91b0cbe7138","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Dinamic\\Controller\\MobileExpenses.php","detail":"","at":"2026-06-25T01:13:12.492Z"}},"at":1782349992492},{"event":"tool_activity","data":{"activity":{"id":"tool_baa7a161-de2c-4d29-b4ac-b6360e47c41","name":"grep","status":"completed","summary":"grep: C:\\xampp\\htdocs\\ALimpiezas","detail":"","at":"2026-06-25T01:13:12.794Z"}},"at":1782349992794},{"event":"tool_activity","data":{"activity":{"id":"tool_031569d3-7930-4161-b45b-51580215459","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\CSS\\mobile.css","detail":"","at":"2026-06-25T01:13:22.982Z"}},"at":1782350002982},{"event":"tool_activity","data":{"activity":{"id":"tool_0bc7ddfc-ca55-4e76-8b41-e26aefe09f7","name":"glob","status":"running","summary":"glob: ","detail":"","at":"2026-06-25T01:13:22.983Z"}},"at":1782350002983},{"event":"tool_activity","data":{"activity":{"id":"tool_031569d3-7930-4161-b45b-51580215459","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\CSS\\mobile.css","detail":"","at":"2026-06-25T01:13:23.085Z"}},"at":1782350003085},{"event":"tool_activity","data":{"activity":{"id":"tool_0bc7ddfc-ca55-4e76-8b41-e26aefe09f7","name":"glob","status":"completed","summary":"glob: ","detail":"","at":"2026-06-25T01:13:23.173Z"}},"at":1782350003173},{"event":"tool_activity","data":{"activity":{"id":"tool_caef5a6a-9098-40ab-b16f-f387c69e95f","name":"glob","status":"running","summary":"glob: ","detail":"","at":"2026-06-25T01:13:24.305Z"}},"at":1782350004305},{"event":"tool_activity","data":{"activity":{"id":"tool_ef00e6c9-57e3-43e5-bf3b-81f0235f02e","name":"glob","status":"running","summary":"glob: ","detail":"","at":"2026-06-25T01:13:24.375Z"}},"at":1782350004375},{"event":"tool_activity","data":{"activity":{"id":"tool_caef5a6a-9098-40ab-b16f-f387c69e95f","name":"glob","status":"completed","summary":"glob: ","detail":"","at":"2026-06-25T01:13:24.455Z"}},"at":1782350004455},{"event":"tool_activity","data":{"activity":{"id":"tool_ef00e6c9-57e3-43e5-bf3b-81f0235f02e","name":"glob","status":"completed","summary":"glob: ","detail":"","at":"2026-06-25T01:13:24.571Z"}},"at":1782350004571},{"event":"tool_activity","data":{"activity":{"id":"tool_4f59f8dc-b664-40e4-8b7a-07409136fc2","name":"shell","status":"completed","summary":"shell","detail":"if (Test-Path \"C:\\xampp\\htdocs\\ALimpiezas\\Dinamic\\Assets\\JS\\mobile.js\") { Get-Item \"C:\\xampp\\htdocs\\ALimpiezas\\Dinamic\\Assets\\JS\\mobile.js\" | Select-Object FullName, Length, LastWriteTime } else { Wr…","at":"2026-06-25T01:13:26.316Z"}},"at":1782350006316},{"event":"tool_activity","data":{"activity":{"id":"tool_d029b605-d994-44f4-9532-7f49baef92b","name":"shell","status":"completed","summary":"shell","detail":"Get-ChildItem \"C:\\xampp\\htdocs\\ALimpiezas\\Dinamic\\Assets\" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 20 FullName","at":"2026-06-25T01:13:27.792Z"}},"at":1782350007792},{"event":"tool_activity","data":{"activity":{"id":"tool_dd56ae01-c45e-4533-9a7a-629c36d4039","name":"shell","status":"completed","summary":"shell","detail":"Get-ChildItem \"C:\\xampp\\htdocs\\ALimpiezas\\Dinamic\" | Select-Object Name, Mode","at":"2026-06-25T01:13:29.382Z"}},"at":1782350009382},{"event":"tool_activity","data":{"activity":{"id":"tool_06ddcc0e-ce1b-4992-8306-d1d4f1f41b9","name":"shell","status":"completed","summary":"shell","detail":"cmd /c \"dir C:\\xampp\\htdocs\\ALimpiezas\\Dinamic\\Assets\\JS 2>&1\"","at":"2026-06-25T01:13:31.086Z"}},"at":1782350011086},{"event":"text","data":{"chunk":"Implementando un array en memoria como fuente única de verdad (más fiable en móvil) y soporte para varios archivos.\n"},"at":1782350049921},{"event":"tool_activity","data":{"activity":{"id":"tool_bf94028a-b312-4692-b9ee-9399ef0893b","name":"edit","status":"running","summary":"edit: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js","detail":"","at":"2026-06-25T01:13:53.229Z"}},"at":1782350033229},{"event":"tool_activity","data":{"activity":{"id":"tool_ce465abe-ae18-446c-8ee0-c7b404db1a5","name":"edit","status":"running","summary":"edit: …ampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig","detail":"","at":"2026-06-25T01:13:53.312Z"}},"at":1782350033312},{"event":"tool_activity","data":{"activity":{"id":"tool_bf94028a-b312-4692-b9ee-9399ef0893b","name":"edit","status":"completed","summary":"edit: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js","detail":"","at":"2026-06-25T01:13:53.582Z"}},"at":1782350033582},{"event":"file_change","data":{"sessionId":"cs-1782349967058-c8w8dj","change":{"id":"ch-1","path":"Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js\n@@ -4,7 +4,9 @@\n     var confirmModal = null;\n     var confirmModalEl = null;\n     var expenseMode = (window.ALIMPIEZAS_MOBILE && window.ALIMPIEZAS_MOBILE.expenseMode) || 'ocr';\n-    var JS_VERSION = '2026-06-25c';\n+    var JS_VERSION = '2026-06-25d';\n+    var selectedFiles = [];\n+    var fileIdCounter = 0;\n \n     function route(path) {\n         var base = (window.ALIMPIEZAS_MOBILE && window.ALIMPIEZAS_MOBILE.route) || '';\n@@ -118,104 +120,182 @@\n         return html;\n     }\n \n-    function activeFileInput() {\n-        return document.getElementById('file-input-active');\n+    function fileKey(file) {\n+        return [file.name, file.size, file.lastModified || 0, file.type || ''].join('|');\n     }\n \n-    function updateSelectedFileLabel() {\n-        var label = document.getElementById('file-selected-name');\n-        var file = resolvedUploadFile();\n-        if (!label) {\n-            return;\n+    function formatFileSize(bytes) {\n+        if (!bytes || bytes < 1024) {\n+            return Math.max(1, bytes || 0) + ' B';\n         }\n-        if (!file) {\n-            label.textContent = 'Ningún documento seleccionado';\n-            label.className = 'small text-muted';\n-            return;\n+        if (bytes < 1024 * 1024) {\n+            return Math.round(bytes / 1024) + ' KB';\n         }\n+        return (bytes / (1024 * 1024)).toFixed(1) + ' MB';\n+    }\n \n-        var sizeKb = Math.max(1, Math.round(file.size / 1024));\n-        label.textContent = file.name + ' (' + sizeKb + ' KB) — listo para escanear';\n-        label.className = 'small text-success fw-semibold';\n+    function getSelectedFiles() {\n+        return selectedFiles.map(function (item) {\n+            return item.file;\n+        });\n     }\n \n-    function assignSelectedFile(sourceInput) {\n-        var target = activeFileInput();\n-        if (!target || !sourceInput || !sourceInput.files.length) {\n-            return false;\n+    function hasSelectedFiles() {\n+        return selectedFiles.length > 0;\n+    }\n+\n+    function renderSelectedFilesList() {\n+        var list = document.getElementById('file-selected-list');\n+        var emptyHint = document.getElementById('file-empty-hint');\n+        if (!list) {\n+            return;\n         }\n \n-        var file = sourceInput.files[0];\n-        var copied = false;\n+        list.innerHTML = '';\n+        selectedFiles.forEach(function (item) {\n+            var row = document.createElement('div');\n+            row.className = 'file-selected-item';\n+            row.setAttribute('data-file-id', String(item.id));\n \n-        if (typeof DataTransfer !== 'undefined') {\n-            try {\n-                var transfer = new DataTransfer();\n-                transfer.items.add(file);\n-                target.files = transfer.files;\n-                copied = target.files.length > 0;\n-            } catch (err) {\n-                copied = false;\n-            }\n-        }\n+            var info = document.createElement('div');\n+            info.className = 'file-selected-info';\n \n-        if (!copied) {\n-            target._pendingFile = file;\n-        } else {\n-            delete target._pendingFile;\n+            var name = document.createElement('div');\n+            name.className = 'file-name fw-semibold';\n+            name.textContent = item.file.name;\n+\n+            var meta = document.createElement('div');\n+            meta.className = 'file-meta small text-muted';\n+            meta.textContent = formatFileSize(item.file.size) + ' · listo para escanear';\n+\n+            info.appendChild(name);\n+            info.appendChild(meta);\n+\n+            var removeBtn = document.createElement('button');\n+            removeBtn.type = 'button';\n+            removeBtn.className = 'btn btn-sm btn-outline-danger btn-remove-file';\n+            removeBtn.setAttribute('data-file-id', String(item.id));\n+            removeBtn.setAttribute('aria-label', 'Quitar ' + item.file.name);\n+            removeBtn.innerHTML = '<i class=\"fa-solid fa-xmark\"></i>';\n+\n+            row.appendChild(info);\n+            row.appendChild(removeBtn);\n+            list.appendChild(row);\n+        });\n+\n+        if (emptyHint) {\n+            emptyHint.style.display = selectedFiles.length ? 'none' : 'block';\n         }\n \n         document.querySelectorAll('.file-picker-wrap').forEach(function (wrap) {\n-            wrap.classList.remove('is-selected');\n+            wrap.classList.toggle('is-selected', selectedFiles.length > 0);\n         });\n-        var wrap = sourceInput.closest('.file-picker-wrap');\n-        if (wrap) {\n-            wrap.classList.add('is-selected');\n+    }\n+\n+    function addSelectedFile(file) {\n+        if (!file || !file.size) {\n+            return false;\n         }\n \n-        updateSelectedFileLabel();\n+        var key = fileKey(file);\n+        for (var i = 0; i < selectedFiles.length; i++) {\n+            if (selectedFiles[i].key === key) {\n+                return false;\n+            }\n+        }\n+\n+        fileIdCounter += 1;\n+        selectedFiles.push({\n+            id: fileIdCounter,\n+            key: key,\n+            file: file\n+        });\n+        renderSelectedFilesList();\n         return true;\n     }\n \n-    function resolvedUploadFile() {\n-        var fileInput = activeFileInput();\n-        if (!fileInput) {\n-            return null;\n+    function removeSelectedFile(id) {\n+        selectedFiles = selectedFiles.filter(function (item) {\n+            return item.id !== id;\n+        });\n+        renderSelectedFilesList();\n+        debugLog('Documento quitado', 'quedan ' + selectedFiles.length);\n+    }\n+\n+    function clearSelectedFiles() {\n+        selectedFiles = [];\n+        renderSelectedFilesList();\n+        ['file-input-gallery', 'file-input-camera'].forEach(function (inputId) {\n+            var input = document.getElementById(inputId);\n+            if (input) {\n+                input.value = '';\n+            }\n+        });\n+    }\n+\n+    function resetPickerInput(input) {\n+        if (!input) {\n+            return;\n         }\n-        if (fileInput._pendingFile) {\n-            return fileInput._pendingFile;\n+        input.value = '';\n+    }\n+\n+    function addFilesFromInput(sourceInput, sourceLabel) {\n+        if (!sourceInput || !sourceInput.files || !sourceInput.files.length) {\n+            debugLog(sourceLabel || 'Archivo', 'sin selección');\n+            return;\n         }\n-        if (fileInput.files && fileInput.files.length) {\n-            return fileInput.files[0];\n+\n+        var added = 0;\n+        var skipped = 0;\n+        for (var i = 0; i < sourceInput.files.length; i++) {\n+            if (addSelectedFile(sourceInput.files[i])) {\n+                added += 1;\n+            } else {\n+                skipped += 1;\n+            }\n         }\n-        return null;\n+\n+        resetPickerInput(sourceInput);\n+        debugLog(sourceLabel || 'Archivo', added + ' añadido(s), total=' + selectedFiles.length);\n+\n+        if (added === 0 && sourceInput.files.length > 0) {\n+            showResult(formatErrorMessage({\n+                message: skipped > 0 ? 'Ese documento ya está en la lista.' : 'No se pudo leer el archivo seleccionado.',\n+                hint: 'Prueba con JPG, PNG, WEBP, HEIC o PDF.'\n+            }));\n+            return;\n+        }\n+\n+        if (added > 0) {\n+            showResult('');\n+        }\n     }\n \n     function bindFilePickers() {\n         var galleryInput = document.getElementById('file-input-gallery');\n         var cameraInput = document.getElementById('file-input-camera');\n+        var list = document.getElementById('file-selected-list');\n \n+        if (list) {\n+            list.addEventListener('click', function (event) {\n+                var btn = event.target.closest('.btn-remove-file');\n+                if (!btn) {\n+                    return;\n+                }\n+                removeSelectedFile(parseInt(btn.getAttribute('data-file-id'), 10));\n+            });\n+        }\n+\n         if (galleryInput) {\n             galleryInput.addEventListener('change', function () {\n-                debugLog('Galería/archivo', galleryInput.files.length ? galleryInput.files[0].name : 'vacío');\n-                if (!assignSelectedFile(galleryInput)) {\n-                    showResult(formatErrorMessage({\n-                        message: 'No se pudo leer el archivo seleccionado.',\n-                        hint: 'Prueba con otro formato (JPG, PNG o PDF).'\n-                    }));\n-                }\n+                addFilesFromInput(galleryInput, 'Galería/archivo');\n             });\n         }\n \n         if (cameraInput) {\n             cameraInput.addEventListener('change', function () {\n-                debugLog('Foto cámara', cameraInput.files.length ? cameraInput.files[0].name : 'vacío');\n-                if (!assignSelectedFile(cameraInput)) {\n-                    showResult(formatErrorMessage({\n-                        message: 'No se pudo leer la foto.',\n-                        hint: 'Comprueba permisos de cámara en el navegador.'\n-                    }));\n-                }\n+                addFilesFromInput(cameraInput, 'Foto cámara');\n             });\n         }\n     }\n@@ -238,7 +318,7 @@\n                 manualBtn.style.display = 'block';\n             }\n             if (fileLabel) {\n-                fileLabel.textContent = '(opcional en manual)';\n+                fileLabel.textContent = '(opcional — puedes añadir varios)';\n             }\n             if (pickFileWrap) {\n                 pickFileWrap.style.display = 'block';\n@@ -363,6 +443,113 @@\n         });\n     }\n \n+    function buildUploadFormData(file, docType) {\n+        var formData = new FormData();\n+        formData.append('action', 'upload');\n+        formData.append('doc_type', docType);\n+        formData.append('multireqtoken', tokenValue());\n+        formData.append('image', file, file.name || 'documento.jpg');\n+        return formData;\n+    }\n+\n+    function uploadSingleFile(file, docType) {\n+        debugLog('Enviando', file.name + ' (' + file.size + ' B)');\n+        return postForm(buildUploadFormData(file, docType)).then(function (data) {\n+            if (data.debug) {\n+                debugLog('Respuesta servidor', data.debug);\n+            }\n+            return data;\n+        });\n+    }\n+\n+    function uploadFilesSequentially(files, docType, btn) {\n+        var index = 0;\n+        var okCount = 0;\n+        var lastExtracted = null;\n+        var errors = [];\n+\n+        function processNext() {\n+            if (index >= files.length) {\n+                var html = '';\n+                if (okCount === files.length) {\n+                    html = '<div class=\"alert alert-success mb-0\">' + okCount + ' documento(s) analizado(s). Revisa la cola.</div>';\n+                } else if (okCount > 0) {\n+                    html = '<div class=\"alert alert-warning mb-0\"><strong>' + okCount + ' de ' + files.length + ' documentos analizados.</strong>';\n+                    if (errors.length) {\n+                        html += '<ul class=\"mb-0 mt-2 small\">';\n+                        errors.forEach(function (err) {\n+                            html += '<li>' + err.name + ': ' + err.message + '</li>';\n+                        });\n+                        html += '</ul>';\n+                    }\n+                    html += '</div>';\n+                } else {\n+                    html = formatErrorMessage({\n+                        message: 'No se pudo analizar ningún documento.',\n+                        reasons: errors.map(function (err) {\n+                            return err.name + ': ' + err.message;\n+                        })\n+                    });\n+                }\n+                showResult(html);\n+\n+                if (okCount > 0) {\n+                    clearSelectedFiles();\n+                }\n+\n+                if (files.length === 1 && lastExtracted) {\n+                    openConfirmModal(lastExtracted);\n+                } else if (okCount > 0) {\n+                    setTimeout(function () {\n+                        window.location.reload();\n+                    }, 1800);\n+                }\n+                return Promise.resolve();\n+            }\n+\n+            var file = files[index];\n+            var current = index + 1;\n+            index += 1;\n+            if (btn) {\n+                btn.innerHTML = '<span class=\"spinner-border spinner-border-sm me-2\"></span>Procesando ' + current + '/' + files.length + '…';\n+            }\n+\n+            return uploadSingleFile(file, docType).then(function (data) {\n+                if (!data.ok) {\n+                    debugLog('Error upload', file.name + ': ' + (data.message || 'error'));\n+                    errors.push({ name: file.name, message: data.message || 'Error' });\n+                    if (files.length === 1) {\n+                        openDebugPanel();\n+                        showResult(formatErrorMessage(data));\n+                        return Promise.resolve();\n+                    }\n+                    return processNext();\n+                }\n+\n+                okCount += 1;\n+                debugLog('Upload OK', file.name);\n+                if (data.extracted) {\n+                    lastExtracted = data.extracted;\n+                }\n+                return processNext();\n+            }).catch(function (err) {\n+                debugLog('Fetch error', file.name + ': ' + (err && err.message ? err.message : 'desconocido'));\n+                errors.push({ name: file.name, message: err && err.message ? err.message : 'Error de red' });\n+                if (files.length === 1) {\n+                    openDebugPanel();\n+                    showResult(formatErrorMessage({\n+                        message: 'Error de conexión con el servidor.',\n+                        hint: err && err.message ? err.message : ''\n+                    }));\n+                    return Promise.resolve();\n+                }\n+                return processNext();\n+            });\n+        }\n+\n+        return processNext();\n+    }\n+\n     function bindManualButton() {\n         var btn = document.getElementById('manual-btn');\n         if (!btn) {\n@@ -401,17 +588,15 @@\n         form.addEventListener('submit', function (event) {\n             event.preventDefault();\n             var btn = document.getElementById('upload-btn');\n-            var uploadFile = resolvedUploadFile();\n-            var formData = new FormData(form);\n-            formData.append('action', 'upload');\n-            if (uploadFile) {\n-                formData.set('image', uploadFile, uploadFile.name || 'documento.jpg');\n-            }\n+            var files = getSelectedFiles();\n+            var docType = document.getElementById('doc-type').value;\n \n-            if (expenseMode === 'manual' && !uploadFile) {\n+            debugLog('Submit', 'modo=' + expenseMode + ' · docs=' + files.length);\n+\n+            if (expenseMode === 'manual' && files.length === 0) {\n                 var manualData = new FormData();\n                 manualData.append('action', 'manual');\n-                manualData.append('doc_type', document.getElementById('doc-type').value);\n+                manualData.append('doc_type', docType);\n                 postForm(manualData).then(function (data) {\n                     if (data.ok && data.extracted) {\n                         openConfirmModal(data.extracted);\n@@ -422,10 +607,14 @@\n                 return;\n             }\n \n-            if (expenseMode !== 'manual' && !uploadFile) {\n+            if (expenseMode !== 'manual' && files.length === 0) {\n                 showResult(formatErrorMessage({\n                     message: 'Debes elegir un archivo, una foto de galería o hacer una foto.',\n-                    reasons: ['En modo OCR/IA el documento es obligatorio.', 'Usa «Elegir archivo o galería» o «Hacer foto».'],\n+                    reasons: [\n+                        'En modo OCR/IA el documento es obligatorio.',\n+                        'Usa «Elegir archivo o galería» o «Hacer foto».',\n+                        'Puedes añadir varios documentos antes de pulsar escanear.'\n+                    ],\n                     hint: 'Formatos admitidos: JPG, PNG, WEBP, HEIC y PDF.'\n                 }));\n                 return;\n@@ -433,44 +622,11 @@\n \n             btn.disabled = true;\n             btn.innerHTML = '<span class=\"spinner-border spinner-border-sm me-2\"></span>Procesando...';\n-            debugLog('Enviando documento', uploadFile ? uploadFile.name + ' (' + uploadFile.size + ' B)' : 'sin archivo');\n \n-            postForm(formData)\n-                .then(function (data) {\n-                    if (data.debug) {\n-                        debugLog('Respuesta servidor', data.debug);\n-                    }\n-                    if (!data.ok) {\n-                        debugLog('Error upload', data.message);\n-                        openDebugPanel();\n-                        showResult(formatErrorMessage(data));\n-                        return;\n-                    }\n-                    debugLog('Upload OK', data.message || 'analizado');\n-\n-                    if (data.warning) {\n-                        showResult('<div class=\"alert alert-warning\">' + data.warning + '</div>');\n-                    } else if (data.message) {\n-                        showResult('<div class=\"alert alert-success\">' + data.message + '</div>');\n-                    }\n-\n-                    if (data.extracted) {\n-                        openConfirmModal(data.extracted);\n-                    }\n-                })\n-                .catch(function (err) {\n-                    debugLog('Fetch error', err && err.message ? err.message : 'desconocido');\n-                    openDebugPanel();\n-                    showResult(formatErrorMessage({\n-                        message: 'Error de conexión con el servidor.',\n-                        reasons: ['Comprueba que Apache (XAMPP) esté activo.'],\n-                        hint: err && err.message ? err.message : ''\n-                    }));\n-                })\n-                .finally(function () {\n-                    btn.disabled = false;\n-                    updateModeUi(expenseMode);\n-                });\n+            uploadFilesSequentially(files, docType, btn).finally(function () {\n+                btn.disabled = false;\n+                updateModeUi(expenseMode);\n+            });\n         });\n     }\n \n@@ -527,6 +683,7 @@\n         bindUploadForm();\n         bindReviewButtons();\n         bindConfirmActions();\n+        renderSelectedFilesList();\n         debugLog('Página lista', 'modo=' + expenseMode + ' · v=' + JS_VERSION);\n         runDiagnose();\n     });","diffHtml":"<div class=\"diff-colored\"><div class=\"diff-line diff-meta\">--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js</div><div class=\"diff-line diff-meta\">+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js</div><div class=\"diff-line diff-hunk\">@@ -4,7 +4,9 @@</div><div class=\"diff-line diff-ctx\">     var confirmModal = null;</div><div class=\"diff-line diff-ctx\">     var confirmModalEl = null;</div><div class=\"diff-line diff-ctx\">     var expenseMode = (window.ALIMPIEZAS_MOBILE &amp;&amp; window.ALIMPIEZAS_MOBILE.expenseMode) || 'ocr';</div><div class=\"diff-line diff-del\">-    var JS_VERSION = '2026-06-25c';</div><div class=\"diff-line diff-add\">+    var JS_VERSION = '2026-06-25d';</div><div class=\"diff-line diff-add\">+    var selectedFiles = [];</div><div class=\"diff-line diff-add\">+    var fileIdCounter = 0;</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-ctx\">     function route(path) {</div><div class=\"diff-line diff-ctx\">         var base = (window.ALIMPIEZAS_MOBILE &amp;&amp; window.ALIMPIEZAS_MOBILE.route) || '';</div><div class=\"diff-line diff-hunk\">@@ -118,104 +120,182 @@</div><div class=\"diff-line diff-ctx\">         return html;</div><div class=\"diff-line diff-ctx\">     }</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-del\">-    function activeFileInput() {</div><div class=\"diff-line diff-del\">-        return document.getElementById('file-input-active');</div><div class=\"diff-line diff-add\">+    function fileKey(file) {</div><div class=\"diff-line diff-add\">+        return [file.name, file.size, file.lastModified || 0, file.type || ''].join('|');</div><div class=\"diff-line diff-ctx\">     }</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-del\">-    function updateSelectedFileLabel() {</div><div class=\"diff-line diff-del\">-        var label = document.getElementById('file-selected-name');</div><div class=\"diff-line diff-del\">-        var file = resolvedUploadFile();</div><div class=\"diff-line diff-del\">-        if (!label) {</div><div class=\"diff-line diff-del\">-            return;</div><div class=\"diff-line diff-add\">+    function formatFileSize(bytes) {</div><div class=\"diff-line diff-add\">+        if (!bytes || bytes &lt; 1024) {</div><div class=\"diff-line diff-add\">+            return Math.max(1, bytes || 0) + ' B';</div><div class=\"diff-line diff-ctx\">         }</div><div class=\"diff-line diff-del\">-        if (!file) {</div><div class=\"diff-line diff-del\">-            label.textContent = 'Ningún documento seleccionado';</div><div class=\"diff-line diff-del\">-            label.className = 'small text-muted';</div><div class=\"diff-line diff-del\">-            return;</div><div class=\"diff-line diff-add\">+        if (bytes &lt; 1024 * 1024) {</div><div class=\"diff-line diff-add\">+            return Math.round(bytes / 1024) + ' KB';</div><div class=\"diff-line diff-ctx\">         }</div><div class=\"diff-line diff-add\">+        return (bytes / (1024 * 1024)).toFixed(1) + ' MB';</div><div class=\"diff-line diff-add\">+    }</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-del\">-        var sizeKb = Math.max(1, Math.round(file.size / 1024));</div><div class=\"diff-line diff-del\">-        label.textContent = file.name + ' (' + sizeKb + ' KB) — listo para escanear';</div><div class=\"diff-line diff-del\">-        label.className = 'small text-success fw-semibold';</div><div class=\"diff-line diff-add\">+    function getSelectedFiles() {</div><div class=\"diff-line diff-add\">+        return selectedFiles.map(function (item) {</div><div class=\"diff-line diff-add\">+            return item.file;</div><div class=\"diff-line diff-add\">+        });</div><div class=\"diff-line diff-ctx\">     }</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-del\">-    function assignSelectedFile(sourceInput) {</div><div class=\"diff-line diff-del\">-        var target = activeFileInput();</div><div class=\"diff-line diff-del\">-        if (!target || !sourceInput || !sourceInput.files.length) {</div><div class=\"diff-line diff-del\">-            return false;</div><div class=\"diff-line diff-add\">+    function hasSelectedFiles() {</div><div class=\"diff-line diff-add\">+        return selectedFiles.length &gt; 0;</div><div class=\"diff-line diff-add\">+    }</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+    function renderSelectedFilesList() {</div><div class=\"diff-line diff-add\">+        var list = document.getElementById('file-selected-list');</div><div class=\"diff-line diff-add\">+        var emptyHint = document.getElementById('file-empty-hint');</div><div class=\"diff-line diff-add\">+        if (!list) {</div><div class=\"diff-line diff-add\">+            return;</div><div class=\"diff-line diff-ctx\">         }</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-del\">-        var file = sourceInput.files[0];</div><div class=\"diff-line diff-del\">-        var copied = false;</div><div class=\"diff-line diff-add\">+        list.innerHTML = '';</div><div class=\"diff-line diff-add\">+        selectedFiles.forEach(function (item) {</div><div class=\"diff-line diff-add\">+            var row = document.createElement('div');</div><div class=\"diff-line diff-add\">+            row.className = 'file-selected-item';</div><div class=\"diff-line diff-add\">+            row.setAttribute('data-file-id', String(item.id));</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-del\">-        if (typeof DataTransfer !== 'undefined') {</div><div class=\"diff-line diff-del\">-            try {</div><div class=\"diff-line diff-del\">-                var transfer = new DataTransfer();</div><div class=\"diff-line diff-del\">-                transfer.items.add(file);</div><div class=\"diff-line diff-del\">-                target.files = transfer.files;</div><div class=\"diff-line diff-del\">-                copied = target.files.length &gt; 0;</div><div class=\"diff-line diff-del\">-            } catch (err) {</div><div class=\"diff-line diff-del\">-                copied = false;</div><div class=\"diff-line diff-del\">-            }</div><div class=\"diff-line diff-del\">-        }</div><div class=\"diff-line diff-add\">+            var info = document.createElement('div');</div><div class=\"diff-line diff-add\">+            info.className = 'file-selected-info';</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-del\">-        if (!copied) {</div><div class=\"diff-line diff-del\">-            target._pendingFile = file;</div><div class=\"diff-line diff-del\">-        } else {</div><div class=\"diff-line diff-del\">-            delete target._pendingFile;</div><div class=\"diff-line diff-add\">+            var name = document.createElement('div');</div><div class=\"diff-line diff-add\">+            name.className = 'file-name fw-semibold';</div><div class=\"diff-line diff-add\">+            name.textContent = item.file.name;</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+            var meta = document.createElement('div');</div><div class=\"diff-line diff-add\">+            meta.className = 'file-meta small text-muted';</div><div class=\"diff-line diff-add\">+            meta.textContent = formatFileSize(item.file.size) + ' · listo para escanear';</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+            info.appendChild(name);</div><div class=\"diff-line diff-add\">+            info.appendChild(meta);</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+            var removeBtn = document.createElement('button');</div><div class=\"diff-line diff-add\">+            removeBtn.type = 'button';</div><div class=\"diff-line diff-add\">+            removeBtn.className = 'btn btn-sm btn-outline-danger btn-remove-file';</div><div class=\"diff-line diff-add\">+            removeBtn.setAttribute('data-file-id', String(item.id));</div><div class=\"diff-line diff-add\">+            removeBtn.setAttribute('aria-label', 'Quitar ' + item.file.name);</div><div class=\"diff-line diff-add\">+            removeBtn.innerHTML = '&lt;i class=\"fa-solid fa-xmark\"&gt;&lt;/i&gt;';</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+            row.appendChild(info);</div><div class=\"diff-line diff-add\">+            row.appendChild(removeBtn);</div><div class=\"diff-line diff-add\">+            list.appendChild(row);</div><div class=\"diff-line diff-add\">+        });</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+        if (emptyHint) {</div><div class=\"diff-line diff-add\">+            emptyHint.style.display = selectedFiles.length ? 'none' : 'block';</div><div class=\"diff-line diff-ctx\">         }</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-ctx\">         document.querySelectorAll('.file-picker-wrap').forEach(function (wrap) {</div><div class=\"diff-line diff-del\">-            wrap.classList.remove('is-selected');</div><div class=\"diff-line diff-add\">+            wrap.classList.toggle('is-selected', selectedFiles.length &gt; 0);</div><div class=\"diff-line diff-ctx\">         });</div><div class=\"diff-line diff-del\">-        var wrap = sourceInput.closest('.file-picker-wrap');</div><div class=\"diff-line diff-del\">-        if (wrap) {</div><div class=\"diff-line diff-del\">-            wrap.classList.add('is-selected');</div><div class=\"diff-line diff-add\">+    }</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+    function addSelectedFile(file) {</div><div class=\"diff-line diff-add\">+        if (!file || !file.size) {</div><div class=\"diff-line diff-add\">+            return false;</div><div class=\"diff-line diff-ctx\">         }</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-del\">-        updateSelectedFileLabel();</div><div class=\"diff-line diff-add\">+        var key = fileKey(file);</div><div class=\"diff-line diff-add\">+        for (var i = 0; i &lt; selectedFiles.length; i++) {</div><div class=\"diff-line diff-add\">+            if (selectedFiles[i].key === key) {</div><div class=\"diff-line diff-add\">+                return false;</div><div class=\"diff-line diff-add\">+            }</div><div class=\"diff-line diff-add\">+        }</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+        fileIdCounter += 1;</div><div class=\"diff-line diff-add\">+        selectedFiles.push({</div><div class=\"diff-line diff-add\">+            id: fileIdCounter,</div><div class=\"diff-line diff-add\">+            key: key,</div><div class=\"diff-line diff-add\">+            file: file</div><div class=\"diff-line diff-add\">+        });</div><div class=\"diff-line diff-add\">+        renderSelectedFilesList();</div><div class=\"diff-line diff-ctx\">         return true;</div><div class=\"diff-line diff-ctx\">     }</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-del\">-    function resolvedUploadFile() {</div><div class=\"diff-line diff-del\">-        var fileInput = activeFileInput();</div><div class=\"diff-line diff-del\">-        if (!fileInput) {</div><div class=\"diff-line diff-del\">-            return null;</div><div class=\"diff-line diff-add\">+    function removeSelectedFile(id) {</div><div class=\"diff-line diff-add\">+        selectedFiles = selectedFiles.filter(function (item) {</div><div class=\"diff-line diff-add\">+            return item.id !== id;</div><div class=\"diff-line diff-add\">+        });</div><div class=\"diff-line diff-add\">+        renderSelectedFilesList();</div><div class=\"diff-line diff-add\">+        debugLog('Documento quitado', 'quedan ' + selectedFiles.length);</div><div class=\"diff-line diff-add\">+    }</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+    function clearSelectedFiles() {</div><div class=\"diff-line diff-add\">+        selectedFiles = [];</div><div class=\"diff-line diff-add\">+        renderSelectedFilesList();</div><div class=\"diff-line diff-add\">+        ['file-input-gallery', 'file-input-camera'].forEach(function (inputId) {</div><div class=\"diff-line diff-add\">+            var input = document.getElementById(inputId);</div><div class=\"diff-line diff-add\">+            if (input) {</div><div class=\"diff-line diff-add\">+                input.value = '';</div><div class=\"diff-line diff-add\">+            }</div><div class=\"diff-line diff-add\">+        });</div><div class=\"diff-line diff-add\">+    }</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+    function resetPickerInput(input) {</div><div class=\"diff-line diff-add\">+        if (!input) {</div><div class=\"diff-line diff-add\">+            return;</div><div class=\"diff-line diff-ctx\">         }</div><div class=\"diff-line diff-del\">-        if (fileInput._pendingFile) {</div><div class=\"diff-line diff-del\">-            return fileInput._pendingFile;</div><div class=\"diff-line diff-add\">+        input.value = '';</div><div class=\"diff-line diff-add\">+    }</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+    function addFilesFromInput(sourceInput, sourceLabel) {</div><div class=\"diff-line diff-add\">+        if (!sourceInput || !sourceInput.files || !sourceInput.files.length) {</div><div class=\"diff-line diff-add\">+            debugLog(sourceLabel || 'Archivo', 'sin selección');</div><div class=\"diff-line diff-add\">+            return;</div><div class=\"diff-line diff-ctx\">         }</div><div class=\"diff-line diff-del\">-        if (fileInput.files &amp;&amp; fileInput.files.length) {</div><div class=\"diff-line diff-del\">-            return fileInput.files[0];</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+        var added = 0;</div><div class=\"diff-line diff-add\">+        var skipped = 0;</div><div class=\"diff-line diff-add\">+        for (var i = 0; i &lt; sourceInput.files.length; i++) {</div><div class=\"diff-line diff-add\">+            if (addSelectedFile(sourceInput.files[i])) {</div><div class=\"diff-line diff-add\">+                added += 1;</div><div class=\"diff-line diff-add\">+            } else {</div><div class=\"diff-line diff-add\">+                skipped += 1;</div><div class=\"diff-line diff-add\">+            }</div><div class=\"diff-line diff-ctx\">         }</div><div class=\"diff-line diff-del\">-        return null;</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+        resetPickerInput(sourceInput);</div><div class=\"diff-line diff-add\">+        debugLog(sourceLabel || 'Archivo', added + ' añadido(s), total=' + selectedFiles.length);</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+        if (added === 0 &amp;&amp; sourceInput.files.length &gt; 0) {</div><div class=\"diff-line diff-add\">+            showResult(formatErrorMessage({</div><div class=\"diff-line diff-add\">+                message: skipped &gt; 0 ? 'Ese documento ya está en la lista.' : 'No se pudo leer el archivo seleccionado.',</div><div class=\"diff-line diff-add\">+                hint: 'Prueba con JPG, PNG, WEBP, HEIC o PDF.'</div><div class=\"diff-line diff-add\">+            }));</div><div class=\"diff-line diff-add\">+            return;</div><div class=\"diff-line diff-add\">+        }</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+        if (added &gt; 0) {</div><div class=\"diff-line diff-add\">+            showResult('');</div><div class=\"diff-line diff-add\">+        }</div><div class=\"diff-line diff-ctx\">     }</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-ctx\">     function bindFilePickers() {</div><div class=\"diff-line diff-ctx\">         var galleryInput = document.getElementById('file-input-gallery');</div><div class=\"diff-line diff-ctx\">         var cameraInput = document.getElementById('file-input-camera');</div><div class=\"diff-line diff-add\">+        var list = document.getElementById('file-selected-list');</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-add\">+        if (list) {</div><div class=\"diff-line diff-add\">+            list.addEventListener('click', function (event) {</div><div class=\"diff-line diff-add\">+                var btn = event.target.closest('.btn-remove-file');</div><div class=\"diff-line diff-add\">+                if (!btn) {</div><div class=\"diff-line diff-add\">+                    return;</div><div class=\"diff-line diff-add\">+                }</div><div class=\"diff-line diff-add\">+                removeSelectedFile(parseInt(btn.getAttribute('data-file-id'), 10));</div><div class=\"diff-line diff-add\">+            });</div><div class=\"diff-line diff-add\">+        }</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-ctx\">         if (galleryInput) {</div><div class=\"diff-line diff-ctx\">             galleryInput.addEventListener('change', function () {</div><div class=\"diff-line diff-del\">-                debugLog('Galería/archivo', galleryInput.files.length ? galleryInput.files[0].name : 'vacío');</div><div class=\"diff-line diff-del\">-                if (!assignSelectedFile(galleryInput)) {</div><div class=\"diff-line diff-del\">-                    showResult(formatErrorMessage({</div><div class=\"diff-line diff-del\">-                        message: 'No se pudo leer el archivo seleccionado.',</div><div class=\"diff-line diff-del\">-                        hint: 'Prueba con otro formato (JPG, PNG o PDF).'</div><div class=\"diff-line diff-del\">-                    }));</div><div class=\"diff-line diff-del\">-                }</div><div class=\"diff-line diff-add\">+                addFilesFromInput(galleryInput, 'Galería/archivo');</div><div class=\"diff-line diff-ctx\">             });</div><div class=\"diff-line diff-ctx\">         }</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-ctx\">         if (cameraInput) {</div><div class=\"diff-line diff-ctx\">             cameraInput.addEventListener('change', function () {</div><div class=\"diff-line diff-del\">-                debugLog('Foto cámara', cameraInput.files.length ? cameraInput.files[0].name : 'vacío');</div><div class=\"diff-line diff-del\">-                if (!assignSelectedFile(cameraInput)) {</div><div class=\"diff-line diff-del\">-                    showResult(formatErrorMessage({</div><div class=\"diff-line diff-del\">-                        message: 'No se pudo leer la foto.',</div><div class=\"diff-line diff-del\">-                        hint: 'Comprueba permisos de cámara en el navegador.'</div><div class=\"diff-line diff-del\">-                    }));</div><div class=\"diff-line diff-del\">-                }</div><div class=\"diff-line diff-add\">+                addFilesFromInput(cameraInput, 'Foto cámara');</div><div class=\"diff-line diff-ctx\">             });</div><div class=\"diff-line diff-ctx\">         }</div><div class=\"diff-line diff-ctx\">     }</div><div class=\"diff-line diff-hunk\">@@ -238,7 +318,7 @@</div><div class=\"diff-line diff-ctx\">                 manualBtn.style.display = 'block';</div><div class=\"diff-line diff-ctx\">             }</div><div class=\"diff-line diff-ctx\">             if (fileLabel) {</div><div class=\"diff-line diff-del\">-                fileLabel.textContent = '(opcional en manual)';</div><div class=\"diff-line diff-add\">+                fileLabel.textContent = '(opcional — puedes añadir varios)';</div><div class=\"diff-line diff-ctx\">             }</div><div class=\"diff-line diff-ctx\">             if (pickFileWrap) {</div><div class=\"diff-line diff-ctx\">                 pickFileWrap.style.display = 'block';</div><div class=\"diff-line diff-hunk\">@@ -363,6 +443,113 @@</div><div class=\"diff-line diff-ctx\">         });</div><div class=\"diff-line diff-ctx\">     }</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-add\">+    function buildUploadFormData(file, docType) {</div><div class=\"diff-line diff-add\">+        var formData = new FormData();</div><div class=\"diff-line diff-add\">+        formData.append('action', 'upload');</div><div class=\"diff-line diff-add\">+        formData.append('doc_type', docType);</div><div class=\"diff-line diff-add\">+        formData.append('multireqtoken', tokenValue());</div><div class=\"diff-line diff-add\">+        formData.append('image', file, file.name || 'documento.jpg');</div><div class=\"diff-line diff-add\">+        return formData;</div><div class=\"diff-line diff-add\">+    }</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+    function uploadSingleFile(file, docType) {</div><div class=\"diff-line diff-add\">+        debugLog('Enviando', file.name + ' (' + file.size + ' B)');</div><div class=\"diff-line diff-add\">+        return postForm(buildUploadFormData(file, docType)).then(function (data) {</div><div class=\"diff-line diff-add\">+            if (data.debug) {</div><div class=\"diff-line diff-add\">+                debugLog('Respuesta servidor', data.debug);</div><div class=\"diff-line diff-add\">+            }</div><div class=\"diff-line diff-add\">+            return data;</div><div class=\"diff-line diff-add\">+        });</div><div class=\"diff-line diff-add\">+    }</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+    function uploadFilesSequentially(files, docType, btn) {</div><div class=\"diff-line diff-add\">+        var index = 0;</div><div class=\"diff-line diff-add\">+        var okCount = 0;</div><div class=\"diff-line diff-add\">+        var lastExtracted = null;</div><div class=\"diff-line diff-add\">+        var errors = [];</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+        function processNext() {</div><div class=\"diff-line diff-add\">+            if (index &gt;= files.length) {</div><div class=\"diff-line diff-add\">+                var html = '';</div><div class=\"diff-line diff-add\">+                if (okCount === files.length) {</div><div class=\"diff-line diff-add\">+                    html = '&lt;div class=\"alert alert-success mb-0\"&gt;' + okCount + ' documento(s) analizado(s). Revisa la cola.&lt;/div&gt;';</div><div class=\"diff-line diff-add\">+                } else if (okCount &gt; 0) {</div><div class=\"diff-line diff-add\">+                    html = '&lt;div class=\"alert alert-warning mb-0\"&gt;&lt;strong&gt;' + okCount + ' de ' + files.length + ' documentos analizados.&lt;/strong&gt;';</div><div class=\"diff-line diff-add\">+                    if (errors.length) {</div><div class=\"diff-line diff-add\">+                        html += '&lt;ul class=\"mb-0 mt-2 small\"&gt;';</div><div class=\"diff-line diff-add\">+                        errors.forEach(function (err) {</div><div class=\"diff-line diff-add\">+                            html += '&lt;li&gt;' + err.name + ': ' + err.message + '&lt;/li&gt;';</div><div class=\"diff-line diff-add\">+                        });</div><div class=\"diff-line diff-add\">+                        html += '&lt;/ul&gt;';</div><div class=\"diff-line diff-add\">+                    }</div><div class=\"diff-line diff-add\">+                    html += '&lt;/div&gt;';</div><div class=\"diff-line diff-add\">+                } else {</div><div class=\"diff-line diff-add\">+                    html = formatErrorMessage({</div><div class=\"diff-line diff-add\">+                        message: 'No se pudo analizar ningún documento.',</div><div class=\"diff-line diff-add\">+                        reasons: errors.map(function (err) {</div><div class=\"diff-line diff-add\">+                            return err.name + ': ' + err.message;</div><div class=\"diff-line diff-add\">+                        })</div><div class=\"diff-line diff-add\">+                    });</div><div class=\"diff-line diff-add\">+                }</div><div class=\"diff-line diff-add\">+                showResult(html);</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+                if (okCount &gt; 0) {</div><div class=\"diff-line diff-add\">+                    clearSelectedFiles();</div><div class=\"diff-line diff-add\">+                }</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+                if (files.length === 1 &amp;&amp; lastExtracted) {</div><div class=\"diff-line diff-add\">+                    openConfirmModal(lastExtracted);</div><div class=\"diff-line diff-add\">+                } else if (okCount &gt; 0) {</div><div class=\"diff-line diff-add\">+                    setTimeout(function () {</div><div class=\"diff-line diff-add\">+                        window.location.reload();</div><div class=\"diff-line diff-add\">+                    }, 1800);</div><div class=\"diff-line diff-add\">+                }</div><div class=\"diff-line diff-add\">+                return Promise.resolve();</div><div class=\"diff-line diff-add\">+            }</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+            var file = files[index];</div><div class=\"diff-line diff-add\">+            var current = index + 1;</div><div class=\"diff-line diff-add\">+            index += 1;</div><div class=\"diff-line diff-add\">+            if (btn) {</div><div class=\"diff-line diff-add\">+                btn.innerHTML = '&lt;span class=\"spinner-border spinner-border-sm me-2\"&gt;&lt;/span&gt;Procesando ' + current + '/' + files.length + '…';</div><div class=\"diff-line diff-add\">+            }</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+            return uploadSingleFile(file, docType).then(function (data) {</div><div class=\"diff-line diff-add\">+                if (!data.ok) {</div><div class=\"diff-line diff-add\">+                    debugLog('Error upload', file.name + ': ' + (data.message || 'error'));</div><div class=\"diff-line diff-add\">+                    errors.push({ name: file.name, message: data.message || 'Error' });</div><div class=\"diff-line diff-add\">+                    if (files.length === 1) {</div><div class=\"diff-line diff-add\">+                        openDebugPanel();</div><div class=\"diff-line diff-add\">+                        showResult(formatErrorMessage(data));</div><div class=\"diff-line diff-add\">+                        return Promise.resolve();</div><div class=\"diff-line diff-add\">+                    }</div><div class=\"diff-line diff-add\">+                    return processNext();</div><div class=\"diff-line diff-add\">+                }</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+                okCount += 1;</div><div class=\"diff-line diff-add\">+                debugLog('Upload OK', file.name);</div><div class=\"diff-line diff-add\">+                if (data.extracted) {</div><div class=\"diff-line diff-add\">+                    lastExtracted = data.extracted;</div><div class=\"diff-line diff-add\">+                }</div><div class=\"diff-line diff-add\">+                return processNext();</div><div class=\"diff-line diff-add\">+            }).catch(function (err) {</div><div class=\"diff-line diff-add\">+                debugLog('Fetch error', file.name + ': ' + (err &amp;&amp; err.message ? err.message : 'desconocido'));</div><div class=\"diff-line diff-add\">+                errors.push({ name: file.name, message: err &amp;&amp; err.message ? err.message : 'Error de red' });</div><div class=\"diff-line diff-add\">+                if (files.length === 1) {</div><div class=\"diff-line diff-add\">+                    openDebugPanel();</div><div class=\"diff-line diff-add\">+                    showResult(formatErrorMessage({</div><div class=\"diff-line diff-add\">+                        message: 'Error de conexión con el servidor.',</div><div class=\"diff-line diff-add\">+                        hint: err &amp;&amp; err.message ? err.message : ''</div><div class=\"diff-line diff-add\">+                    }));</div><div class=\"diff-line diff-add\">+                    return Promise.resolve();</div><div class=\"diff-line diff-add\">+                }</div><div class=\"diff-line diff-add\">+                return processNext();</div><div class=\"diff-line diff-add\">+            });</div><div class=\"diff-line diff-add\">+        }</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+        return processNext();</div><div class=\"diff-line diff-add\">+    }</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-ctx\">     function bindManualButton() {</div><div class=\"diff-line diff-ctx\">         var btn = document.getElementById('manual-btn');</div><div class=\"diff-line diff-ctx\">         if (!btn) {</div><div class=\"diff-line diff-hunk\">@@ -401,17 +588,15 @@</div><div class=\"diff-line diff-ctx\">         form.addEventListener('submit', function (event) {</div><div class=\"diff-line diff-ctx\">             event.preventDefault();</div><div class=\"diff-line diff-ctx\">             var btn = document.getElementById('upload-btn');</div><div class=\"diff-line diff-del\">-            var uploadFile = resolvedUploadFile();</div><div class=\"diff-line diff-del\">-            var formData = new FormData(form);</div><div class=\"diff-line diff-del\">-            formData.append('action', 'upload');</div><div class=\"diff-line diff-del\">-            if (uploadFile) {</div><div class=\"diff-line diff-del\">-                formData.set('image', uploadFile, uploadFile.name || 'documento.jpg');</div><div class=\"diff-line diff-del\">-            }</div><div class=\"diff-line diff-add\">+            var files = getSelectedFiles();</div><div class=\"diff-line diff-add\">+            var docType = document.getElementById('doc-type').value;</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-del\">-            if (expenseMode === 'manual' &amp;&amp; !uploadFile) {</div><div class=\"diff-line diff-add\">+            debugLog('Submit', 'modo=' + expenseMode + ' · docs=' + files.length);</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+            if (expenseMode === 'manual' &amp;&amp; files.length === 0) {</div><div class=\"diff-line diff-ctx\">                 var manualData = new FormData();</div><div class=\"diff-line diff-ctx\">                 manualData.append('action', 'manual');</div><div class=\"diff-line diff-del\">-                manualData.append('doc_type', document.getElementById('doc-type').value);</div><div class=\"diff-line diff-add\">+                manualData.append('doc_type', docType);</div><div class=\"diff-line diff-ctx\">                 postForm(manualData).then(function (data) {</div><div class=\"diff-line diff-ctx\">                     if (data.ok &amp;&amp; data.extracted) {</div><div class=\"diff-line diff-ctx\">                         openConfirmModal(data.extracted);</div><div class=\"diff-line diff-hunk\">@@ -422,10 +607,14 @@</div><div class=\"diff-line diff-ctx\">                 return;</div><div class=\"diff-line diff-ctx\">             }</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-del\">-            if (expenseMode !== 'manual' &amp;&amp; !uploadFile) {</div><div class=\"diff-line diff-add\">+            if (expenseMode !== 'manual' &amp;&amp; files.length === 0) {</div><div class=\"diff-line diff-ctx\">                 showResult(formatErrorMessage({</div><div class=\"diff-line diff-ctx\">                     message: 'Debes elegir un archivo, una foto de galería o hacer una foto.',</div><div class=\"diff-line diff-del\">-                    reasons: ['En modo OCR/IA el documento es obligatorio.', 'Usa «Elegir archivo o galería» o «Hacer foto».'],</div><div class=\"diff-line diff-add\">+                    reasons: [</div><div class=\"diff-line diff-add\">+                        'En modo OCR/IA el documento es obligatorio.',</div><div class=\"diff-line diff-add\">+                        'Usa «Elegir archivo o galería» o «Hacer foto».',</div><div class=\"diff-line diff-add\">+                        'Puedes añadir varios documentos antes de pulsar escanear.'</div><div class=\"diff-line diff-add\">+                    ],</div><div class=\"diff-line diff-ctx\">                     hint: 'Formatos admitidos: JPG, PNG, WEBP, HEIC y PDF.'</div><div class=\"diff-line diff-ctx\">                 }));</div><div class=\"diff-line diff-ctx\">                 return;</div><div class=\"diff-line diff-hunk\">@@ -433,44 +622,11 @@</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-ctx\">             btn.disabled = true;</div><div class=\"diff-line diff-ctx\">             btn.innerHTML = '&lt;span class=\"spinner-border spinner-border-sm me-2\"&gt;&lt;/span&gt;Procesando...';</div><div class=\"diff-line diff-del\">-            debugLog('Enviando documento', uploadFile ? uploadFile.name + ' (' + uploadFile.size + ' B)' : 'sin archivo');</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-del\">-            postForm(formData)</div><div class=\"diff-line diff-del\">-                .then(function (data) {</div><div class=\"diff-line diff-del\">-                    if (data.debug) {</div><div class=\"diff-line diff-del\">-                        debugLog('Respuesta servidor', data.debug);</div><div class=\"diff-line diff-del\">-                    }</div><div class=\"diff-line diff-del\">-                    if (!data.ok) {</div><div class=\"diff-line diff-del\">-                        debugLog('Error upload', data.message);</div><div class=\"diff-line diff-del\">-                        openDebugPanel();</div><div class=\"diff-line diff-del\">-                        showResult(formatErrorMessage(data));</div><div class=\"diff-line diff-del\">-                        return;</div><div class=\"diff-line diff-del\">-                    }</div><div class=\"diff-line diff-del\">-                    debugLog('Upload OK', data.message || 'analizado');</div><div class=\"diff-line diff-del\">-</div><div class=\"diff-line diff-del\">-                    if (data.warning) {</div><div class=\"diff-line diff-del\">-                        showResult('&lt;div class=\"alert alert-warning\"&gt;' + data.warning + '&lt;/div&gt;');</div><div class=\"diff-line diff-del\">-                    } else if (data.message) {</div><div class=\"diff-line diff-del\">-                        showResult('&lt;div class=\"alert alert-success\"&gt;' + data.message + '&lt;/div&gt;');</div><div class=\"diff-line diff-del\">-                    }</div><div class=\"diff-line diff-del\">-</div><div class=\"diff-line diff-del\">-                    if (data.extracted) {</div><div class=\"diff-line diff-del\">-                        openConfirmModal(data.extracted);</div><div class=\"diff-line diff-del\">-                    }</div><div class=\"diff-line diff-del\">-                })</div><div class=\"diff-line diff-del\">-                .catch(function (err) {</div><div class=\"diff-line diff-del\">-                    debugLog('Fetch error', err &amp;&amp; err.message ? err.message : 'desconocido');</div><div class=\"diff-line diff-del\">-                    openDebugPanel();</div><div class=\"diff-line diff-del\">-                    showResult(formatErrorMessage({</div><div class=\"diff-line diff-del\">-                        message: 'Error de conexión con el servidor.',</div><div class=\"diff-line diff-del\">-                        reasons: ['Comprueba que Apache (XAMPP) esté activo.'],</div><div class=\"diff-line diff-del\">-                        hint: err &amp;&amp; err.message ? err.message : ''</div><div class=\"diff-line diff-del\">-                    }));</div><div class=\"diff-line diff-del\">-                })</div><div class=\"diff-line diff-del\">-                .finally(function () {</div><div class=\"diff-line diff-del\">-                    btn.disabled = false;</div><div class=\"diff-line diff-del\">-                    updateModeUi(expenseMode);</div><div class=\"diff-line diff-del\">-                });</div><div class=\"diff-line diff-add\">+            uploadFilesSequentially(files, docType, btn).finally(function () {</div><div class=\"diff-line diff-add\">+                btn.disabled = false;</div><div class=\"diff-line diff-add\">+                updateModeUi(expenseMode);</div><div class=\"diff-line diff-add\">+            });</div><div class=\"diff-line diff-ctx\">         });</div><div class=\"diff-line diff-ctx\">     }</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-hunk\">@@ -527,6 +683,7 @@</div><div class=\"diff-line diff-ctx\">         bindUploadForm();</div><div class=\"diff-line diff-ctx\">         bindReviewButtons();</div><div class=\"diff-line diff-ctx\">         bindConfirmActions();</div><div class=\"diff-line diff-add\">+        renderSelectedFilesList();</div><div class=\"diff-line diff-ctx\">         debugLog('Página lista', 'modo=' + expenseMode + ' · v=' + JS_VERSION);</div><div class=\"diff-line diff-ctx\">         runDiagnose();</div><div class=\"diff-line diff-ctx\">     });</div></div>","linesAdded":268,"linesRemoved":111,"status":"pending"},"pending":1},"at":1782350033583},{"event":"tool_activity","data":{"activity":{"id":"tool_ce465abe-ae18-446c-8ee0-c7b404db1a5","name":"edit","status":"running","summary":"edit: …ampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig","detail":"","at":"2026-06-25T01:13:55.431Z"}},"at":1782350035431},{"event":"tool_activity","data":{"activity":{"id":"tool_5c7f1319-385d-4bb0-acda-f615f614556","name":"edit","status":"running","summary":"edit: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\CSS\\mobile.css","detail":"","at":"2026-06-25T01:13:55.515Z"}},"at":1782350035515},{"event":"tool_activity","data":{"activity":{"id":"tool_ce465abe-ae18-446c-8ee0-c7b404db1a5","name":"edit","status":"completed","summary":"edit: …ampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig","detail":"","at":"2026-06-25T01:13:55.769Z"}},"at":1782350035769},{"event":"file_change","data":{"sessionId":"cs-1782349967058-c8w8dj","change":{"id":"ch-2","path":"Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig\n@@ -30,7 +30,6 @@\n                 </div>\n                 <div class=\"mb-3\" id=\"file-field\">\n                     <label class=\"form-label\">Documento <span class=\"text-muted\" id=\"file-label-hint\">(obligatorio para OCR)</span></label>\n-                    <input type=\"file\" name=\"image\" id=\"file-input-active\" class=\"visually-hidden\" tabindex=\"-1\" aria-hidden=\"true\" accept=\"image/*,.pdf,application/pdf,.heic,.heif\">\n                     <div class=\"d-grid gap-2 mb-2\">\n                         <div class=\"file-picker-wrap\" id=\"pick-file-wrap\">\n                             <span class=\"btn btn-outline-secondary btn-lg w-100\">\n@@ -49,7 +48,9 @@\n                                    aria-label=\"Hacer foto con cámara\">\n                         </div>\n                     </div>\n-                    <div id=\"file-selected-name\" class=\"small text-muted\">Ningún documento seleccionado</div>\n+                    <div id=\"file-empty-hint\" class=\"small text-muted\">Ningún documento seleccionado</div>\n+                    <div id=\"file-selected-list\" class=\"file-selected-list mt-2\" aria-live=\"polite\"></div>\n+                    <p class=\"small text-muted mb-0 mt-2\">Puedes añadir varios documentos por separado; se analizarán uno por uno.</p>\n                 </div>\n                 <div class=\"d-grid gap-2\">\n                     <button type=\"button\" class=\"btn btn-success btn-lg\" id=\"manual-btn\">","diffHtml":"<div class=\"diff-colored\"><div class=\"diff-line diff-meta\">--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig</div><div class=\"diff-line diff-meta\">+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig</div><div class=\"diff-line diff-hunk\">@@ -30,7 +30,6 @@</div><div class=\"diff-line diff-ctx\">                 &lt;/div&gt;</div><div class=\"diff-line diff-ctx\">                 &lt;div class=\"mb-3\" id=\"file-field\"&gt;</div><div class=\"diff-line diff-ctx\">                     &lt;label class=\"form-label\"&gt;Documento &lt;span class=\"text-muted\" id=\"file-label-hint\"&gt;(obligatorio para OCR)&lt;/span&gt;&lt;/label&gt;</div><div class=\"diff-line diff-del\">-                    &lt;input type=\"file\" name=\"image\" id=\"file-input-active\" class=\"visually-hidden\" tabindex=\"-1\" aria-hidden=\"true\" accept=\"image/*,.pdf,application/pdf,.heic,.heif\"&gt;</div><div class=\"diff-line diff-ctx\">                     &lt;div class=\"d-grid gap-2 mb-2\"&gt;</div><div class=\"diff-line diff-ctx\">                         &lt;div class=\"file-picker-wrap\" id=\"pick-file-wrap\"&gt;</div><div class=\"diff-line diff-ctx\">                             &lt;span class=\"btn btn-outline-secondary btn-lg w-100\"&gt;</div><div class=\"diff-line diff-hunk\">@@ -49,7 +48,9 @@</div><div class=\"diff-line diff-ctx\">                                    aria-label=\"Hacer foto con cámara\"&gt;</div><div class=\"diff-line diff-ctx\">                         &lt;/div&gt;</div><div class=\"diff-line diff-ctx\">                     &lt;/div&gt;</div><div class=\"diff-line diff-del\">-                    &lt;div id=\"file-selected-name\" class=\"small text-muted\"&gt;Ningún documento seleccionado&lt;/div&gt;</div><div class=\"diff-line diff-add\">+                    &lt;div id=\"file-empty-hint\" class=\"small text-muted\"&gt;Ningún documento seleccionado&lt;/div&gt;</div><div class=\"diff-line diff-add\">+                    &lt;div id=\"file-selected-list\" class=\"file-selected-list mt-2\" aria-live=\"polite\"&gt;&lt;/div&gt;</div><div class=\"diff-line diff-add\">+                    &lt;p class=\"small text-muted mb-0 mt-2\"&gt;Puedes añadir varios documentos por separado; se analizarán uno por uno.&lt;/p&gt;</div><div class=\"diff-line diff-ctx\">                 &lt;/div&gt;</div><div class=\"diff-line diff-ctx\">                 &lt;div class=\"d-grid gap-2\"&gt;</div><div class=\"diff-line diff-ctx\">                     &lt;button type=\"button\" class=\"btn btn-success btn-lg\" id=\"manual-btn\"&gt;</div></div>","linesAdded":3,"linesRemoved":2,"status":"pending"},"pending":2},"at":1782350035769},{"event":"tool_activity","data":{"activity":{"id":"tool_5c7f1319-385d-4bb0-acda-f615f614556","name":"edit","status":"running","summary":"edit: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\CSS\\mobile.css","detail":"","at":"2026-06-25T01:13:56.411Z"}},"at":1782350036411},{"event":"tool_activity","data":{"activity":{"id":"tool_0390a35a-ddff-4dbe-b92f-c3c3bb980d1","name":"edit","status":"running","summary":"edit: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\sw.js","detail":"","at":"2026-06-25T01:13:56.592Z"}},"at":1782350036592},{"event":"tool_activity","data":{"activity":{"id":"tool_5c7f1319-385d-4bb0-acda-f615f614556","name":"edit","status":"completed","summary":"edit: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\CSS\\mobile.css","detail":"","at":"2026-06-25T01:13:56.601Z"}},"at":1782350036601},{"event":"file_change","data":{"sessionId":"cs-1782349967058-c8w8dj","change":{"id":"ch-3","path":"Plugins\\ALimpiezasMobile\\Assets\\CSS\\mobile.css","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\CSS\\mobile.css\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\CSS\\mobile.css\n@@ -167,6 +167,44 @@\n     background-color: rgba(25, 135, 84, 0.08);\n }\n \n+.file-selected-list {\n+    display: flex;\n+    flex-direction: column;\n+    gap: 8px;\n+}\n+\n+.file-selected-item {\n+    display: flex;\n+    align-items: center;\n+    justify-content: space-between;\n+    gap: 10px;\n+    padding: 10px 12px;\n+    background: rgba(25, 135, 84, 0.08);\n+    border: 1px solid rgba(25, 135, 84, 0.25);\n+    border-radius: 10px;\n+}\n+\n+.file-selected-info {\n+    flex: 1;\n+    min-width: 0;\n+}\n+\n+.file-selected-item .file-name {\n+    overflow: hidden;\n+    text-overflow: ellipsis;\n+    white-space: nowrap;\n+}\n+\n+.file-selected-item .btn-remove-file {\n+    flex-shrink: 0;\n+    width: 36px;\n+    height: 36px;\n+    padding: 0;\n+    display: inline-flex;\n+    align-items: center;\n+    justify-content: center;\n+}\n+\n .btn-voice-lg {\n     width: 88px;\n     height: 88px;","diffHtml":"<div class=\"diff-colored\"><div class=\"diff-line diff-meta\">--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\CSS\\mobile.css</div><div class=\"diff-line diff-meta\">+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\CSS\\mobile.css</div><div class=\"diff-line diff-hunk\">@@ -167,6 +167,44 @@</div><div class=\"diff-line diff-ctx\">     background-color: rgba(25, 135, 84, 0.08);</div><div class=\"diff-line diff-ctx\"> }</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-add\">+.file-selected-list {</div><div class=\"diff-line diff-add\">+    display: flex;</div><div class=\"diff-line diff-add\">+    flex-direction: column;</div><div class=\"diff-line diff-add\">+    gap: 8px;</div><div class=\"diff-line diff-add\">+}</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+.file-selected-item {</div><div class=\"diff-line diff-add\">+    display: flex;</div><div class=\"diff-line diff-add\">+    align-items: center;</div><div class=\"diff-line diff-add\">+    justify-content: space-between;</div><div class=\"diff-line diff-add\">+    gap: 10px;</div><div class=\"diff-line diff-add\">+    padding: 10px 12px;</div><div class=\"diff-line diff-add\">+    background: rgba(25, 135, 84, 0.08);</div><div class=\"diff-line diff-add\">+    border: 1px solid rgba(25, 135, 84, 0.25);</div><div class=\"diff-line diff-add\">+    border-radius: 10px;</div><div class=\"diff-line diff-add\">+}</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+.file-selected-info {</div><div class=\"diff-line diff-add\">+    flex: 1;</div><div class=\"diff-line diff-add\">+    min-width: 0;</div><div class=\"diff-line diff-add\">+}</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+.file-selected-item .file-name {</div><div class=\"diff-line diff-add\">+    overflow: hidden;</div><div class=\"diff-line diff-add\">+    text-overflow: ellipsis;</div><div class=\"diff-line diff-add\">+    white-space: nowrap;</div><div class=\"diff-line diff-add\">+}</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+.file-selected-item .btn-remove-file {</div><div class=\"diff-line diff-add\">+    flex-shrink: 0;</div><div class=\"diff-line diff-add\">+    width: 36px;</div><div class=\"diff-line diff-add\">+    height: 36px;</div><div class=\"diff-line diff-add\">+    padding: 0;</div><div class=\"diff-line diff-add\">+    display: inline-flex;</div><div class=\"diff-line diff-add\">+    align-items: center;</div><div class=\"diff-line diff-add\">+    justify-content: center;</div><div class=\"diff-line diff-add\">+}</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-ctx\"> .btn-voice-lg {</div><div class=\"diff-line diff-ctx\">     width: 88px;</div><div class=\"diff-line diff-ctx\">     height: 88px;</div></div>","linesAdded":38,"linesRemoved":0,"status":"pending"},"pending":3},"at":1782350036601},{"event":"tool_activity","data":{"activity":{"id":"tool_f624e05f-2c23-4da0-a2aa-c0f158cd3f7","name":"edit","status":"running","summary":"edit: …htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\Master\\MobileLayout.html.twig","detail":"","at":"2026-06-25T01:13:56.691Z"}},"at":1782350036691},{"event":"tool_activity","data":{"activity":{"id":"tool_0390a35a-ddff-4dbe-b92f-c3c3bb980d1","name":"edit","status":"completed","summary":"edit: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\sw.js","detail":"","at":"2026-06-25T01:13:56.780Z"}},"at":1782350036780},{"event":"file_change","data":{"sessionId":"cs-1782349967058-c8w8dj","change":{"id":"ch-4","path":"Plugins\\ALimpiezasMobile\\Assets\\sw.js","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\sw.js\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\sw.js\n@@ -1,4 +1,4 @@\n-const CACHE_NAME = 'alimpiezas-mobile-v3';\n+const CACHE_NAME = 'alimpiezas-mobile-v4';\n const CORE_ASSETS = [\n     '/ALimpiezas/Dinamic/Assets/CSS/mobile.css',\n     '/ALimpiezas/Dinamic/Assets/JS/mobile.js',","diffHtml":"<div class=\"diff-colored\"><div class=\"diff-line diff-meta\">--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\sw.js</div><div class=\"diff-line diff-meta\">+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\sw.js</div><div class=\"diff-line diff-hunk\">@@ -1,4 +1,4 @@</div><div class=\"diff-line diff-del\">-const CACHE_NAME = 'alimpiezas-mobile-v3';</div><div class=\"diff-line diff-add\">+const CACHE_NAME = 'alimpiezas-mobile-v4';</div><div class=\"diff-line diff-ctx\"> const CORE_ASSETS = [</div><div class=\"diff-line diff-ctx\">     '/ALimpiezas/Dinamic/Assets/CSS/mobile.css',</div><div class=\"diff-line diff-ctx\">     '/ALimpiezas/Dinamic/Assets/JS/mobile.js',</div></div>","linesAdded":1,"linesRemoved":1,"status":"pending"},"pending":4},"at":1782350036780},{"event":"tool_activity","data":{"activity":{"id":"tool_f624e05f-2c23-4da0-a2aa-c0f158cd3f7","name":"edit","status":"completed","summary":"edit: …htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\Master\\MobileLayout.html.twig","detail":"","at":"2026-06-25T01:13:57.160Z"}},"at":1782350037160},{"event":"file_change","data":{"sessionId":"cs-1782349967058-c8w8dj","change":{"id":"ch-5","path":"Plugins\\ALimpiezasMobile\\View\\Master\\MobileLayout.html.twig","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\Master\\MobileLayout.html.twig\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\Master\\MobileLayout.html.twig\n@@ -14,7 +14,7 @@\n     <title>{{ fsc.title }}</title>\n     <link rel=\"stylesheet\" href=\"{{ asset('node_modules/bootstrap/dist/css/bootstrap.min.css') }}\">\n     <link rel=\"stylesheet\" href=\"{{ asset('node_modules/@fortawesome/fontawesome-free/css/all.min.css') }}\">\n-    <link rel=\"stylesheet\" href=\"{{ asset('Dinamic/Assets/CSS/mobile.css') }}?v=20260625c\">\n+    <link rel=\"stylesheet\" href=\"{{ asset('Dinamic/Assets/CSS/mobile.css') }}?v=20260625d\">\n     {% block head %}{% endblock %}\n </head>\n <body class=\"mobile-app\">","diffHtml":"<div class=\"diff-colored\"><div class=\"diff-line diff-meta\">--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\Master\\MobileLayout.html.twig</div><div class=\"diff-line diff-meta\">+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\Master\\MobileLayout.html.twig</div><div class=\"diff-line diff-hunk\">@@ -14,7 +14,7 @@</div><div class=\"diff-line diff-ctx\">     &lt;title&gt;{{ fsc.title }}&lt;/title&gt;</div><div class=\"diff-line diff-ctx\">     &lt;link rel=\"stylesheet\" href=\"{{ asset('node_modules/bootstrap/dist/css/bootstrap.min.css') }}\"&gt;</div><div class=\"diff-line diff-ctx\">     &lt;link rel=\"stylesheet\" href=\"{{ asset('node_modules/@fortawesome/fontawesome-free/css/all.min.css') }}\"&gt;</div><div class=\"diff-line diff-del\">-    &lt;link rel=\"stylesheet\" href=\"{{ asset('Dinamic/Assets/CSS/mobile.css') }}?v=20260625c\"&gt;</div><div class=\"diff-line diff-add\">+    &lt;link rel=\"stylesheet\" href=\"{{ asset('Dinamic/Assets/CSS/mobile.css') }}?v=20260625d\"&gt;</div><div class=\"diff-line diff-ctx\">     {% block head %}{% endblock %}</div><div class=\"diff-line diff-ctx\"> &lt;/head&gt;</div><div class=\"diff-line diff-ctx\"> &lt;body class=\"mobile-app\"&gt;</div></div>","linesAdded":1,"linesRemoved":1,"status":"pending"},"pending":5},"at":1782350037160},{"event":"tool_activity","data":{"activity":{"id":"tool_5115ef06-54eb-44a9-9787-7fdb61eaedc","name":"edit","status":"completed","summary":"edit: …htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\Master\\MobileLayout.html.twig","detail":"","at":"2026-06-25T01:13:57.880Z"}},"at":1782350037880},{"event":"file_change","data":{"sessionId":"cs-1782349967058-c8w8dj","change":{"id":"ch-6","path":"Plugins\\ALimpiezasMobile\\View\\Master\\MobileLayout.html.twig","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\Master\\MobileLayout.html.twig\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\Master\\MobileLayout.html.twig\n@@ -67,11 +67,11 @@\n             expenseMode: {{ settings('alimpiezas', 'expense_mode', 'ocr')|json_encode|raw }}\n         };\n     </script>\n-    <script src=\"{{ asset('Dinamic/Assets/JS/help-guide.js') }}?v=20260625c\"></script>\n-    <script src=\"{{ asset('Dinamic/Assets/JS/mobile.js') }}?v=20260625c\"></script>\n+    <script src=\"{{ asset('Dinamic/Assets/JS/help-guide.js') }}?v=20260625d\"></script>\n+    <script src=\"{{ asset('Dinamic/Assets/JS/mobile.js') }}?v=20260625d\"></script>\n     <script>\n         if ('serviceWorker' in navigator) {\n-            navigator.serviceWorker.register('{{ asset('Dinamic/Assets/sw.js') }}?v=20260625c')\n+            navigator.serviceWorker.register('{{ asset('Dinamic/Assets/sw.js') }}?v=20260625d')\n                 .then(function (reg) {\n                     if (reg.waiting) {\n                         reg.waiting.postMessage({ type: 'SKIP_WAITING' });","diffHtml":"<div class=\"diff-colored\"><div class=\"diff-line diff-meta\">--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\Master\\MobileLayout.html.twig</div><div class=\"diff-line diff-meta\">+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\Master\\MobileLayout.html.twig</div><div class=\"diff-line diff-hunk\">@@ -67,11 +67,11 @@</div><div class=\"diff-line diff-ctx\">             expenseMode: {{ settings('alimpiezas', 'expense_mode', 'ocr')|json_encode|raw }}</div><div class=\"diff-line diff-ctx\">         };</div><div class=\"diff-line diff-ctx\">     &lt;/script&gt;</div><div class=\"diff-line diff-del\">-    &lt;script src=\"{{ asset('Dinamic/Assets/JS/help-guide.js') }}?v=20260625c\"&gt;&lt;/script&gt;</div><div class=\"diff-line diff-del\">-    &lt;script src=\"{{ asset('Dinamic/Assets/JS/mobile.js') }}?v=20260625c\"&gt;&lt;/script&gt;</div><div class=\"diff-line diff-add\">+    &lt;script src=\"{{ asset('Dinamic/Assets/JS/help-guide.js') }}?v=20260625d\"&gt;&lt;/script&gt;</div><div class=\"diff-line diff-add\">+    &lt;script src=\"{{ asset('Dinamic/Assets/JS/mobile.js') }}?v=20260625d\"&gt;&lt;/script&gt;</div><div class=\"diff-line diff-ctx\">     &lt;script&gt;</div><div class=\"diff-line diff-ctx\">         if ('serviceWorker' in navigator) {</div><div class=\"diff-line diff-del\">-            navigator.serviceWorker.register('{{ asset('Dinamic/Assets/sw.js') }}?v=20260625c')</div><div class=\"diff-line diff-add\">+            navigator.serviceWorker.register('{{ asset('Dinamic/Assets/sw.js') }}?v=20260625d')</div><div class=\"diff-line diff-ctx\">                 .then(function (reg) {</div><div class=\"diff-line diff-ctx\">                     if (reg.waiting) {</div><div class=\"diff-line diff-ctx\">                         reg.waiting.postMessage({ type: 'SKIP_WAITING' });</div></div>","linesAdded":3,"linesRemoved":3,"status":"pending"},"pending":6},"at":1782350037880},{"event":"tool_activity","data":{"activity":{"id":"tool_c695eb71-cc20-4d75-9588-fa84096ebe4","name":"edit","status":"completed","summary":"edit: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js","detail":"","at":"2026-06-25T01:14:00.802Z"}},"at":1782350040802},{"event":"file_change","data":{"sessionId":"cs-1782349967058-c8w8dj","change":{"id":"ch-7","path":"Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js\n@@ -246,20 +246,22 @@\n             return;\n         }\n \n+        var picked = Array.prototype.slice.call(sourceInput.files);\n         var added = 0;\n         var skipped = 0;\n-        for (var i = 0; i < sourceInput.files.length; i++) {\n-            if (addSelectedFile(sourceInput.files[i])) {\n+\n+        picked.forEach(function (file) {\n+            if (addSelectedFile(file)) {\n                 added += 1;\n             } else {\n                 skipped += 1;\n             }\n-        }\n+        });\n \n         resetPickerInput(sourceInput);\n         debugLog(sourceLabel || 'Archivo', added + ' añadido(s), total=' + selectedFiles.length);\n \n-        if (added === 0 && sourceInput.files.length > 0) {\n+        if (added === 0 && picked.length > 0) {\n             showResult(formatErrorMessage({\n                 message: skipped > 0 ? 'Ese documento ya está en la lista.' : 'No se pudo leer el archivo seleccionado.',\n                 hint: 'Prueba con JPG, PNG, WEBP, HEIC o PDF.'","diffHtml":"<div class=\"diff-colored\"><div class=\"diff-line diff-meta\">--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js</div><div class=\"diff-line diff-meta\">+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js</div><div class=\"diff-line diff-hunk\">@@ -246,20 +246,22 @@</div><div class=\"diff-line diff-ctx\">             return;</div><div class=\"diff-line diff-ctx\">         }</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-add\">+        var picked = Array.prototype.slice.call(sourceInput.files);</div><div class=\"diff-line diff-ctx\">         var added = 0;</div><div class=\"diff-line diff-ctx\">         var skipped = 0;</div><div class=\"diff-line diff-del\">-        for (var i = 0; i &lt; sourceInput.files.length; i++) {</div><div class=\"diff-line diff-del\">-            if (addSelectedFile(sourceInput.files[i])) {</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+        picked.forEach(function (file) {</div><div class=\"diff-line diff-add\">+            if (addSelectedFile(file)) {</div><div class=\"diff-line diff-ctx\">                 added += 1;</div><div class=\"diff-line diff-ctx\">             } else {</div><div class=\"diff-line diff-ctx\">                 skipped += 1;</div><div class=\"diff-line diff-ctx\">             }</div><div class=\"diff-line diff-del\">-        }</div><div class=\"diff-line diff-add\">+        });</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-ctx\">         resetPickerInput(sourceInput);</div><div class=\"diff-line diff-ctx\">         debugLog(sourceLabel || 'Archivo', added + ' añadido(s), total=' + selectedFiles.length);</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-del\">-        if (added === 0 &amp;&amp; sourceInput.files.length &gt; 0) {</div><div class=\"diff-line diff-add\">+        if (added === 0 &amp;&amp; picked.length &gt; 0) {</div><div class=\"diff-line diff-ctx\">             showResult(formatErrorMessage({</div><div class=\"diff-line diff-ctx\">                 message: skipped &gt; 0 ? 'Ese documento ya está en la lista.' : 'No se pudo leer el archivo seleccionado.',</div><div class=\"diff-line diff-ctx\">                 hint: 'Prueba con JPG, PNG, WEBP, HEIC o PDF.'</div></div>","linesAdded":6,"linesRemoved":4,"status":"pending"},"pending":7},"at":1782350040803},{"event":"tool_activity","data":{"activity":{"id":"tool_92fa8af2-ed23-4016-88d5-244d4f67679","name":"edit","status":"completed","summary":"edit: …ampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php","detail":"","at":"2026-06-25T01:14:02.356Z"}},"at":1782350042356},{"event":"file_change","data":{"sessionId":"cs-1782349967058-c8w8dj","change":{"id":"ch-8","path":"Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php\n@@ -128,16 +128,54 @@\n     private function resolveUploadedFile(string $field): ?UploadedFile\n     {\n         $file = $this->request->files->get($field);\n-        if ($file instanceof UploadedFile) {\n+        if ($file instanceof UploadedFile && $file->isValid()) {\n             return $file;\n         }\n \n         foreach ($this->request->files->getArray($field) as $item) {\n-            if ($item instanceof UploadedFile) {\n+            if ($item instanceof UploadedFile && $item->isValid()) {\n                 return $item;\n             }\n         }\n \n         return null;\n     }\n+\n+    /** @return array<string, mixed> */\n+    private function uploadDebugContext(): array\n+    {\n+        $files = $this->request->files->all();\n+        $summary = [];\n+        foreach ($files as $key => $value) {\n+            if ($value instanceof UploadedFile) {\n+                $summary[$key] = [\n+                    'name' => $value->getClientOriginalName(),\n+                    'size' => $value->getSize(),\n+                    'error' => $value->getError(),\n+                    'valid' => $value->isValid(),\n+                ];\n+                continue;\n+            }\n+            if (is_array($value)) {\n+                $summary[$key] = [];\n+                foreach ($value as $idx => $item) {\n+                    if ($item instanceof UploadedFile) {\n+                        $summary[$key][$idx] = [\n+                            'name' => $item->getClientOriginalName(),\n+                            'size' => $item->getSize(),\n+                            'error' => $item->getError(),\n+                            'valid' => $item->isValid(),\n+                        ];\n+                    }\n+                }\n+            }\n+        }\n+\n+        return [\n+            'files_keys' => array_keys($files),\n+            'files' => $summary,\n+            'content_length' => (int) ($_SERVER['CONTENT_LENGTH'] ?? 0),\n+            'content_type' => (string) ($_SERVER['CONTENT_TYPE'] ?? ''),\n+        ];\n+    }\n }","diffHtml":"<div class=\"diff-colored\"><div class=\"diff-line diff-meta\">--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php</div><div class=\"diff-line diff-meta\">+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php</div><div class=\"diff-line diff-hunk\">@@ -128,16 +128,54 @@</div><div class=\"diff-line diff-ctx\">     private function resolveUploadedFile(string $field): ?UploadedFile</div><div class=\"diff-line diff-ctx\">     {</div><div class=\"diff-line diff-ctx\">         $file = $this-&gt;request-&gt;files-&gt;get($field);</div><div class=\"diff-line diff-del\">-        if ($file instanceof UploadedFile) {</div><div class=\"diff-line diff-add\">+        if ($file instanceof UploadedFile &amp;&amp; $file-&gt;isValid()) {</div><div class=\"diff-line diff-ctx\">             return $file;</div><div class=\"diff-line diff-ctx\">         }</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-ctx\">         foreach ($this-&gt;request-&gt;files-&gt;getArray($field) as $item) {</div><div class=\"diff-line diff-del\">-            if ($item instanceof UploadedFile) {</div><div class=\"diff-line diff-add\">+            if ($item instanceof UploadedFile &amp;&amp; $item-&gt;isValid()) {</div><div class=\"diff-line diff-ctx\">                 return $item;</div><div class=\"diff-line diff-ctx\">             }</div><div class=\"diff-line diff-ctx\">         }</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-ctx\">         return null;</div><div class=\"diff-line diff-ctx\">     }</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+    /** @return array&lt;string, mixed&gt; */</div><div class=\"diff-line diff-add\">+    private function uploadDebugContext(): array</div><div class=\"diff-line diff-add\">+    {</div><div class=\"diff-line diff-add\">+        $files = $this-&gt;request-&gt;files-&gt;all();</div><div class=\"diff-line diff-add\">+        $summary = [];</div><div class=\"diff-line diff-add\">+        foreach ($files as $key =&gt; $value) {</div><div class=\"diff-line diff-add\">+            if ($value instanceof UploadedFile) {</div><div class=\"diff-line diff-add\">+                $summary[$key] = [</div><div class=\"diff-line diff-add\">+                    'name' =&gt; $value-&gt;getClientOriginalName(),</div><div class=\"diff-line diff-add\">+                    'size' =&gt; $value-&gt;getSize(),</div><div class=\"diff-line diff-add\">+                    'error' =&gt; $value-&gt;getError(),</div><div class=\"diff-line diff-add\">+                    'valid' =&gt; $value-&gt;isValid(),</div><div class=\"diff-line diff-add\">+                ];</div><div class=\"diff-line diff-add\">+                continue;</div><div class=\"diff-line diff-add\">+            }</div><div class=\"diff-line diff-add\">+            if (is_array($value)) {</div><div class=\"diff-line diff-add\">+                $summary[$key] = [];</div><div class=\"diff-line diff-add\">+                foreach ($value as $idx =&gt; $item) {</div><div class=\"diff-line diff-add\">+                    if ($item instanceof UploadedFile) {</div><div class=\"diff-line diff-add\">+                        $summary[$key][$idx] = [</div><div class=\"diff-line diff-add\">+                            'name' =&gt; $item-&gt;getClientOriginalName(),</div><div class=\"diff-line diff-add\">+                            'size' =&gt; $item-&gt;getSize(),</div><div class=\"diff-line diff-add\">+                            'error' =&gt; $item-&gt;getError(),</div><div class=\"diff-line diff-add\">+                            'valid' =&gt; $item-&gt;isValid(),</div><div class=\"diff-line diff-add\">+                        ];</div><div class=\"diff-line diff-add\">+                    }</div><div class=\"diff-line diff-add\">+                }</div><div class=\"diff-line diff-add\">+            }</div><div class=\"diff-line diff-add\">+        }</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+        return [</div><div class=\"diff-line diff-add\">+            'files_keys' =&gt; array_keys($files),</div><div class=\"diff-line diff-add\">+            'files' =&gt; $summary,</div><div class=\"diff-line diff-add\">+            'content_length' =&gt; (int) ($_SERVER['CONTENT_LENGTH'] ?? 0),</div><div class=\"diff-line diff-add\">+            'content_type' =&gt; (string) ($_SERVER['CONTENT_TYPE'] ?? ''),</div><div class=\"diff-line diff-add\">+        ];</div><div class=\"diff-line diff-add\">+    }</div><div class=\"diff-line diff-ctx\"> }</div></div>","linesAdded":40,"linesRemoved":2,"status":"pending"},"pending":8},"at":1782350042356},{"event":"tool_activity","data":{"activity":{"id":"tool_f6595e9b-ccff-4e6d-9578-1609b94d8c5","name":"edit","status":"completed","summary":"edit: …ampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php","detail":"","at":"2026-06-25T01:14:02.575Z"}},"at":1782350042575},{"event":"file_change","data":{"sessionId":"cs-1782349967058-c8w8dj","change":{"id":"ch-9","path":"Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php\n@@ -83,10 +83,7 @@\n                 'message' => 'No se recibió ningún documento',\n                 'reasons' => ['Selecciona un archivo, una foto de galería o haz una foto antes de escanear.'],\n                 'hint' => 'Formatos admitidos: JPG, PNG, WEBP, HEIC y PDF.',\n-                'debug' => [\n-                    'files_keys' => array_keys($this->request->files->all()),\n-                    'content_length' => (int) ($_SERVER['CONTENT_LENGTH'] ?? 0),\n-                ],\n+                'debug' => $this->uploadDebugContext(),\n             ];\n         }\n ","diffHtml":"<div class=\"diff-colored\"><div class=\"diff-line diff-meta\">--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php</div><div class=\"diff-line diff-meta\">+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php</div><div class=\"diff-line diff-hunk\">@@ -83,10 +83,7 @@</div><div class=\"diff-line diff-ctx\">                 'message' =&gt; 'No se recibió ningún documento',</div><div class=\"diff-line diff-ctx\">                 'reasons' =&gt; ['Selecciona un archivo, una foto de galería o haz una foto antes de escanear.'],</div><div class=\"diff-line diff-ctx\">                 'hint' =&gt; 'Formatos admitidos: JPG, PNG, WEBP, HEIC y PDF.',</div><div class=\"diff-line diff-del\">-                'debug' =&gt; [</div><div class=\"diff-line diff-del\">-                    'files_keys' =&gt; array_keys($this-&gt;request-&gt;files-&gt;all()),</div><div class=\"diff-line diff-del\">-                    'content_length' =&gt; (int) ($_SERVER['CONTENT_LENGTH'] ?? 0),</div><div class=\"diff-line diff-del\">-                ],</div><div class=\"diff-line diff-add\">+                'debug' =&gt; $this-&gt;uploadDebugContext(),</div><div class=\"diff-line diff-ctx\">             ];</div><div class=\"diff-line diff-ctx\">         }</div><div class=\"diff-line diff-ctx\"> </div></div>","linesAdded":1,"linesRemoved":4,"status":"pending"},"pending":9},"at":1782350042575},{"event":"tool_activity","data":{"activity":{"id":"tool_53ecf7d8-efd4-47d3-a751-2b8fe70fe53","name":"shell","status":"completed","summary":"shell","detail":"$files = @(\n  @{src='Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js'; dst='Dinamic\\Assets\\JS\\mobile.js'},\n  @{src='Plugins\\ALimpiezasMobile\\Assets\\CSS\\mobile.css'; dst='Dinamic\\Assets\\CSS\\mobile.css'},…","at":"2026-06-25T01:14:04.088Z"}},"at":1782350044088},{"event":"tool_activity","data":{"activity":{"id":"tool_a60f40ed-8d09-4b7c-a44c-1ab9b3ea6a5","name":"shell","status":"running","summary":"shell","detail":"php -l \"C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php\" 2>&1","at":"2026-06-25T01:14:06.251Z"}},"at":1782350046251},{"event":"tool_activity","data":{"activity":{"id":"tool_3af4ba1b-751d-4c9f-a110-795d1d6eb9a","name":"grep","status":"completed","summary":"grep: C:\\xampp\\htdocs\\ALimpiezas","detail":"","at":"2026-06-25T01:14:06.558Z"}},"at":1782350046558},{"event":"tool_activity","data":{"activity":{"id":"tool_a60f40ed-8d09-4b7c-a44c-1ab9b3ea6a5","name":"shell","status":"completed","summary":"shell","detail":"php -l \"C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php\" 2>&1","at":"2026-06-25T01:14:07.186Z"}},"at":1782350047186},{"event":"text","data":{"chunk":"**Respuesta desde móvil · Cursor Bridge**\n\nCompleté la implementación que se cortó. El problema era que en móvil el archivo se copiaba a un `<input>` oculto con `DataTransfer`, y esa referencia se perdía al pulsar «Escanear».\n\n## Qué se corrigió\n\n1. **Archivos en memoria** — Los documentos se guardan en un array `selectedFiles[]` al elegirlos. Ya no depende del input oculto ni de `DataTransfer`.\n2. **Varios documentos** — Puedes añadir fotos/archivos uno a uno; aparecen en una lista con botón ✕ para quitar. Se procesan secuencialmente (1/N, 2/N…).\n3. **UI actualizada** — Lista de documentos listos + texto: *«Puedes añadir varios documentos por separado»*.\n4. **Service worker v4** — Cache bust `20260625d` para forzar el JS nuevo.\n5. **Debug backend** — Si falla la subida, el panel muestra detalle de `$_FILES` (nombre, tamaño, error).\n\n## Cómo probar\n\n1. **Recarga forzada** en el móvil (cerrar pestaña y volver, o vaciar caché del sitio).\n2. Elige una foto → debe aparecer en la **lista verde** (no solo el texto gris).\n3. Pulsa **Escanear con OCR** → debe enviar sin pedir de nuevo el archivo.\n4. Añade otra foto → deben quedar **2 en la lista** antes de escanear.\n\nEn **Debug conexión OCR/IA** deberías ver:\n- `JS · 2026-06-25d`\n- `Galería/archivo · 1 añadido(s), total=1`\n- `Submit · modo=ocr · docs=1`\n\nSi sigue fallando, abre el panel debug y dime qué aparece en `Submit` y `Respuesta servidor`."},"at":1782350049921},{"event":"status","data":{"text":"Procesando…"},"at":1782350049913},{"event":"status","data":{"text":"Turno añadido al transcript del IDE (reabrir chat para verlo)"},"at":1782350049916},{"event":"done","data":{"ok":true,"bridgeSessionId":"bs-1782350049914-xumjuq","agentId":"agent-18f3257a-043c-4892-a88b-6efdf18acae9","runId":"run-4863c429-c24f-4aa9-a110-095a57e5b19c","status":"finished","result":"**Respuesta desde móvil · Cursor Bridge**\n\nCompleté la implementación que se cortó. El problema era que en móvil el archivo se copiaba a un `<input>` oculto con `DataTransfer`, y esa referencia se perdía al pulsar «Escanear».\n\n## Qué se corrigió\n\n1. **Archivos en memoria** — Los documentos se guardan en un array `selectedFiles[]` al elegirlos. Ya no depende del input oculto ni de `DataTransfer`.\n2. **Varios documentos** — Puedes añadir fotos/archivos uno a uno; aparecen en una lista con botón ✕ para quitar. Se procesan secuencialmente (1/N, 2/N…).\n3. **UI actualizada** — Lista de documentos listos + texto: *«Puedes añadir varios documentos por separado»*.\n4. **Service worker v4** — Cache bust `20260625d` para forzar el JS nuevo.\n5. **Debug backend** — Si falla la subida, el panel muestra detalle de `$_FILES` (nombre, tamaño, error).\n\n## Cómo probar\n\n1. **Recarga forzada** en el móvil (cerrar pestaña y volver, o vaciar caché del sitio).\n2. Elige una foto → debe aparecer en la **lista verde** (no solo el texto gris).\n3. Pulsa **Escanear con OCR** → debe enviar sin pedir de nuevo el archivo.\n4. Añade otra foto → deben quedar **2 en la lista** antes de escanear.\n\nEn **Debug conexión OCR/IA** deberías ver:\n- `JS · 2026-06-25d`\n- `Galería/archivo · 1 añadido(s), total=1`\n- `Submit · modo=ocr · docs=1`\n\nSi sigue fallando, abre el panel debug y dime qué aparece en `Submit` y `Respuesta servidor`.","changeSessionId":"cs-1782349967058-c8w8dj","pendingChanges":9,"changes":[{"id":"ch-1","callId":"tool_bf94028a-b312-4692-b9ee-9399ef0893b","path":"Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js\n@@ -4,7 +4,9 @@\n     var confirmModal = null;\n     var confirmModalEl = null;\n     var expenseMode = (window.ALIMPIEZAS_MOBILE && window.ALIMPIEZAS_MOBILE.expenseMode) || 'ocr';\n-    var JS_VERSION = '2026-06-25c';\n+    var JS_VERSION = '2026-06-25d';\n+    var selectedFiles = [];\n+    var fileIdCounter = 0;\n \n     function route(path) {\n         var base = (window.ALIMPIEZAS_MOBILE && window.ALIMPIEZAS_MOBILE.route) || '';\n@@ -118,104 +120,182 @@\n         return html;\n     }\n \n-    function activeFileInput() {\n-        return document.getElementById('file-input-active');\n+    function fileKey(file) {\n+        return [file.name, file.size, file.lastModified || 0, file.type || ''].join('|');\n     }\n \n-    function updateSelectedFileLabel() {\n-        var label = document.getElementById('file-selected-name');\n-        var file = resolvedUploadFile();\n-        if (!label) {\n-            return;\n+    function formatFileSize(bytes) {\n+        if (!bytes || bytes < 1024) {\n+            return Math.max(1, bytes || 0) + ' B';\n         }\n-        if (!file) {\n-            label.textContent = 'Ningún documento seleccionado';\n-            label.className = 'small text-muted';\n-            return;\n+        if (bytes < 1024 * 1024) {\n+            return Math.round(bytes / 1024) + ' KB';\n         }\n+        return (bytes / (1024 * 1024)).toFixed(1) + ' MB';\n+    }\n \n-        var sizeKb = Math.max(1, Math.round(file.size / 1024));\n-        label.textContent = file.name + ' (' + sizeKb + ' KB) — listo para escanear';\n-        label.className = 'small text-success fw-semibold';\n+    function getSelectedFiles() {\n+        return selectedFiles.map(function (item) {\n+            return item.file;\n+        });\n     }\n \n-    function assignSelectedFile(sourceInput) {\n-        var target = activeFileInput();\n-        if (!target || !sourceInput || !sourceInput.files.length) {\n-            return false;\n+    function hasSelectedFiles() {\n+        return selectedFiles.length > 0;\n+    }\n+\n+    function renderSelectedFilesList() {\n+        var list = document.getElementById('file-selected-list');\n+        var emptyHint = document.getElementById('file-empty-hint');\n+        if (!list) {\n+            return;\n         }\n \n-        var file = sourceInput.files[0];\n-        var copied = false;\n+        list.innerHTML = '';\n+        selectedFiles.forEach(function (item) {\n+            var row = document.createElement('div');\n+            row.className = 'file-selected-item';\n+            row.setAttribute('data-file-id', String(item.id));\n \n-        if (typeof DataTransfer !== 'undefined') {\n-            try {\n-                var transfer = new DataTransfer();\n-                transfer.items.add(file);\n-                target.files = transfer.files;\n-                copied = target.files.length > 0;\n-            } catch (err) {\n-                copied = false;\n-            }\n-        }\n+            var info = document.createElement('div');\n+            info.className = 'file-selected-info';\n \n-        if (!copied) {\n-            target._pendingFile = file;\n-        } else {\n-            delete target._pendingFile;\n+            var name = document.createElement('div');\n+            name.className = 'file-name fw-semibold';\n+            name.textContent = item.file.name;\n+\n+            var meta = document.createElement('div');\n+            meta.className = 'file-meta small text-muted';\n+            meta.textContent = formatFileSize(item.file.size) + ' · listo para escanear';\n+\n+            info.appendChild(name);\n+            info.appendChild(meta);\n+\n+            var removeBtn = document.createElement('button');\n+            removeBtn.type = 'button';\n+            removeBtn.className = 'btn btn-sm btn-outline-danger btn-remove-file';\n+            removeBtn.setAttribute('data-file-id', String(item.id));\n+            removeBtn.setAttribute('aria-label', 'Quitar ' + item.file.name);\n+            removeBtn.innerHTML = '<i class=\"fa-solid fa-xmark\"></i>';\n+\n+            row.appendChild(info);\n+            row.appendChild(removeBtn);\n+            list.appendChild(row);\n+        });\n+\n+        if (emptyHint) {\n+            emptyHint.style.display = selectedFiles.length ? 'none' : 'block';\n         }\n \n         document.querySelectorAll('.file-picker-wrap').forEach(function (wrap) {\n-            wrap.classList.remove('is-selected');\n+            wrap.classList.toggle('is-selected', selectedFiles.length > 0);\n         });\n-        var wrap = sourceInput.closest('.file-picker-wrap');\n-        if (wrap) {\n-            wrap.classList.add('is-selected');\n+    }\n+\n+    function addSelectedFile(file) {\n+        if (!file || !file.size) {\n+            return false;\n         }\n \n-        updateSelectedFileLabel();\n+        var key = fileKey(file);\n+        for (var i = 0; i < selectedFiles.length; i++) {\n+            if (selectedFiles[i].key === key) {\n+                return false;\n+            }\n+        }\n+\n+        fileIdCounter += 1;\n+        selectedFiles.push({\n+            id: fileIdCounter,\n+            key: key,\n+            file: file\n+        });\n+        renderSelectedFilesList();\n         return true;\n     }\n \n-    function resolvedUploadFile() {\n-        var fileInput = activeFileInput();\n-        if (!fileInput) {\n-            return null;\n+    function removeSelectedFile(id) {\n+        selectedFiles = selectedFiles.filter(function (item) {\n+            return item.id !== id;\n+        });\n+        renderSelectedFilesList();\n+        debugLog('Documento quitado', 'quedan ' + selectedFiles.length);\n+    }\n+\n+    function clearSelectedFiles() {\n+        selectedFiles = [];\n+        renderSelectedFilesList();\n+        ['file-input-gallery', 'file-input-camera'].forEach(function (inputId) {\n+            var input = document.getElementById(inputId);\n+            if (input) {\n+                input.value = '';\n+            }\n+        });\n+    }\n+\n+    function resetPickerInput(input) {\n+        if (!input) {\n+            return;\n         }\n-        if (fileInput._pendingFile) {\n-            return fileInput._pendingFile;\n+        input.value = '';\n+    }\n+\n+    function addFilesFromInput(sourceInput, sourceLabel) {\n+        if (!sourceInput || !sourceInput.files || !sourceInput.files.length) {\n+            debugLog(sourceLabel || 'Archivo', 'sin selección');\n+            return;\n         }\n-        if (fileInput.files && fileInput.files.length) {\n-            return fileInput.files[0];\n+\n+        var added = 0;\n+        var skipped = 0;\n+        for (var i = 0; i < sourceInput.files.length; i++) {\n+            if (addSelectedFile(sourceInput.files[i])) {\n+                added += 1;\n+            } else {\n+                skipped += 1;\n+            }\n         }\n-        return null;\n+\n+        resetPickerInput(sourceInput);\n+        debugLog(sourceLabel || 'Archivo', added + ' añadido(s), total=' + selectedFiles.length);\n+\n+        if (added === 0 && sourceInput.files.length > 0) {\n+            showResult(formatErrorMessage({\n+                message: skipped > 0 ? 'Ese documento ya está en la lista.' : 'No se pudo leer el archivo seleccionado.',\n+                hint: 'Prueba con JPG, PNG, WEBP, HEIC o PDF.'\n+            }));\n+            return;\n+        }\n+\n+        if (added > 0) {\n+            showResult('');\n+        }\n     }\n \n     function bindFilePickers() {\n         var galleryInput = document.getElementById('file-input-gallery');\n         var cameraInput = document.getElementById('file-input-camera');\n+        var list = document.getElementById('file-selected-list');\n \n+        if (list) {\n+            list.addEventListener('click', function (event) {\n+                var btn = event.target.closest('.btn-remove-file');\n+                if (!btn) {\n+                    return;\n+                }\n+                removeSelectedFile(parseInt(btn.getAttribute('data-file-id'), 10));\n+            });\n+        }\n+\n         if (galleryInput) {\n             galleryInput.addEventListener('change', function () {\n-                debugLog('Galería/archivo', galleryInput.files.length ? galleryInput.files[0].name : 'vacío');\n-                if (!assignSelectedFile(galleryInput)) {\n-                    showResult(formatErrorMessage({\n-                        message: 'No se pudo leer el archivo seleccionado.',\n-                        hint: 'Prueba con otro formato (JPG, PNG o PDF).'\n-                    }));\n-                }\n+                addFilesFromInput(galleryInput, 'Galería/archivo');\n             });\n         }\n \n         if (cameraInput) {\n             cameraInput.addEventListener('change', function () {\n-                debugLog('Foto cámara', cameraInput.files.length ? cameraInput.files[0].name : 'vacío');\n-                if (!assignSelectedFile(cameraInput)) {\n-                    showResult(formatErrorMessage({\n-                        message: 'No se pudo leer la foto.',\n-                        hint: 'Comprueba permisos de cámara en el navegador.'\n-                    }));\n-                }\n+                addFilesFromInput(cameraInput, 'Foto cámara');\n             });\n         }\n     }\n@@ -238,7 +318,7 @@\n                 manualBtn.style.display = 'block';\n             }\n             if (fileLabel) {\n-                fileLabel.textContent = '(opcional en manual)';\n+                fileLabel.textContent = '(opcional — puedes añadir varios)';\n             }\n             if (pickFileWrap) {\n                 pickFileWrap.style.display = 'block';\n@@ -363,6 +443,113 @@\n         });\n     }\n \n+    function buildUploadFormData(file, docType) {\n+        var formData = new FormData();\n+        formData.append('action', 'upload');\n+        formData.append('doc_type', docType);\n+        formData.append('multireqtoken', tokenValue());\n+        formData.append('image', file, file.name || 'documento.jpg');\n+        return formData;\n+    }\n+\n+    function uploadSingleFile(file, docType) {\n+        debugLog('Enviando', file.name + ' (' + file.size + ' B)');\n+        return postForm(buildUploadFormData(file, docType)).then(function (data) {\n+            if (data.debug) {\n+                debugLog('Respuesta servidor', data.debug);\n+            }\n+            return data;\n+        });\n+    }\n+\n+    function uploadFilesSequentially(files, docType, btn) {\n+        var index = 0;\n+        var okCount = 0;\n+        var lastExtracted = null;\n+        var errors = [];\n+\n+        function processNext() {\n+            if (index >= files.length) {\n+                var html = '';\n+                if (okCount === files.length) {\n+                    html = '<div class=\"alert alert-success mb-0\">' + okCount + ' documento(s) analizado(s). Revisa la cola.</div>';\n+                } else if (okCount > 0) {\n+                    html = '<div class=\"alert alert-warning mb-0\"><strong>' + okCount + ' de ' + files.length + ' documentos analizados.</strong>';\n+                    if (errors.length) {\n+                        html += '<ul class=\"mb-0 mt-2 small\">';\n+                        errors.forEach(function (err) {\n+                            html += '<li>' + err.name + ': ' + err.message + '</li>';\n+                        });\n+                        html += '</ul>';\n+                    }\n+                    html += '</div>';\n+                } else {\n+                    html = formatErrorMessage({\n+                        message: 'No se pudo analizar ningún documento.',\n+                        reasons: errors.map(function (err) {\n+                            return err.name + ': ' + err.message;\n+                        })\n+                    });\n+                }\n+                showResult(html);\n+\n+                if (okCount > 0) {\n+                    clearSelectedFiles();\n+                }\n+\n+                if (files.length === 1 && lastExtracted) {\n+                    openConfirmModal(lastExtracted);\n+                } else if (okCount > 0) {\n+                    setTimeout(function () {\n+                        window.location.reload();\n+                    }, 1800);\n+                }\n+                return Promise.resolve();\n+            }\n+\n+            var file = files[index];\n+            var current = index + 1;\n+            index += 1;\n+            if (btn) {\n+                btn.innerHTML = '<span class=\"spinner-border spinner-border-sm me-2\"></span>Procesando ' + current + '/' + files.length + '…';\n+            }\n+\n+            return uploadSingleFile(file, docType).then(function (data) {\n+                if (!data.ok) {\n+                    debugLog('Error upload', file.name + ': ' + (data.message || 'error'));\n+                    errors.push({ name: file.name, message: data.message || 'Error' });\n+                    if (files.length === 1) {\n+                        openDebugPanel();\n+                        showResult(formatErrorMessage(data));\n+                        return Promise.resolve();\n+                    }\n+                    return processNext();\n+                }\n+\n+                okCount += 1;\n+                debugLog('Upload OK', file.name);\n+                if (data.extracted) {\n+                    lastExtracted = data.extracted;\n+                }\n+                return processNext();\n+            }).catch(function (err) {\n+                debugLog('Fetch error', file.name + ': ' + (err && err.message ? err.message : 'desconocido'));\n+                errors.push({ name: file.name, message: err && err.message ? err.message : 'Error de red' });\n+                if (files.length === 1) {\n+                    openDebugPanel();\n+                    showResult(formatErrorMessage({\n+                        message: 'Error de conexión con el servidor.',\n+                        hint: err && err.message ? err.message : ''\n+                    }));\n+                    return Promise.resolve();\n+                }\n+                return processNext();\n+            });\n+        }\n+\n+        return processNext();\n+    }\n+\n     function bindManualButton() {\n         var btn = document.getElementById('manual-btn');\n         if (!btn) {\n@@ -401,17 +588,15 @@\n         form.addEventListener('submit', function (event) {\n             event.preventDefault();\n             var btn = document.getElementById('upload-btn');\n-            var uploadFile = resolvedUploadFile();\n-            var formData = new FormData(form);\n-            formData.append('action', 'upload');\n-            if (uploadFile) {\n-                formData.set('image', uploadFile, uploadFile.name || 'documento.jpg');\n-            }\n+            var files = getSelectedFiles();\n+            var docType = document.getElementById('doc-type').value;\n \n-            if (expenseMode === 'manual' && !uploadFile) {\n+            debugLog('Submit', 'modo=' + expenseMode + ' · docs=' + files.length);\n+\n+            if (expenseMode === 'manual' && files.length === 0) {\n                 var manualData = new FormData();\n                 manualData.append('action', 'manual');\n-                manualData.append('doc_type', document.getElementById('doc-type').value);\n+                manualData.append('doc_type', docType);\n                 postForm(manualData).then(function (data) {\n                     if (data.ok && data.extracted) {\n                         openConfirmModal(data.extracted);\n@@ -422,10 +607,14 @@\n                 return;\n             }\n \n-            if (expenseMode !== 'manual' && !uploadFile) {\n+            if (expenseMode !== 'manual' && files.length === 0) {\n                 showResult(formatErrorMessage({\n                     message: 'Debes elegir un archivo, una foto de galería o hacer una foto.',\n-                    reasons: ['En modo OCR/IA el documento es obligatorio.', 'Usa «Elegir archivo o galería» o «Hacer foto».'],\n+                    reasons: [\n+                        'En modo OCR/IA el documento es obligatorio.',\n+                        'Usa «Elegir archivo o galería» o «Hacer foto».',\n+                        'Puedes añadir varios documentos antes de pulsar escanear.'\n+                    ],\n                     hint: 'Formatos admitidos: JPG, PNG, WEBP, HEIC y PDF.'\n                 }));\n                 return;\n@@ -433,44 +622,11 @@\n \n             btn.disabled = true;\n             btn.innerHTML = '<span class=\"spinner-border spinner-border-sm me-2\"></span>Procesando...';\n-            debugLog('Enviando documento', uploadFile ? uploadFile.name + ' (' + uploadFile.size + ' B)' : 'sin archivo');\n \n-            postForm(formData)\n-                .then(function (data) {\n-                    if (data.debug) {\n-                        debugLog('Respuesta servidor', data.debug);\n-                    }\n-                    if (!data.ok) {\n-                        debugLog('Error upload', data.message);\n-                        openDebugPanel();\n-                        showResult(formatErrorMessage(data));\n-                        return;\n-                    }\n-                    debugLog('Upload OK', data.message || 'analizado');\n-\n-                    if (data.warning) {\n-                        showResult('<div class=\"alert alert-warning\">' + data.warning + '</div>');\n-                    } else if (data.message) {\n-                        showResult('<div class=\"alert alert-success\">' + data.message + '</div>');\n-                    }\n-\n-                    if (data.extracted) {\n-                        openConfirmModal(data.extracted);\n-                    }\n-                })\n-                .catch(function (err) {\n-                    debugLog('Fetch error', err && err.message ? err.message : 'desconocido');\n-                    openDebugPanel();\n-                    showResult(formatErrorMessage({\n-                        message: 'Error de conexión con el servidor.',\n-                        reasons: ['Comprueba que Apache (XAMPP) esté activo.'],\n-                        hint: err && err.message ? err.message : ''\n-                    }));\n-                })\n-                .finally(function () {\n-                    btn.disabled = false;\n-                    updateModeUi(expenseMode);\n-                });\n+            uploadFilesSequentially(files, docType, btn).finally(function () {\n+                btn.disabled = false;\n+                updateModeUi(expenseMode);\n+            });\n         });\n     }\n \n@@ -527,6 +683,7 @@\n         bindUploadForm();\n         bindReviewButtons();\n         bindConfirmActions();\n+        renderSelectedFilesList();\n         debugLog('Página lista', 'modo=' + expenseMode + ' · v=' + JS_VERSION);\n         runDiagnose();\n     });","linesAdded":268,"linesRemoved":111,"status":"pending"},{"id":"ch-2","callId":"tool_ce465abe-ae18-446c-8ee0-c7b404db1a5","path":"Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig\n@@ -30,7 +30,6 @@\n                 </div>\n                 <div class=\"mb-3\" id=\"file-field\">\n                     <label class=\"form-label\">Documento <span class=\"text-muted\" id=\"file-label-hint\">(obligatorio para OCR)</span></label>\n-                    <input type=\"file\" name=\"image\" id=\"file-input-active\" class=\"visually-hidden\" tabindex=\"-1\" aria-hidden=\"true\" accept=\"image/*,.pdf,application/pdf,.heic,.heif\">\n                     <div class=\"d-grid gap-2 mb-2\">\n                         <div class=\"file-picker-wrap\" id=\"pick-file-wrap\">\n                             <span class=\"btn btn-outline-secondary btn-lg w-100\">\n@@ -49,7 +48,9 @@\n                                    aria-label=\"Hacer foto con cámara\">\n                         </div>\n                     </div>\n-                    <div id=\"file-selected-name\" class=\"small text-muted\">Ningún documento seleccionado</div>\n+                    <div id=\"file-empty-hint\" class=\"small text-muted\">Ningún documento seleccionado</div>\n+                    <div id=\"file-selected-list\" class=\"file-selected-list mt-2\" aria-live=\"polite\"></div>\n+                    <p class=\"small text-muted mb-0 mt-2\">Puedes añadir varios documentos por separado; se analizarán uno por uno.</p>\n                 </div>\n                 <div class=\"d-grid gap-2\">\n                     <button type=\"button\" class=\"btn btn-success btn-lg\" id=\"manual-btn\">","linesAdded":3,"linesRemoved":2,"status":"pending"},{"id":"ch-3","callId":"tool_5c7f1319-385d-4bb0-acda-f615f614556","path":"Plugins\\ALimpiezasMobile\\Assets\\CSS\\mobile.css","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\CSS\\mobile.css\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\CSS\\mobile.css\n@@ -167,6 +167,44 @@\n     background-color: rgba(25, 135, 84, 0.08);\n }\n \n+.file-selected-list {\n+    display: flex;\n+    flex-direction: column;\n+    gap: 8px;\n+}\n+\n+.file-selected-item {\n+    display: flex;\n+    align-items: center;\n+    justify-content: space-between;\n+    gap: 10px;\n+    padding: 10px 12px;\n+    background: rgba(25, 135, 84, 0.08);\n+    border: 1px solid rgba(25, 135, 84, 0.25);\n+    border-radius: 10px;\n+}\n+\n+.file-selected-info {\n+    flex: 1;\n+    min-width: 0;\n+}\n+\n+.file-selected-item .file-name {\n+    overflow: hidden;\n+    text-overflow: ellipsis;\n+    white-space: nowrap;\n+}\n+\n+.file-selected-item .btn-remove-file {\n+    flex-shrink: 0;\n+    width: 36px;\n+    height: 36px;\n+    padding: 0;\n+    display: inline-flex;\n+    align-items: center;\n+    justify-content: center;\n+}\n+\n .btn-voice-lg {\n     width: 88px;\n     height: 88px;","linesAdded":38,"linesRemoved":0,"status":"pending"},{"id":"ch-4","callId":"tool_0390a35a-ddff-4dbe-b92f-c3c3bb980d1","path":"Plugins\\ALimpiezasMobile\\Assets\\sw.js","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\sw.js\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\sw.js\n@@ -1,4 +1,4 @@\n-const CACHE_NAME = 'alimpiezas-mobile-v3';\n+const CACHE_NAME = 'alimpiezas-mobile-v4';\n const CORE_ASSETS = [\n     '/ALimpiezas/Dinamic/Assets/CSS/mobile.css',\n     '/ALimpiezas/Dinamic/Assets/JS/mobile.js',","linesAdded":1,"linesRemoved":1,"status":"pending"},{"id":"ch-5","callId":"tool_f624e05f-2c23-4da0-a2aa-c0f158cd3f7","path":"Plugins\\ALimpiezasMobile\\View\\Master\\MobileLayout.html.twig","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\Master\\MobileLayout.html.twig\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\Master\\MobileLayout.html.twig\n@@ -14,7 +14,7 @@\n     <title>{{ fsc.title }}</title>\n     <link rel=\"stylesheet\" href=\"{{ asset('node_modules/bootstrap/dist/css/bootstrap.min.css') }}\">\n     <link rel=\"stylesheet\" href=\"{{ asset('node_modules/@fortawesome/fontawesome-free/css/all.min.css') }}\">\n-    <link rel=\"stylesheet\" href=\"{{ asset('Dinamic/Assets/CSS/mobile.css') }}?v=20260625c\">\n+    <link rel=\"stylesheet\" href=\"{{ asset('Dinamic/Assets/CSS/mobile.css') }}?v=20260625d\">\n     {% block head %}{% endblock %}\n </head>\n <body class=\"mobile-app\">","linesAdded":1,"linesRemoved":1,"status":"pending"},{"id":"ch-6","callId":"tool_5115ef06-54eb-44a9-9787-7fdb61eaedc","path":"Plugins\\ALimpiezasMobile\\View\\Master\\MobileLayout.html.twig","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\Master\\MobileLayout.html.twig\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\Master\\MobileLayout.html.twig\n@@ -67,11 +67,11 @@\n             expenseMode: {{ settings('alimpiezas', 'expense_mode', 'ocr')|json_encode|raw }}\n         };\n     </script>\n-    <script src=\"{{ asset('Dinamic/Assets/JS/help-guide.js') }}?v=20260625c\"></script>\n-    <script src=\"{{ asset('Dinamic/Assets/JS/mobile.js') }}?v=20260625c\"></script>\n+    <script src=\"{{ asset('Dinamic/Assets/JS/help-guide.js') }}?v=20260625d\"></script>\n+    <script src=\"{{ asset('Dinamic/Assets/JS/mobile.js') }}?v=20260625d\"></script>\n     <script>\n         if ('serviceWorker' in navigator) {\n-            navigator.serviceWorker.register('{{ asset('Dinamic/Assets/sw.js') }}?v=20260625c')\n+            navigator.serviceWorker.register('{{ asset('Dinamic/Assets/sw.js') }}?v=20260625d')\n                 .then(function (reg) {\n                     if (reg.waiting) {\n                         reg.waiting.postMessage({ type: 'SKIP_WAITING' });","linesAdded":3,"linesRemoved":3,"status":"pending"},{"id":"ch-7","callId":"tool_c695eb71-cc20-4d75-9588-fa84096ebe4","path":"Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js\n@@ -246,20 +246,22 @@\n             return;\n         }\n \n+        var picked = Array.prototype.slice.call(sourceInput.files);\n         var added = 0;\n         var skipped = 0;\n-        for (var i = 0; i < sourceInput.files.length; i++) {\n-            if (addSelectedFile(sourceInput.files[i])) {\n+\n+        picked.forEach(function (file) {\n+            if (addSelectedFile(file)) {\n                 added += 1;\n             } else {\n                 skipped += 1;\n             }\n-        }\n+        });\n \n         resetPickerInput(sourceInput);\n         debugLog(sourceLabel || 'Archivo', added + ' añadido(s), total=' + selectedFiles.length);\n \n-        if (added === 0 && sourceInput.files.length > 0) {\n+        if (added === 0 && picked.length > 0) {\n             showResult(formatErrorMessage({\n                 message: skipped > 0 ? 'Ese documento ya está en la lista.' : 'No se pudo leer el archivo seleccionado.',\n                 hint: 'Prueba con JPG, PNG, WEBP, HEIC o PDF.'","linesAdded":6,"linesRemoved":4,"status":"pending"},{"id":"ch-8","callId":"tool_92fa8af2-ed23-4016-88d5-244d4f67679","path":"Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php\n@@ -128,16 +128,54 @@\n     private function resolveUploadedFile(string $field): ?UploadedFile\n     {\n         $file = $this->request->files->get($field);\n-        if ($file instanceof UploadedFile) {\n+        if ($file instanceof UploadedFile && $file->isValid()) {\n             return $file;\n         }\n \n         foreach ($this->request->files->getArray($field) as $item) {\n-            if ($item instanceof UploadedFile) {\n+            if ($item instanceof UploadedFile && $item->isValid()) {\n                 return $item;\n             }\n         }\n \n         return null;\n     }\n+\n+    /** @return array<string, mixed> */\n+    private function uploadDebugContext(): array\n+    {\n+        $files = $this->request->files->all();\n+        $summary = [];\n+        foreach ($files as $key => $value) {\n+            if ($value instanceof UploadedFile) {\n+                $summary[$key] = [\n+                    'name' => $value->getClientOriginalName(),\n+                    'size' => $value->getSize(),\n+                    'error' => $value->getError(),\n+                    'valid' => $value->isValid(),\n+                ];\n+                continue;\n+            }\n+            if (is_array($value)) {\n+                $summary[$key] = [];\n+                foreach ($value as $idx => $item) {\n+                    if ($item instanceof UploadedFile) {\n+                        $summary[$key][$idx] = [\n+                            'name' => $item->getClientOriginalName(),\n+                            'size' => $item->getSize(),\n+                            'error' => $item->getError(),\n+                            'valid' => $item->isValid(),\n+                        ];\n+                    }\n+                }\n+            }\n+        }\n+\n+        return [\n+            'files_keys' => array_keys($files),\n+            'files' => $summary,\n+            'content_length' => (int) ($_SERVER['CONTENT_LENGTH'] ?? 0),\n+            'content_type' => (string) ($_SERVER['CONTENT_TYPE'] ?? ''),\n+        ];\n+    }\n }","linesAdded":40,"linesRemoved":2,"status":"pending"},{"id":"ch-9","callId":"tool_f6595e9b-ccff-4e6d-9578-1609b94d8c5","path":"Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php\n@@ -83,10 +83,7 @@\n                 'message' => 'No se recibió ningún documento',\n                 'reasons' => ['Selecciona un archivo, una foto de galería o haz una foto antes de escanear.'],\n                 'hint' => 'Formatos admitidos: JPG, PNG, WEBP, HEIC y PDF.',\n-                'debug' => [\n-                    'files_keys' => array_keys($this->request->files->all()),\n-                    'content_length' => (int) ($_SERVER['CONTENT_LENGTH'] ?? 0),\n-                ],\n+                'debug' => $this->uploadDebugContext(),\n             ];\n         }\n ","linesAdded":1,"linesRemoved":4,"status":"pending"}],"transcriptPath":"C:\\Users\\Paterna\\.cursor\\projects\\c-xampp-htdocs-ALimpiezas\\agent-transcripts\\736ac9a4-90f0-4491-8940-56ec149befda\\736ac9a4-90f0-4491-8940-56ec149befda.jsonl","notifyTitle":"Agente terminado","notifyBody":"Continúo el hilo: reviso los archivos móvil ya identificados para corregir la validación tras subir y añadir soporte de "},"at":1782350049917}]}
