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:
parent
80b170a735
commit
d31f394ad5
5 changed files with 1087 additions and 19 deletions
409
css/styles.css
409
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
140
global-to-local.php
Normal file
140
global-to-local.php
Normal 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
34
header.php
Normal 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>
|
||||
25
index.php
25
index.php
|
|
@ -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
498
js/global-to-local.js
Normal 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();
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue