Add Global to Local CSV transformation frontend with visual progress tracking

Created Complete Tab System:
- header.php: Shared navigation between Asset Submission and Global to Local tabs
- global-to-local.php: Upload page with drag & drop, progress tracker, preview
- global-to-local.js: Frontend logic for upload, processing, preview, and Box upload

Visual Progress System:
- 6-stage progress tracker with icons and status (⏸️ pending, 🔄 processing,  success, ⚠️ warning,  error)
- Real-time status updates for each stage
- Detailed error cards with actionable messages
- Warning cards for data quality issues
- Success cards with completion info

Features:
- Drag & drop CSV upload with file size validation
- Step-by-step progress visualization
- Error reporting at each stage (upload, parse, campaign, OMG API, business unit, transform)
- CSV preview table (first 50 rows) before Box upload
- Download preview CSV before committing
- User approval required before Box upload
- Summary cards showing input/output counts, campaign, business unit

Error Handling:
- File validation errors (wrong type, too large, empty)
- CSV parsing errors (malformed, wrong columns)
- Campaign extraction errors (invalid filename)
- OMG API errors (404, timeout, auth failure)
- Business unit mapping errors (unrecognized brands)
- Date transformation errors (invalid formats)
- Box upload errors (permissions, folder issues)

UI Enhancements:
- Tab navigation with active state highlighting
- Professional error cards with details and actions
- Responsive design for mobile/desktop
- Maintains black/yellow L'Oréal brand colors

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
DJP 2025-11-17 16:48:43 -05:00
parent 80b170a735
commit d31f394ad5
5 changed files with 1087 additions and 19 deletions

View file

@ -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;
}
}

140
global-to-local.php Normal file
View file

@ -0,0 +1,140 @@
<?php
/**
* Global to Local CSV Transformation
* Upload and transform global campaign CSV into regional CSVs
*/
// Load dependencies
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/AuthMiddleware.php';
// Initialize authentication
$auth = new AuthMiddleware();
$user = $auth->requireAuth();
$pageTitle = 'Global to Local - L\'Oréal Box';
// Include shared header
require_once __DIR__ . '/header.php';
?>
<div class="g2l-container">
<!-- Upload Section -->
<div class="upload-section" id="uploadSection">
<h2>Upload Global Campaign CSV</h2>
<p class="section-description">Upload a global campaign CSV file to transform it into regional market CSVs.</p>
<form id="csvUploadForm" enctype="multipart/form-data">
<div class="upload-dropzone" id="dropzone">
<svg width="60" height="60" viewBox="0 0 60 60" fill="none">
<path d="M30 10L30 40M30 10L20 20M30 10L40 20" stroke="#FFC407" stroke-width="3" stroke-linecap="round"/>
<path d="M10 35L10 45C10 47.7614 12.2386 50 15 50L45 50C47.7614 50 50 47.7614 50 45L50 35" stroke="#FFC407" stroke-width="3" stroke-linecap="round"/>
</svg>
<p class="dropzone-text">Drag & drop CSV file here or click to browse</p>
<p class="dropzone-limit">Maximum file size: 5MB</p>
<input type="file" id="csvFile" name="csvFile" accept=".csv" required hidden>
</div>
<div id="fileInfo" class="file-info" style="display:none;">
<div class="file-icon">📄</div>
<div class="file-details">
<div class="file-name" id="fileName"></div>
<div class="file-size" id="fileSize"></div>
</div>
<button type="button" class="remove-file-btn" id="removeFile"></button>
</div>
<button type="submit" class="submit-btn" id="processBtn" disabled>
Process CSV
</button>
</form>
</div>
<!-- Progress Tracker -->
<div class="progress-section" id="progressSection" style="display:none;">
<h2>Processing Progress</h2>
<div class="progress-tracker" id="progressTracker">
<div class="progress-step" data-stage="upload">
<div class="step-icon">⏸️</div>
<div class="step-content">
<div class="step-title">Upload CSV</div>
<div class="step-message">Waiting...</div>
</div>
</div>
<div class="progress-step" data-stage="parse">
<div class="step-icon">⏸️</div>
<div class="step-content">
<div class="step-title">Parse CSV</div>
<div class="step-message">Pending</div>
</div>
</div>
<div class="progress-step" data-stage="campaign">
<div class="step-icon">⏸️</div>
<div class="step-content">
<div class="step-title">Extract Campaign Number</div>
<div class="step-message">Pending</div>
</div>
</div>
<div class="progress-step" data-stage="omg_api">
<div class="step-icon">⏸️</div>
<div class="step-content">
<div class="step-title">OMG API Lookup</div>
<div class="step-message">Pending</div>
</div>
</div>
<div class="progress-step" data-stage="business_unit">
<div class="step-icon">⏸️</div>
<div class="step-content">
<div class="step-title">Map Business Unit</div>
<div class="step-message">Pending</div>
</div>
</div>
<div class="progress-step" data-stage="transform">
<div class="step-icon">⏸️</div>
<div class="step-content">
<div class="step-title">Transform Data</div>
<div class="step-message">Pending</div>
</div>
</div>
</div>
<!-- Error/Warning Display -->
<div id="alertsContainer"></div>
</div>
<!-- Preview Section -->
<div class="preview-section" id="previewSection" style="display:none;">
<h2>Preview Transformed CSV</h2>
<div class="preview-summary" id="previewSummary"></div>
<div class="preview-table-container">
<table class="preview-table" id="previewTable"></table>
</div>
<div class="preview-actions">
<button type="button" class="action-btn download-btn" id="downloadPreviewBtn">
Download Preview
</button>
<button type="button" class="action-btn cancel-btn" id="cancelBtn">
Cancel
</button>
<button type="button" class="submit-btn" id="uploadToBoxBtn">
Approve & Upload to Box
</button>
</div>
</div>
</div>
<script>
// User data
const currentUser = {
name: '<?php echo addslashes($user['name']); ?>',
email: '<?php echo addslashes($user['email']); ?>'
};
</script>
<script src="js/global-to-local.js"></script>
</body>
</html>

34
header.php Normal file
View file

@ -0,0 +1,34 @@
<?php
/**
* Shared Header and Navigation
*/
// Get current page
$currentPage = basename($_SERVER['PHP_SELF'], '.php');
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo $pageTitle ?? 'L\'Oréal Box Asset Submission'; ?></title>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<div class="page-header">
<div class="header-content">
<h1>L'Oréal Box Asset Submission</h1>
<div class="user-info">
<span><?php echo htmlspecialchars($user['email']); ?></span>
</div>
</div>
<nav class="tab-navigation">
<a href="index.php" class="tab <?php echo $currentPage === 'index' ? 'active' : ''; ?>">
Asset Submission
</a>
<a href="global-to-local.php" class="tab <?php echo $currentPage === 'global-to-local' ? 'active' : ''; ?>">
Global to Local
</a>
</nav>
</div>

View file

@ -1,7 +1,7 @@
<?php
/**
* L'Oréal Box Asset Submission Form
* Main application page
* Main application page - Asset Submission Tab
*/
// Load dependencies
@ -12,25 +12,12 @@ require_once __DIR__ . '/AuthMiddleware.php';
$auth = new AuthMiddleware();
$user = $auth->requireAuth();
$ssoEnabled = $auth->isSSOEnabled();
$pageTitle = 'Asset Submission - L\'Oréal Box';
// Include shared header
require_once __DIR__ . '/header.php';
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>L'Oréal Box Asset Submission</title>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<div class="page-header">
<div class="header-content">
<h1>L'Oréal Box Asset Submission</h1>
<div class="user-info">
<span><?php echo htmlspecialchars($user['email']); ?></span>
</div>
</div>
</div>
<div class="main-container">
<!-- Left Column: Form -->

498
js/global-to-local.js Normal file
View file

@ -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 = `
<div class="alert-card error">
<div class="alert-header">
<span class="alert-icon"></span>
<span class="alert-title">${escapeHtml(result.error || 'Error')}</span>
</div>
${result.details ? `
<div class="alert-message">${escapeHtml(result.details)}</div>
` : ''}
${result.details && result.details.length > 100 ? `
<div class="alert-details">${escapeHtml(result.details)}</div>
` : ''}
${result.action ? `
<div class="alert-action">${escapeHtml(result.action)}</div>
` : ''}
${result.endpoint ? `
<div class="alert-details">Endpoint: ${escapeHtml(result.endpoint)}</div>
` : ''}
</div>
`;
alertsContainer.innerHTML = errorHtml;
}
/**
* Show warnings
*/
function showWarnings(warnings) {
if (warnings.length === 0) return;
let warningHtml = `
<div class="alert-card warning">
<div class="alert-header">
<span class="alert-icon"></span>
<span class="alert-title">Data Quality Issues (${warnings.length})</span>
</div>
<div class="alert-message">Found ${warnings.length} warning(s) in the data:</div>
<ul style="margin: 10px 0; padding-left: 20px;">
`;
warnings.forEach(warning => {
warningHtml += `<li>Row ${warning.row}: ${escapeHtml(warning.warning)}</li>`;
});
warningHtml += `
</ul>
<div class="alert-action">These rows will be processed but may have issues downstream.</div>
</div>
`;
alertsContainer.innerHTML += warningHtml;
}
/**
* Show alert
*/
function showAlert(type, title, message) {
const iconMap = {
error: '❌',
warning: '⚠️',
success: '✅'
};
const alertHtml = `
<div class="alert-card ${type}">
<div class="alert-header">
<span class="alert-icon">${iconMap[type]}</span>
<span class="alert-title">${escapeHtml(title)}</span>
</div>
<div class="alert-message">${escapeHtml(message)}</div>
</div>
`;
alertsContainer.innerHTML = alertHtml;
}
/**
* Show preview
*/
function showPreview(data) {
progressSection.style.display = 'none';
previewSection.style.display = 'block';
// Show summary
previewSummary.innerHTML = `
<div class="summary-card">
<div class="summary-label">Input Rows</div>
<div class="summary-value">${data.inputRows}</div>
</div>
<div class="summary-card">
<div class="summary-label">Output Rows</div>
<div class="summary-value">${data.outputRows}</div>
</div>
<div class="summary-card">
<div class="summary-label">Campaign Number</div>
<div class="summary-value">${escapeHtml(data.campaignNumber)}</div>
</div>
<div class="summary-card">
<div class="summary-label">Business Unit</div>
<div class="summary-value">${escapeHtml(data.businessUnit)}</div>
</div>
<div class="summary-card">
<div class="summary-label">Output Filename</div>
<div class="summary-value" style="font-size: 14px; word-break: break-all;">${escapeHtml(data.filename)}</div>
</div>
`;
// Build table
const preview = data.preview;
if (preview.length > 0) {
const headers = Object.keys(preview[0]);
let tableHtml = '<thead><tr>';
headers.forEach(header => {
tableHtml += `<th>${escapeHtml(header)}</th>`;
});
tableHtml += '</tr></thead><tbody>';
preview.forEach(row => {
tableHtml += '<tr>';
headers.forEach(header => {
tableHtml += `<td>${escapeHtml(row[header] || '')}</td>`;
});
tableHtml += '</tr>';
});
tableHtml += '</tbody>';
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 += `
<div style="text-align: center; margin-top: 20px;">
<a href="${result.data.fileUrl}" target="_blank" class="submit-btn" style="display: inline-block; text-decoration: none;">
View File in Box
</a>
</div>
`;
}
// 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();
}