diff --git a/css/styles.css b/css/styles.css index a4ae0bc..e351dd8 100644 --- a/css/styles.css +++ b/css/styles.css @@ -475,6 +475,394 @@ input.invalid { } } +/* Tab Navigation */ +.tab-navigation { + display: flex; + gap: 0; + margin-top: 20px; + border-bottom: 2px solid #FFC407; +} + +.tab { + padding: 12px 24px; + background-color: transparent; + color: #000000; + text-decoration: none; + font-weight: 600; + font-size: 14px; + border: none; + border-bottom: 3px solid transparent; + cursor: pointer; + transition: all 0.3s ease; + margin-bottom: -2px; +} + +.tab:hover { + background-color: rgba(255, 196, 7, 0.1); +} + +.tab.active { + color: #FFC407; + border-bottom-color: #FFC407; + background-color: rgba(255, 196, 7, 0.05); +} + +/* Global to Local Styles */ +.g2l-container { + max-width: 1200px; + margin: 0 auto; +} + +.upload-section, +.progress-section, +.preview-section { + background-color: #ffffff; + padding: 40px; + border-radius: 10px; + box-shadow: 0 4px 6px rgba(255, 196, 7, 0.3); + margin-bottom: 30px; +} + +.upload-section h2, +.progress-section h2, +.preview-section h2 { + color: #000000; + font-size: 22px; + margin-bottom: 10px; +} + +.section-description { + color: #666; + font-size: 14px; + margin-bottom: 30px; +} + +/* Upload Dropzone */ +.upload-dropzone { + border: 3px dashed #FFC407; + border-radius: 10px; + padding: 60px 40px; + text-align: center; + cursor: pointer; + transition: all 0.3s ease; + margin-bottom: 20px; +} + +.upload-dropzone:hover { + border-color: #000000; + background-color: rgba(255, 196, 7, 0.05); +} + +.upload-dropzone.dragover { + border-color: #000000; + background-color: rgba(255, 196, 7, 0.1); + transform: scale(1.02); +} + +.dropzone-text { + color: #000000; + font-size: 16px; + font-weight: 600; + margin-top: 20px; +} + +.dropzone-limit { + color: #999; + font-size: 12px; + margin-top: 8px; +} + +/* File Info Display */ +.file-info { + display: flex; + align-items: center; + gap: 15px; + padding: 15px; + background-color: #f9f9f9; + border: 2px solid #FFC407; + border-radius: 8px; + margin-bottom: 20px; +} + +.file-icon { + font-size: 32px; +} + +.file-details { + flex: 1; +} + +.file-name { + font-weight: 600; + color: #000000; + font-size: 14px; + margin-bottom: 4px; +} + +.file-size { + color: #666; + font-size: 12px; +} + +.remove-file-btn { + background: none; + border: none; + color: #ff4444; + font-size: 24px; + cursor: pointer; + padding: 5px 10px; + transition: transform 0.2s ease; +} + +.remove-file-btn:hover { + transform: scale(1.2); +} + +/* Progress Tracker */ +.progress-tracker { + display: flex; + flex-direction: column; + gap: 15px; +} + +.progress-step { + display: flex; + align-items: flex-start; + gap: 15px; + padding: 15px; + background-color: #f9f9f9; + border-radius: 8px; + border-left: 4px solid #ddd; + transition: all 0.3s ease; +} + +.progress-step.pending { + border-left-color: #ddd; + opacity: 0.6; +} + +.progress-step.processing { + border-left-color: #FFC407; + background-color: rgba(255, 196, 7, 0.1); + animation: pulse 2s infinite; +} + +.progress-step.success { + border-left-color: #4CAF50; + background-color: rgba(76, 175, 80, 0.05); +} + +.progress-step.warning { + border-left-color: #ff9800; + background-color: rgba(255, 152, 0, 0.05); +} + +.progress-step.error { + border-left-color: #ff4444; + background-color: rgba(255, 68, 68, 0.05); +} + +.step-icon { + font-size: 24px; + min-width: 30px; + text-align: center; +} + +.step-content { + flex: 1; +} + +.step-title { + font-weight: 700; + color: #000000; + font-size: 15px; + margin-bottom: 4px; +} + +.step-message { + color: #666; + font-size: 13px; +} + +/* Alert Cards */ +#alertsContainer { + margin-top: 30px; +} + +.alert-card { + padding: 20px; + border-radius: 8px; + margin-bottom: 15px; + border-left: 5px solid; +} + +.alert-card.error { + background-color: rgba(255, 68, 68, 0.1); + border-left-color: #ff4444; +} + +.alert-card.warning { + background-color: rgba(255, 152, 0, 0.1); + border-left-color: #ff9800; +} + +.alert-card.success { + background-color: rgba(76, 175, 80, 0.1); + border-left-color: #4CAF50; +} + +.alert-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 12px; +} + +.alert-icon { + font-size: 24px; +} + +.alert-title { + font-weight: 700; + font-size: 16px; + color: #000000; +} + +.alert-message { + color: #333; + font-size: 14px; + margin-bottom: 10px; + line-height: 1.5; +} + +.alert-details { + background-color: rgba(0, 0, 0, 0.05); + padding: 10px; + border-radius: 5px; + font-family: 'Courier New', monospace; + font-size: 12px; + color: #333; + margin-bottom: 10px; + overflow-x: auto; +} + +.alert-action { + color: #000000; + font-weight: 600; + font-size: 13px; + margin-top: 10px; +} + +.alert-action::before { + content: '→ '; + color: #FFC407; + font-weight: 700; +} + +/* Preview Styles */ +.preview-summary { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 20px; + margin-bottom: 30px; +} + +.summary-card { + background-color: #f9f9f9; + padding: 20px; + border-radius: 8px; + border-left: 4px solid #FFC407; +} + +.summary-label { + font-size: 12px; + color: #666; + text-transform: uppercase; + margin-bottom: 8px; + font-weight: 600; +} + +.summary-value { + font-size: 24px; + color: #000000; + font-weight: 700; +} + +.preview-table-container { + max-height: 500px; + overflow: auto; + border: 2px solid #FFC407; + border-radius: 8px; + margin-bottom: 30px; +} + +.preview-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +.preview-table th { + position: sticky; + top: 0; + background-color: #000000; + color: #FFC407; + padding: 12px 10px; + text-align: left; + font-weight: 700; + border-bottom: 2px solid #FFC407; + z-index: 10; +} + +.preview-table td { + padding: 10px; + border-bottom: 1px solid #eee; + color: #000000; +} + +.preview-table tr:hover { + background-color: rgba(255, 196, 7, 0.05); +} + +.preview-actions { + display: flex; + gap: 15px; + justify-content: flex-end; +} + +.action-btn { + padding: 12px 24px; + border-radius: 5px; + font-family: 'Montserrat', sans-serif; + font-size: 14px; + font-weight: 700; + cursor: pointer; + transition: all 0.3s ease; + border: 2px solid; +} + +.download-btn { + background-color: #ffffff; + color: #000000; + border-color: #FFC407; +} + +.download-btn:hover { + background-color: #FFC407; + color: #000000; +} + +.cancel-btn { + background-color: #ffffff; + color: #ff4444; + border-color: #ff4444; +} + +.cancel-btn:hover { + background-color: #ff4444; + color: #ffffff; +} + /* Responsive Design */ @media (max-width: 1024px) { .main-container { @@ -484,6 +872,14 @@ input.invalid { .preview-column { order: -1; } + + .preview-actions { + flex-direction: column; + } + + .action-btn { + width: 100%; + } } @media (max-width: 768px) { @@ -497,4 +893,17 @@ input.invalid { .preview-column { padding: 20px; } + + .tab-navigation { + width: 100%; + } + + .tab { + flex: 1; + text-align: center; + } + + .preview-summary { + grid-template-columns: 1fr; + } } diff --git a/global-to-local.php b/global-to-local.php new file mode 100644 index 0000000..50c343a --- /dev/null +++ b/global-to-local.php @@ -0,0 +1,140 @@ +requireAuth(); + +$pageTitle = 'Global to Local - L\'Oréal Box'; + +// Include shared header +require_once __DIR__ . '/header.php'; +?> + +
+ +
+

Upload Global Campaign CSV

+

Upload a global campaign CSV file to transform it into regional market CSVs.

+ +
+
+ + + + +

Drag & drop CSV file here or click to browse

+

Maximum file size: 5MB

+ +
+ + + + +
+
+ + + + + + +
+ + + + + + diff --git a/header.php b/header.php new file mode 100644 index 0000000..d29fecd --- /dev/null +++ b/header.php @@ -0,0 +1,34 @@ + + + + + + + <?php echo $pageTitle ?? 'L\'Oréal Box Asset Submission'; ?> + + + + + diff --git a/index.php b/index.php index 66cbd1e..814858d 100644 --- a/index.php +++ b/index.php @@ -1,7 +1,7 @@ requireAuth(); $ssoEnabled = $auth->isSSOEnabled(); + +$pageTitle = 'Asset Submission - L\'Oréal Box'; + +// Include shared header +require_once __DIR__ . '/header.php'; ?> - - - - - - L'Oréal Box Asset Submission - - - - -
diff --git a/js/global-to-local.js b/js/global-to-local.js new file mode 100644 index 0000000..fb15535 --- /dev/null +++ b/js/global-to-local.js @@ -0,0 +1,498 @@ +/** + * Global to Local CSV Transformation - Frontend + */ + +// DOM Elements +const dropzone = document.getElementById('dropzone'); +const csvFileInput = document.getElementById('csvFile'); +const fileInfo = document.getElementById('fileInfo'); +const fileName = document.getElementById('fileName'); +const fileSize = document.getElementById('fileSize'); +const removeFileBtn = document.getElementById('removeFile'); +const csvUploadForm = document.getElementById('csvUploadForm'); +const processBtn = document.getElementById('processBtn'); + +const uploadSection = document.getElementById('uploadSection'); +const progressSection = document.getElementById('progressSection'); +const previewSection = document.getElementById('previewSection'); +const progressTracker = document.getElementById('progressTracker'); +const alertsContainer = document.getElementById('alertsContainer'); + +const previewSummary = document.getElementById('previewSummary'); +const previewTable = document.getElementById('previewTable'); +const downloadPreviewBtn = document.getElementById('downloadPreviewBtn'); +const cancelBtn = document.getElementById('cancelBtn'); +const uploadToBoxBtn = document.getElementById('uploadToBoxBtn'); + +// State +let selectedFile = null; +let processedData = null; + +/** + * Initialize + */ +function init() { + // Dropzone click + dropzone.addEventListener('click', () => csvFileInput.click()); + + // File selection + csvFileInput.addEventListener('change', handleFileSelect); + + // Remove file + removeFileBtn.addEventListener('click', removeFile); + + // Drag & drop + dropzone.addEventListener('dragover', handleDragOver); + dropzone.addEventListener('dragleave', handleDragLeave); + dropzone.addEventListener('drop', handleDrop); + + // Form submission + csvUploadForm.addEventListener('submit', handleFormSubmit); + + // Preview actions + downloadPreviewBtn.addEventListener('click', downloadPreview); + cancelBtn.addEventListener('click', resetForm); + uploadToBoxBtn.addEventListener('click', uploadToBox); +} + +/** + * Handle file selection + */ +function handleFileSelect(e) { + const file = e.target.files[0]; + if (file) { + displayFileInfo(file); + } +} + +/** + * Handle drag over + */ +function handleDragOver(e) { + e.preventDefault(); + dropzone.classList.add('dragover'); +} + +/** + * Handle drag leave + */ +function handleDragLeave(e) { + e.preventDefault(); + dropzone.classList.remove('dragover'); +} + +/** + * Handle drop + */ +function handleDrop(e) { + e.preventDefault(); + dropzone.classList.remove('dragover'); + + const file = e.dataTransfer.files[0]; + if (file) { + // Set the file to the input + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(file); + csvFileInput.files = dataTransfer.files; + + displayFileInfo(file); + } +} + +/** + * Display file information + */ +function displayFileInfo(file) { + selectedFile = file; + + fileName.textContent = file.name; + fileSize.textContent = formatFileSize(file.size); + + dropzone.style.display = 'none'; + fileInfo.style.display = 'flex'; + processBtn.disabled = false; +} + +/** + * Remove selected file + */ +function removeFile() { + selectedFile = null; + csvFileInput.value = ''; + + dropzone.style.display = 'block'; + fileInfo.style.display = 'none'; + processBtn.disabled = true; +} + +/** + * Format file size + */ +function formatFileSize(bytes) { + if (bytes >= 1048576) { + return (bytes / 1048576).toFixed(2) + ' MB'; + } else if (bytes >= 1024) { + return (bytes / 1024).toFixed(2) + ' KB'; + } else { + return bytes + ' bytes'; + } +} + +/** + * Handle form submission + */ +async function handleFormSubmit(e) { + e.preventDefault(); + + if (!selectedFile) { + showAlert('error', 'No File Selected', 'Please select a CSV file to upload.'); + return; + } + + // Hide upload section, show progress + uploadSection.style.display = 'none'; + progressSection.style.display = 'block'; + previewSection.style.display = 'none'; + + // Clear previous alerts + alertsContainer.innerHTML = ''; + + // Prepare form data + const formData = new FormData(); + formData.append('csvFile', selectedFile); + + // Process CSV + try { + const response = await fetch('process-csv.php', { + method: 'POST', + body: formData + }); + + const result = await response.json(); + + // Update progress for each stage + if (result.progress) { + result.progress.forEach(updateProgress); + } + + if (result.success) { + // Success - show preview + processedData = result.data; + updateProgress({ stage: 'complete', status: 'success', message: 'Processing complete' }); + + // Show warnings if any + if (result.data.warnings && result.data.warnings.length > 0) { + showWarnings(result.data.warnings); + } + + // Show preview after brief delay + setTimeout(() => { + showPreview(result.data); + }, 500); + + } else { + // Error occurred + updateProgress({ + stage: result.stage, + status: 'error', + message: result.error || 'Processing failed' + }); + + showError(result); + } + + } catch (error) { + console.error('Processing error:', error); + showAlert('error', 'Processing Failed', 'An unexpected error occurred. Please try again.'); + } +} + +/** + * Update progress step + */ +function updateProgress(step) { + const stepEl = progressTracker.querySelector(`[data-stage="${step.stage}"]`); + if (!stepEl) return; + + // Remove all status classes + stepEl.classList.remove('pending', 'processing', 'success', 'warning', 'error'); + + // Add new status + stepEl.classList.add(step.status); + + // Update icon + const iconEl = stepEl.querySelector('.step-icon'); + const messageEl = stepEl.querySelector('.step-message'); + + switch (step.status) { + case 'processing': + iconEl.textContent = '🔄'; + break; + case 'success': + iconEl.textContent = '✅'; + break; + case 'warning': + iconEl.textContent = '⚠️'; + break; + case 'error': + iconEl.textContent = '❌'; + break; + default: + iconEl.textContent = '⏸️'; + } + + // Update message + messageEl.textContent = step.message || ''; +} + +/** + * Show error alert + */ +function showError(result) { + const errorHtml = ` +
+
+ + ${escapeHtml(result.error || 'Error')} +
+ ${result.details ? ` +
${escapeHtml(result.details)}
+ ` : ''} + ${result.details && result.details.length > 100 ? ` +
${escapeHtml(result.details)}
+ ` : ''} + ${result.action ? ` +
${escapeHtml(result.action)}
+ ` : ''} + ${result.endpoint ? ` +
Endpoint: ${escapeHtml(result.endpoint)}
+ ` : ''} +
+ `; + + alertsContainer.innerHTML = errorHtml; +} + +/** + * Show warnings + */ +function showWarnings(warnings) { + if (warnings.length === 0) return; + + let warningHtml = ` +
+
+ ⚠️ + Data Quality Issues (${warnings.length}) +
+
Found ${warnings.length} warning(s) in the data:
+ +
These rows will be processed but may have issues downstream.
+
+ `; + + alertsContainer.innerHTML += warningHtml; +} + +/** + * Show alert + */ +function showAlert(type, title, message) { + const iconMap = { + error: '❌', + warning: '⚠️', + success: '✅' + }; + + const alertHtml = ` +
+
+ ${iconMap[type]} + ${escapeHtml(title)} +
+
${escapeHtml(message)}
+
+ `; + + alertsContainer.innerHTML = alertHtml; +} + +/** + * Show preview + */ +function showPreview(data) { + progressSection.style.display = 'none'; + previewSection.style.display = 'block'; + + // Show summary + previewSummary.innerHTML = ` +
+
Input Rows
+
${data.inputRows}
+
+
+
Output Rows
+
${data.outputRows}
+
+
+
Campaign Number
+
${escapeHtml(data.campaignNumber)}
+
+
+
Business Unit
+
${escapeHtml(data.businessUnit)}
+
+
+
Output Filename
+
${escapeHtml(data.filename)}
+
+ `; + + // Build table + const preview = data.preview; + if (preview.length > 0) { + const headers = Object.keys(preview[0]); + + let tableHtml = ''; + headers.forEach(header => { + tableHtml += `${escapeHtml(header)}`; + }); + tableHtml += ''; + + preview.forEach(row => { + tableHtml += ''; + headers.forEach(header => { + tableHtml += `${escapeHtml(row[header] || '')}`; + }); + tableHtml += ''; + }); + + tableHtml += ''; + previewTable.innerHTML = tableHtml; + } +} + +/** + * Download preview CSV + */ +function downloadPreview() { + if (!processedData || !processedData.csvContent) { + alert('No data to download'); + return; + } + + const blob = new Blob([processedData.csvContent], { type: 'text/csv' }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = processedData.filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); +} + +/** + * Upload to Box + */ +async function uploadToBox() { + uploadToBoxBtn.disabled = true; + uploadToBoxBtn.textContent = 'Uploading to Box...'; + + try { + const response = await fetch('upload-to-box.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }); + + const result = await response.json(); + + if (result.success) { + // Success + showAlert('success', 'Upload Complete', + `CSV uploaded successfully to Box!\n\nFile: ${result.data.filename}\nRows: ${result.data.rowCount}` + ); + + // Show Box link + if (result.data.fileUrl) { + alertsContainer.innerHTML += ` +
+ + View File in Box + +
+ `; + } + + // Reset after 3 seconds + setTimeout(resetForm, 3000); + + } else { + // Error + showAlert('error', result.error || 'Upload Failed', result.details || ''); + uploadToBoxBtn.disabled = false; + uploadToBoxBtn.textContent = 'Approve & Upload to Box'; + } + + } catch (error) { + console.error('Upload error:', error); + showAlert('error', 'Upload Failed', 'An error occurred while uploading to Box.'); + uploadToBoxBtn.disabled = false; + uploadToBoxBtn.textContent = 'Approve & Upload to Box'; + } +} + +/** + * Reset form + */ +function resetForm() { + // Clear file + removeFile(); + + // Hide sections + progressSection.style.display = 'none'; + previewSection.style.display = 'none'; + uploadSection.style.display = 'block'; + + // Clear data + processedData = null; + + // Reset progress steps + const steps = progressTracker.querySelectorAll('.progress-step'); + steps.forEach(step => { + step.classList.remove('processing', 'success', 'warning', 'error'); + step.classList.add('pending'); + step.querySelector('.step-icon').textContent = '⏸️'; + step.querySelector('.step-message').textContent = 'Pending'; + }); + + // Clear alerts + alertsContainer.innerHTML = ''; +} + +/** + * Escape HTML + */ +function escapeHtml(text) { + if (text === null || text === undefined) return ''; + const div = document.createElement('div'); + div.textContent = String(text); + return div.innerHTML; +} + +// Initialize on load +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); +} else { + init(); +}