Features: - Added "Lookup" button to trigger Box ID validation manually - No automatic validation on typing - user must click Lookup - Recursive folder contents fetching (3 levels deep) - Collapsible nested folder display with expand/collapse buttons - Visual hierarchy with indentation and folder icons - Shows all files within nested folders - Updated UI with better folder/file organization 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
419 lines
13 KiB
JavaScript
419 lines
13 KiB
JavaScript
/**
|
|
* L'Oréal Box Asset Submission Form - Frontend JavaScript
|
|
*/
|
|
|
|
// State management
|
|
let validatedBoxData = null;
|
|
let isBoxIdValid = false;
|
|
|
|
// DOM Elements
|
|
const boxIdInput = document.getElementById('boxId');
|
|
const boxIdValidation = document.getElementById('boxIdValidation');
|
|
const lookupBtn = document.getElementById('lookupBtn');
|
|
const submitBtn = document.getElementById('submitBtn');
|
|
const assetForm = document.getElementById('assetForm');
|
|
const messageDiv = document.getElementById('message');
|
|
const previewContainer = document.getElementById('previewContainer');
|
|
|
|
/**
|
|
* Initialize the application
|
|
*/
|
|
function init() {
|
|
// Lookup button event listener
|
|
lookupBtn.addEventListener('click', handleLookupClick);
|
|
|
|
// Allow Enter key in Box ID field to trigger lookup
|
|
boxIdInput.addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
handleLookupClick();
|
|
}
|
|
});
|
|
|
|
// Clear validation state when input changes
|
|
boxIdInput.addEventListener('input', () => {
|
|
isBoxIdValid = false;
|
|
validatedBoxData = null;
|
|
submitBtn.disabled = true;
|
|
boxIdInput.classList.remove('valid', 'invalid');
|
|
boxIdValidation.textContent = '';
|
|
boxIdValidation.className = 'validation-message';
|
|
});
|
|
|
|
// Form submission
|
|
assetForm.addEventListener('submit', handleFormSubmit);
|
|
}
|
|
|
|
/**
|
|
* Handle Lookup button click
|
|
*/
|
|
function handleLookupClick() {
|
|
const boxId = boxIdInput.value.trim();
|
|
|
|
if (!boxId) {
|
|
showMessage('Please enter a Box ID', 'error');
|
|
boxIdInput.focus();
|
|
return;
|
|
}
|
|
|
|
// Validate immediately
|
|
boxIdInput.classList.add('validating');
|
|
boxIdValidation.textContent = 'Looking up Box ID...';
|
|
boxIdValidation.className = 'validation-message loading';
|
|
lookupBtn.disabled = true;
|
|
lookupBtn.textContent = 'Looking up...';
|
|
|
|
validateBoxId(boxId);
|
|
}
|
|
|
|
/**
|
|
* Validate Box ID via API
|
|
*/
|
|
async function validateBoxId(boxId) {
|
|
try {
|
|
const response = await fetch('validate-box.php', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
},
|
|
body: JSON.stringify({ boxId })
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
// Valid Box ID
|
|
isBoxIdValid = true;
|
|
validatedBoxData = result.data;
|
|
|
|
boxIdInput.classList.remove('validating', 'invalid');
|
|
boxIdInput.classList.add('valid');
|
|
|
|
boxIdValidation.textContent = '✓ Valid Box ID';
|
|
boxIdValidation.className = 'validation-message success';
|
|
|
|
// Enable submit button if all other fields are filled
|
|
checkFormValidity();
|
|
|
|
// Show preview
|
|
showPreview(result.data);
|
|
|
|
} else {
|
|
// Invalid Box ID
|
|
isBoxIdValid = false;
|
|
validatedBoxData = null;
|
|
|
|
boxIdInput.classList.remove('validating', 'valid');
|
|
boxIdInput.classList.add('invalid');
|
|
|
|
boxIdValidation.textContent = '✗ ' + (result.error || 'Invalid Box ID');
|
|
boxIdValidation.className = 'validation-message error';
|
|
|
|
submitBtn.disabled = true;
|
|
|
|
// Show placeholder
|
|
showPreviewPlaceholder();
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Validation error:', error);
|
|
|
|
boxIdInput.classList.remove('validating', 'valid');
|
|
boxIdInput.classList.add('invalid');
|
|
|
|
boxIdValidation.textContent = '✗ Error validating Box ID';
|
|
boxIdValidation.className = 'validation-message error';
|
|
|
|
submitBtn.disabled = true;
|
|
showPreviewPlaceholder();
|
|
} finally {
|
|
// Re-enable lookup button
|
|
lookupBtn.disabled = false;
|
|
lookupBtn.textContent = 'Lookup';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show preview placeholder
|
|
*/
|
|
function showPreviewPlaceholder() {
|
|
previewContainer.innerHTML = `
|
|
<div class="preview-placeholder">
|
|
<svg width="100" height="100" viewBox="0 0 100 100" fill="none">
|
|
<path d="M50 10L80 30V70L50 90L20 70V30L50 10Z" stroke="#FFC407" stroke-width="3" fill="none"/>
|
|
<path d="M50 35V65M35 50H65" stroke="#FFC407" stroke-width="3" stroke-linecap="round"/>
|
|
</svg>
|
|
<p>Enter a Box ID to preview contents</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Render nested folder structure recursively
|
|
*/
|
|
function renderFolderContents(folders, files, level = 0) {
|
|
let html = '';
|
|
|
|
// Render folders first
|
|
if (folders && folders.length > 0) {
|
|
folders.forEach(folder => {
|
|
const hasContents = (folder.contents?.folders?.length > 0 || folder.contents?.files?.length > 0);
|
|
const folderId = 'folder-' + folder.id;
|
|
|
|
html += `
|
|
<div class="item folder-item" style="padding-left: ${level * 20}px;">
|
|
${hasContents ? `
|
|
<button type="button" class="folder-toggle" onclick="toggleFolder('${folderId}')">
|
|
<span class="toggle-icon">▶</span>
|
|
</button>
|
|
` : '<span class="folder-spacer"></span>'}
|
|
<div class="item-icon">📁</div>
|
|
<div class="item-name">${escapeHtml(folder.name)}</div>
|
|
</div>
|
|
`;
|
|
|
|
if (hasContents) {
|
|
html += `<div class="folder-contents" id="${folderId}" style="display: none;">`;
|
|
html += renderFolderContents(folder.contents.folders, folder.contents.files, level + 1);
|
|
html += '</div>';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Render files
|
|
if (files && files.length > 0) {
|
|
files.forEach(file => {
|
|
html += `
|
|
<div class="item" style="padding-left: ${level * 20 + 24}px;">
|
|
<div class="item-icon">📄</div>
|
|
<div class="item-name">${escapeHtml(file.name)}</div>
|
|
</div>
|
|
`;
|
|
});
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
/**
|
|
* Toggle folder expansion
|
|
*/
|
|
function toggleFolder(folderId) {
|
|
const folderContents = document.getElementById(folderId);
|
|
const toggleBtn = folderContents.previousElementSibling.querySelector('.folder-toggle');
|
|
const toggleIcon = toggleBtn.querySelector('.toggle-icon');
|
|
|
|
if (folderContents.style.display === 'none') {
|
|
folderContents.style.display = 'block';
|
|
toggleIcon.textContent = '▼';
|
|
} else {
|
|
folderContents.style.display = 'none';
|
|
toggleIcon.textContent = '▶';
|
|
}
|
|
}
|
|
|
|
// Make toggleFolder available globally
|
|
window.toggleFolder = toggleFolder;
|
|
|
|
/**
|
|
* Show Box preview
|
|
*/
|
|
function showPreview(data) {
|
|
const { folderName, masterCampaignNumber, masterCampaignId, contents } = data;
|
|
const { total, folders, files } = contents;
|
|
|
|
const folderContentsHtml = renderFolderContents(folders, files);
|
|
|
|
previewContainer.innerHTML = `
|
|
<div class="preview-content">
|
|
<div class="preview-header">
|
|
<h2>Box Preview</h2>
|
|
</div>
|
|
|
|
<div class="preview-info">
|
|
<div class="preview-info-label">Master Campaign Number</div>
|
|
<div class="preview-info-value">${escapeHtml(masterCampaignNumber)}</div>
|
|
</div>
|
|
|
|
<div class="preview-info">
|
|
<div class="preview-info-label">Master Campaign ID</div>
|
|
<div class="preview-info-value">${escapeHtml(masterCampaignId || 'N/A')}</div>
|
|
</div>
|
|
|
|
<div class="preview-info">
|
|
<div class="preview-info-label">Folder Name</div>
|
|
<div class="preview-info-value">${escapeHtml(folderName)}</div>
|
|
</div>
|
|
|
|
<div class="preview-section">
|
|
<h3>Contents Summary</h3>
|
|
<div class="contents-summary">
|
|
<div class="summary-item">
|
|
<div class="summary-count">${total}</div>
|
|
<div class="summary-label">Total Items</div>
|
|
</div>
|
|
<div class="summary-item">
|
|
<div class="summary-count">${folders.length}</div>
|
|
<div class="summary-label">Top-Level Folders</div>
|
|
</div>
|
|
<div class="summary-item">
|
|
<div class="summary-count">${files.length}</div>
|
|
<div class="summary-label">Top-Level Files</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="preview-section">
|
|
<h3>All Contents</h3>
|
|
<div class="items-list nested-items">
|
|
${folderContentsHtml || '<p class="empty-message">No contents found</p>'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Check if form is valid and enable/disable submit button
|
|
*/
|
|
function checkFormValidity() {
|
|
const supplyDate = document.getElementById('supplyDate').value;
|
|
const liveDate = document.getElementById('liveDate').value;
|
|
const endDate = document.getElementById('endDate').value;
|
|
|
|
// Enable submit only if Box ID is valid and all dates are filled
|
|
if (isBoxIdValid && supplyDate && liveDate && endDate) {
|
|
submitBtn.disabled = false;
|
|
} else {
|
|
submitBtn.disabled = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle form submission
|
|
*/
|
|
async function handleFormSubmit(e) {
|
|
e.preventDefault();
|
|
|
|
// Double-check validation
|
|
if (!isBoxIdValid || !validatedBoxData) {
|
|
showMessage('Please enter a valid Box ID first', 'error');
|
|
return;
|
|
}
|
|
|
|
const supplyDate = document.getElementById('supplyDate').value;
|
|
const liveDate = document.getElementById('liveDate').value;
|
|
const endDate = document.getElementById('endDate').value;
|
|
|
|
if (!supplyDate || !liveDate || !endDate) {
|
|
showMessage('Please fill in all date fields', 'error');
|
|
return;
|
|
}
|
|
|
|
// Disable submit button and show loading state
|
|
submitBtn.disabled = true;
|
|
submitBtn.textContent = 'Submitting...';
|
|
|
|
try {
|
|
const payload = {
|
|
boxId: boxIdInput.value.trim(),
|
|
supplyDate: formatDate(supplyDate),
|
|
liveDate: formatDate(liveDate),
|
|
endDate: formatDate(endDate),
|
|
boxData: validatedBoxData
|
|
};
|
|
|
|
const response = await fetch('submit.php', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
},
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
// Success - show message
|
|
showMessage(
|
|
result.message || 'Form submitted successfully! ' +
|
|
(result.webhookResponse?.message || ''),
|
|
'success'
|
|
);
|
|
|
|
// Reset form after successful submission
|
|
setTimeout(() => {
|
|
assetForm.reset();
|
|
isBoxIdValid = false;
|
|
validatedBoxData = null;
|
|
boxIdInput.classList.remove('valid', 'invalid', 'validating');
|
|
boxIdValidation.textContent = '';
|
|
showPreviewPlaceholder();
|
|
submitBtn.disabled = true;
|
|
}, 2000);
|
|
|
|
} else {
|
|
// Error from webhook or server
|
|
showMessage(
|
|
'Submission failed: ' + (result.error || 'Unknown error') +
|
|
(result.webhookResponse?.message ? ' - ' + result.webhookResponse.message : ''),
|
|
'error'
|
|
);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Submission error:', error);
|
|
showMessage('Error submitting form. Please try again.', 'error');
|
|
} finally {
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = 'Submit';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Format date to DD/MM/YYYY 00:00
|
|
*/
|
|
function formatDate(dateString) {
|
|
const date = new Date(dateString);
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const year = date.getFullYear();
|
|
return `${day}/${month}/${year} 00:00`;
|
|
}
|
|
|
|
/**
|
|
* Show message to user
|
|
*/
|
|
function showMessage(text, type) {
|
|
messageDiv.textContent = text;
|
|
messageDiv.className = `message ${type}`;
|
|
messageDiv.style.display = 'block';
|
|
|
|
// Auto-hide after 5 seconds
|
|
setTimeout(() => {
|
|
messageDiv.style.display = 'none';
|
|
}, 5000);
|
|
}
|
|
|
|
/**
|
|
* Escape HTML to prevent XSS
|
|
*/
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Add event listeners to date fields to check form validity
|
|
document.getElementById('supplyDate').addEventListener('change', checkFormValidity);
|
|
document.getElementById('liveDate').addEventListener('change', checkFormValidity);
|
|
document.getElementById('endDate').addEventListener('change', checkFormValidity);
|
|
|
|
// Initialize app when DOM is ready
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|