document.addEventListener('DOMContentLoaded', () => { // --- DOM Elements --- console.log("Maquetador Script v1.1 - Rate Limit Fix Loaded"); const candidateList = document.getElementById('candidate-list'); const addCandidateBtn = document.getElementById('add-candidate-btn'); const processAllBtn = document.getElementById('process-all-btn'); const candidateTemplate = document.getElementById('candidate-template'); const signOutBtn = document.getElementById('sign-out-btn'); // --- State Management --- let candidateIdCounter = 0; const processedData = new Map(); // --- REMOVED: TEMPLATE_FORMATS object --- // --- Auth Helper --- const getAuthToken = async () => { const auth = window.firebaseAuth; if (auth && auth.currentUser) { try { return await auth.currentUser.getIdToken(true); // Force refresh } catch (error) { console.error("Error getting auth token:", error); if (error.code === 'auth/user-token-expired') { window.location.href = 'login.html'; } return null; } } return null; }; // --- REMOVED: updateFormatDropdown function --- // --- Core Functions --- const updateFormatSelectForRow = (row) => { const templateSelect = row.querySelector('.template-select'); const formatSelect = row.querySelector('.format-select'); if (!templateSelect || !formatSelect) { return; } const templateValue = templateSelect.value; const pdfOption = formatSelect.querySelector('option[value="pdf"]'); const docxOption = formatSelect.querySelector('option[value="docx"]'); if (templateValue === 'capgemini') { // Enable the entire dropdown and all options for Capgemini formatSelect.disabled = false; if (pdfOption) pdfOption.disabled = false; if (docxOption) docxOption.disabled = false; // Ensure a valid value is selected if (!['pdf', 'docx'].includes(formatSelect.value)) { formatSelect.value = 'docx'; } } else { // For non-Capgemini templates: keep dropdown enabled but disable PDF option formatSelect.disabled = false; if (pdfOption) pdfOption.disabled = true; if (docxOption) docxOption.disabled = false; // Force DOCX selection for non-Capgemini templates if (formatSelect.value === 'pdf') { formatSelect.value = 'docx'; } } }; const addCandidateRow = () => { const newRow = candidateTemplate.content.cloneNode(true).firstElementChild; const uniqueId = `row-${candidateIdCounter++}`; newRow.dataset.id = uniqueId; newRow.dataset.needsProcessing = "true"; newRow.classList.add('needs-processing'); const fileInput = newRow.querySelector('.cv-pdf-input'); const fileLabel = newRow.querySelector('.file-upload-label'); const inputId = `${uniqueId}-file-input`; fileInput.id = inputId; fileLabel.setAttribute('for', inputId); // --- Add unique ID for the soft skills toggle --- const softSkillsToggle = newRow.querySelector('.soft-skills-toggle'); const softSkillsLabel = newRow.querySelector('.soft-skills-label'); const softSkillsId = `${uniqueId}-soft-skills-toggle`; softSkillsToggle.id = softSkillsId; // The label's `for` now points to the checkbox ID softSkillsLabel.setAttribute('for', softSkillsId); candidateList.appendChild(newRow); updateFormatSelectForRow(newRow); // --- REMOVED: call to updateFormatDropdown --- checkProcessAllButtonState(); }; /** Processes a single candidate row */ const processSingleCandidate = async (row) => { const rowId = row.dataset.id; const fileInput = row.querySelector('.cv-pdf-input'); const statusIndicator = row.querySelector('.status-indicator'); const actionButtons = row.querySelector('.action-buttons'); const languageSelect = row.querySelector('.language-select'); const selectedLanguage = languageSelect.value; if (!fileInput.files || fileInput.files.length === 0) { const storedInfo = processedData.get(rowId); if (!storedInfo || !storedInfo.originalFileName) { statusIndicator.innerHTML = `Sin Archivo`; return 'skipped'; } } const token = await getAuthToken(); if (!token) { statusIndicator.innerHTML = `Auth Error`; console.error("Your session has expired. Please sign in again."); window.location.href = 'login.html'; return 'error'; } console.log(`[script.js] Processing Row ID: ${rowId} for language: ${selectedLanguage}`); statusIndicator.innerHTML = '
Procesando...'; actionButtons.innerHTML = ''; row.dataset.status = 'processing'; row.dataset.needsProcessing = "false"; row.classList.remove('needs-processing'); const formData = new FormData(); if (fileInput.files.length > 0) { formData.append('cvPdf', fileInput.files[0]); } else { statusIndicator.innerHTML = `File error`; console.error("Cannot re-process without a file. Please re-select the file."); return 'error'; } formData.append('language', selectedLanguage); try { const response = await fetch('/api/generate-cv', { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: formData, }); if (!response.ok) { const status = response.status; let errorMsg = 'Processing failed.'; try { const errorResult = await response.json(); // FastAPI detail or a generic error string errorMsg = errorResult.detail || errorResult.error || JSON.stringify(errorResult); } catch (e) { errorMsg = response.statusText || errorMsg; } if (status === 401 || status === 403) { throw new Error('Authentication failed. Please log in again.'); } // Aggressive check for Rate Limit if (status === 429 || errorMsg.toLowerCase().includes('limite')) { const err = new Error('limite de uso alcanzado'); err.isRateLimit = true; throw err; } if (status === 413) { throw new Error('Archivo demasiado grande (Server Limit)'); } throw new Error(errorMsg); } const data = await response.json(); const storedInfo = processedData.get(rowId); if (storedInfo) { storedInfo.processedLanguages.set(selectedLanguage, { data: data }); } else { processedData.set(rowId, { originalFileName: fileInput.files[0].name, processedLanguages: new Map() }); processedData.get(rowId).processedLanguages.set(selectedLanguage, { data: data }); } statusIndicator.innerHTML = `Procesado (${selectedLanguage.toUpperCase()})`; const downloadBtn = document.createElement('button'); downloadBtn.innerHTML = ` `; downloadBtn.className = 'download-btn px-6 py-2 text-white bg-green-600 rounded-md hover:bg-green-700 flex items-center justify-center font-medium shadow-sm transition-colors'; actionButtons.appendChild(downloadBtn); row.dataset.status = 'success'; return 'success'; } catch (error) { console.error("Processing failed for row:", rowId, error); if (error.isRateLimit || error.message.includes('limite de uso alcanzado')) { statusIndicator.innerHTML = `limite de uso alcanzado`; } else { statusIndicator.innerHTML = `Error`; } row.dataset.status = 'error'; row.dataset.needsProcessing = "true"; row.classList.add('needs-processing'); if (error.message.includes('Authentication failed')) { window.location.href = 'login.html'; } return 'error'; } finally { checkProcessAllButtonState(); } }; const processAllCandidates = async () => { const rowsToProcess = candidateList.querySelectorAll('.candidate-row[data-needs-processing="true"]'); if (rowsToProcess.length === 0) return; processAllBtn.disabled = true; processAllBtn.textContent = `Procesando ${rowsToProcess.length}...`; for (const row of rowsToProcess) { await processSingleCandidate(row); } processAllBtn.textContent = 'Procesar Nuevos CVs'; checkProcessAllButtonState(); }; /** * --- UPDATED: handleDownload --- * Removed format dropdown logic, kept soft skills toggle logic. */ const handleDownload = async (row) => { const rowId = row.dataset.id; const storedInfo = processedData.get(rowId); const templateSelect = row.querySelector('.template-select'); const languageSelect = row.querySelector('.language-select'); const softSkillsToggle = row.querySelector('.soft-skills-toggle'); const formatSelect = row.querySelector('.format-select'); const selectedTemplateName = templateSelect.value; const selectedLanguage = languageSelect.value; const downloadBtn = row.querySelector('.download-btn'); if (!storedInfo || !storedInfo.processedLanguages.has(selectedLanguage) || !downloadBtn || !selectedTemplateName) { console.error('Cannot download: Missing processed data or template selection.'); return; } const token = await getAuthToken(); if (!token) { console.error("Session expired. Please sign in again."); window.location.href = 'login.html'; return; } const cvData = storedInfo.processedLanguages.get(selectedLanguage).data; const processedLanguage = selectedLanguage; // --- Create a deep copy to modify --- const cvDataToSend = JSON.parse(JSON.stringify(cvData)); if (softSkillsToggle.checked) { if (cvDataToSend.skills && cvDataToSend.skills.softSkills) { console.log(`[script.js] Row ${rowId}: Removing soft skills.`); cvDataToSend.skills.softSkills = []; // Empty the array } } // --- Handle Manual Position Override --- const positionInput = row.querySelector('.position-input'); if (positionInput && positionInput.value.trim() !== '') { console.log(`[script.js] Row ${rowId}: Overriding position with "${positionInput.value.trim()}"`); if (!cvDataToSend.profile) { cvDataToSend.profile = {}; } cvDataToSend.profile.position = positionInput.value.trim(); } console.log(JSON.stringify(cvDataToSend, null, 2)); downloadBtn.disabled = true; downloadBtn.innerHTML = '
'; const selectedFormat = (selectedTemplateName === 'capgemini' && formatSelect) ? (formatSelect.value === 'pdf' ? 'pdf' : 'docx') : 'docx'; const fileExtension = selectedFormat === 'docx' ? '.docx' : '.pdf'; try { const response = await fetch('/api/download-pdf', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ cvData: cvDataToSend, // Send the modified copy templateName: selectedTemplateName, language: processedLanguage, format: selectedFormat // Send the format we determined }), }); if (!response.ok) { if (response.status === 401 || response.status === 403) { throw new Error('Authentication failed. Please log in again.'); } const errorData = await response.json(); throw new Error(errorData.error || 'File generation failed.'); } const fileBlob = await response.blob(); const url = window.URL.createObjectURL(fileBlob); const a = document.createElement('a'); a.style.display = 'none'; a.href = url; // --- Helper to sanitize filename --- const sanitizeFilename = (str) => { if (!str) return "candidate"; return str.replace(/[^\x00-\x7F]/g, ""); // Remove non-ASCII chars } const rawCandidateName = cvDataToSend.profile?.name || 'Candidate'; const safeCandidateName = sanitizeFilename(rawCandidateName); a.download = `CV_${safeCandidateName.replace(/ /g, '_')}_${selectedTemplateName}_${processedLanguage}${fileExtension}`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); a.remove(); } catch (error) { console.error('Download failed:', error); if (error.message.includes('Authentication failed')) { window.location.href = 'login.html'; } } finally { downloadBtn.disabled = false; downloadBtn.innerHTML = ` `; } }; const checkProcessAllButtonState = () => { const rowsNeedingProcessing = candidateList.querySelectorAll('.candidate-row[data-needs-processing="true"]'); processAllBtn.disabled = rowsNeedingProcessing.length === 0; }; const handleSignOut = async () => { const auth = window.firebaseAuth; if (auth) { try { await auth.signOut(); window.location.href = 'login.html'; } catch (error) { console.error("Sign out error:", error); } } }; // --- Event Listeners --- addCandidateBtn.addEventListener('click', addCandidateRow); processAllBtn.addEventListener('click', processAllCandidates); signOutBtn.addEventListener('click', handleSignOut); candidateList.addEventListener('click', (event) => { const target = event.target; const row = target.closest('.candidate-row'); if (!row) return; if (target.closest('.remove-btn')) { processedData.delete(row.dataset.id); row.remove(); checkProcessAllButtonState(); return; } if (target.closest('.download-btn')) { handleDownload(row); return; } if (target.closest('.process-single-btn')) { processSingleCandidate(row); return; } }); /** * --- UPDATED: Main 'change' listener --- * Removed all 'format-select' logic. */ candidateList.addEventListener('change', (event) => { const target = event.target; const row = target.closest('.candidate-row'); if (!row) return; const rowId = row.dataset.id; let needsReprocessCheck = false; if (target.matches('.cv-pdf-input')) { const label = row.querySelector('.file-upload-label'); // Changed from .file-name-display row.dataset.needsProcessing = "true"; row.classList.add('needs-processing'); row.dataset.status = 'pending'; row.querySelector('.status-indicator').innerHTML = ''; row.querySelector('.action-buttons').innerHTML = ''; if (target.files.length > 0) { const file = target.files[0]; const maxSize = 50 * 1024 * 1024; // 50MB if (file.size >= maxSize) { // Show popup as requested alert("El archivo seleccionado es demasiado grande. El lĂ­mite es de 50MB."); target.value = ''; // Clear input processedData.delete(rowId); label.textContent = 'Seleccionar PDF'; label.removeAttribute('title'); row.querySelector('.status-indicator').innerHTML = `Archivo demasiado grande (Max 50MB)`; // Prevent processing row.dataset.needsProcessing = "false"; row.classList.remove('needs-processing'); checkProcessAllButtonState(); return; } const originalFileName = file.name; processedData.set(rowId, { originalFileName: originalFileName, processedLanguages: new Map() }); label.textContent = originalFileName; // Update label label.title = originalFileName; // Add tooltip } else { processedData.delete(rowId); label.textContent = 'Seleccionar PDF'; // Reset label label.removeAttribute('title'); } checkProcessAllButtonState(); } else if (target.matches('.template-select')) { updateFormatSelectForRow(row); needsReprocessCheck = true; // Mark for reprocess check } else if (target.matches('.format-select')) { // Safeguard: Ensure PDF cannot be selected for non-Capgemini templates const templateSelect = row.querySelector('.template-select'); const formatSelect = row.querySelector('.format-select'); if (templateSelect && formatSelect && templateSelect.value !== 'capgemini' && formatSelect.value === 'pdf') { formatSelect.value = 'docx'; } } else if (target.matches('.language-select')) { needsReprocessCheck = true; // Mark for reprocess check } else if (target.matches('.soft-skills-toggle')) { // Do nothing, selection is read at download time. } // --- Run Reprocess Check if needed --- if (needsReprocessCheck) { const storedInfo = processedData.get(rowId); const currentLanguage = row.querySelector('.language-select').value; // --- FIX: This condition is updated. --- // It now checks if any data exists and the row is not mid-process, // instead of requiring the status to be 'success'. if (storedInfo && storedInfo.processedLanguages.size > 0 && row.dataset.status !== 'processing') { if (storedInfo.processedLanguages.has(currentLanguage)) { // YES! Show download button. row.dataset.needsProcessing = "false"; row.classList.remove('needs-processing'); row.dataset.status = 'success'; row.querySelector('.status-indicator').innerHTML = `Procesado (${currentLanguage.toUpperCase()})`; const actionButtons = row.querySelector('.action-buttons'); actionButtons.innerHTML = ''; const downloadBtn = document.createElement('button'); downloadBtn.innerHTML = ` `; downloadBtn.className = 'download-btn px-6 py-2 text-white bg-green-600 rounded-md hover:bg-green-700 flex items-center justify-center font-medium shadow-sm transition-colors'; actionButtons.appendChild(downloadBtn); } else { // NO. Show reprocess button. row.dataset.needsProcessing = "true"; row.classList.add('needs-processing'); row.dataset.status = 'pending-reprocess'; // --- Using your reported text for consistency --- row.querySelector('.status-indicator').innerHTML = `Necesario Reprocesar`; const actionButtons = row.querySelector('.action-buttons'); actionButtons.innerHTML = ''; const processBtn = document.createElement('button'); processBtn.textContent = 'Process'; processBtn.className = 'process-single-btn px-3 py-1 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700'; actionButtons.appendChild(processBtn); } } else if (row.dataset.status !== 'processing') { if (processedData.has(rowId) && processedData.get(rowId).originalFileName) { row.dataset.needsProcessing = "true"; row.classList.add('needs-processing'); } } checkProcessAllButtonState(); } }); // --- Initial State --- if (candidateList.children.length === 0) { addCandidateRow(); } checkProcessAllButtonState(); // Check state on load });