bissell-naming-tool/index.html
DJP 25f5f96bc4 Major cleanup and reorganization for deployment
- Renamed aem-naming-tool-updated.html to index.html for easier hosting
- Created OLD/ folder for all legacy/deprecated files (17 files moved)
- Added OLD/ to .gitignore to exclude from future commits
- Updated all documentation references to use index.html
- Cleaned root directory to contain only active project files
- Updated README with new project structure and deployment-ready setup
- Preserved BISSELL_AEM_Folder_Master.xlsx as standard Excel filename

Project now deployment-ready with clean structure:
- index.html + bissell-product-data.json = complete web app
- All legacy files archived in OLD/ (excluded from git)
- Comprehensive README for users and administrators

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 11:38:16 -04:00

1716 lines
No EOL
83 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BISSELL AEM DAM Naming Tool</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&display=swap');
:root {
--primary-color: #2c3e50;
--primary-hover-color: #1a252f;
--primary-btn-color: #2c3e50;
--primary-btn-hover-color: #1a252f;
--bissell-red: #e53e3e;
--bissell-red-hover: #c53030;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Montserrat', sans-serif;
line-height: 1.6;
color: #333;
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
min-height: 100vh;
transition: background-color 0.3s ease, color 0.3s ease;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header {
text-align: center;
color: white;
margin-bottom: 30px;
}
.header h1 {
font-size: 2.5rem;
margin-bottom: 10px;
}
.header p {
font-size: 1.2rem;
opacity: 0.9;
}
.main-content {
background: white;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
overflow: hidden;
}
.nav-tabs {
display: flex;
background: #f8f9fa;
border-bottom: 1px solid #dee2e6;
}
.nav-tab {
flex: 1;
padding: 15px 20px;
background: none;
border: none;
cursor: pointer;
transition: all 0.3s;
font-size: 1rem;
font-weight: 500;
}
.nav-tab:hover {
background: #e9ecef;
}
.nav-tab.active {
background: var(--primary-color);
color: white;
}
.tab-content {
display: none;
padding: 30px;
}
.tab-content.active {
display: block;
}
.card {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
border-left: 4px solid var(--primary-color);
}
.btn {
background: var(--primary-btn-color);
color: white;
border: none;
padding: 12px 24px;
border-radius: 5px;
cursor: pointer;
font-size: 1rem;
font-family: 'Montserrat', sans-serif;
font-weight: 500;
transition: background 0.3s;
}
.btn:hover {
background: var(--primary-btn-hover-color);
}
.btn-secondary {
background: #6c757d;
}
.btn-secondary:hover {
background: #545b62;
}
.btn-success {
background: var(--bissell-red);
}
.btn-success:hover {
background: var(--bissell-red-hover);
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #495057;
}
.form-control {
width: 100%;
padding: 10px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 1rem;
}
.form-control:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 0.2rem rgba(44,62,80,.25);
}
select.form-control {
height: 42px;
}
.form-group {
position: relative;
}
.enter-hint {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
color: #999;
font-size: 0.8rem;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s;
background: white;
padding: 2px 5px;
border-radius: 3px;
}
.form-control:focus + .enter-hint,
.form-control:hover + .enter-hint {
opacity: 1;
}
.result-box {
background: #d4edda;
border: 1px solid #c3e6cb;
border-radius: 5px;
padding: 15px;
margin: 20px 0;
}
.result-filename {
font-family: 'Courier New', monospace;
font-size: 1.1rem;
font-weight: bold;
color: #155724;
word-break: break-all;
}
.error-box {
background: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 5px;
padding: 15px;
margin: 20px 0;
color: #721c24;
}
.step {
display: none;
}
.step.active {
display: block;
}
.step-indicator {
display: flex;
justify-content: center;
margin-bottom: 30px;
flex-wrap: wrap;
}
.step-circle {
width: 40px;
height: 40px;
border-radius: 50%;
background: #dee2e6;
display: flex;
align-items: center;
justify-content: center;
margin: 5px;
font-weight: bold;
transition: all 0.3s;
font-size: 0.9rem;
}
.step-circle.active {
background: var(--primary-color);
color: white;
}
.step-circle.completed {
background: #28a745;
color: white;
}
.navigation-buttons {
display: flex;
justify-content: space-between;
margin-top: 30px;
}
.help-section {
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 5px;
padding: 15px;
margin: 20px 0;
}
.code {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 3px;
padding: 2px 6px;
font-family: 'Courier New', monospace;
font-size: 0.9rem;
}
@media (max-width: 768px) {
.nav-tabs {
flex-direction: column;
}
.header h1 {
font-size: 2rem;
}
.navigation-buttons {
flex-direction: column;
gap: 10px;
}
.step-indicator {
flex-wrap: wrap;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎯 BISSELL AEM DAM Naming Tool 3.0</h1>
<p>Create perfect filenames using the new consolidated structure</p>
</div>
<div class="main-content">
<div class="nav-tabs">
<button class="nav-tab active" onclick="showTab('naming-wizard')">📝 Name a File</button>
<button class="nav-tab" onclick="showTab('decoder')">🔍 Decode Filename</button>
<button class="nav-tab" onclick="showTab('help')">❓ Help & How-To</button>
</div>
<!-- Naming Wizard Tab -->
<div id="naming-wizard" class="tab-content active">
<h2>File Naming Wizard</h2>
<p>Follow the steps to generate your filename using the complete BISSELL product hierarchy.</p>
<p style="color: #666; font-size: 0.9rem;"><strong>💡 Tip:</strong> Press <kbd style="background: #f1f1f1; padding: 2px 6px; border-radius: 3px; font-size: 0.8rem;">Enter</kbd> after each selection to quickly move to the next step!</p>
<div class="step-indicator">
<div class="step-circle active" id="indicator-1">1</div>
<div class="step-circle" id="indicator-2">2</div>
<div class="step-circle" id="indicator-3">3</div>
<div class="step-circle" id="indicator-4">4</div>
<div class="step-circle" id="indicator-5">5</div>
<div class="step-circle" id="indicator-6">6</div>
<div class="step-circle" id="indicator-7">7</div>
<div class="step-circle" id="indicator-8">8</div>
</div>
<!-- Step 1: OMG Job Number -->
<div class="step active" id="step-1">
<div class="card">
<h3>Step 1: OMG Job Number</h3>
<p>Enter the OMG job number that will prefix your filename.</p>
<div class="form-group">
<label for="omgJobNumber">OMG Job Number:</label>
<input type="text" class="form-control" id="omgJobNumber" placeholder="5791356">
<span class="enter-hint">Press Enter ↵</span>
<small>This number will prefix your final filename</small>
</div>
</div>
</div>
<!-- Step 2: Product Category -->
<div class="step" id="step-2">
<div class="card">
<h3>Step 2: Product Category</h3>
<p>Select the main product category.</p>
<div class="form-group">
<label for="productCategory">Product Category:</label>
<select class="form-control" id="productCategory"
onchange="updateProductTypes()">
<option value="">-- Select Product Category --</option>
</select>
<span class="enter-hint">Press Enter ↵</span>
</div>
</div>
</div>
<!-- Step 3: Product Type -->
<div class="step" id="step-3">
<div class="card">
<h3>Step 3: Product Type</h3>
<p>Select the specific product type.</p>
<div class="form-group">
<label for="productType">Product Type:</label>
<select class="form-control" id="productType"
onchange="updateGPDNumbers()">
<option value="">-- Select Product Type --</option>
</select>
</div>
</div>
</div>
<!-- Step 4: GPD Number -->
<div class="step" id="step-4">
<div class="card">
<h3>Step 4: GPD Number</h3>
<p>Select the GPD product number.</p>
<div class="form-group">
<label for="gpdNumber">GPD Number:</label>
<select class="form-control" id="gpdNumber"
onchange="updateProductNames()">
<option value="">-- Select GPD Number --</option>
</select>
</div>
</div>
</div>
<!-- Step 5: Product Name -->
<div class="step" id="step-5">
<div class="card">
<h3>Step 5: Product Name</h3>
<p>Select the specific product name.</p>
<div class="form-group">
<label for="productName">Product Name:</label>
<select class="form-control" id="productName"
onchange="updateAssetTypeA()">
<option value="">-- Select Product Name --</option>
</select>
<span class="enter-hint">Press Enter ↵</span>
</div>
</div>
</div>
<!-- Step 6: Asset Type A -->
<div class="step" id="step-6">
<div class="card">
<h3>Step 6: Asset Type A</h3>
<p>Select the primary asset type.</p>
<div class="form-group">
<label for="assetTypeA">Asset Type A:</label>
<select class="form-control" id="assetTypeA"
onchange="updateAssetTypeB()">
<option value="">-- Select Asset Type A --</option>
</select>
</div>
</div>
</div>
<!-- Step 7: Asset Type B -->
<div class="step" id="step-7">
<div class="card">
<h3>Step 7: Asset Type B</h3>
<p>Select the specific asset type.</p>
<div class="form-group">
<label for="assetTypeB">Asset Type B:</label>
<select class="form-control" id="assetTypeB"
onchange="updateAssetTypeC()">
<option value="">-- Select Asset Type B --</option>
</select>
</div>
</div>
</div>
<!-- Step 8: Final Details -->
<div class="step" id="step-8">
<div class="card">
<h3>Step 8: Final Details</h3>
<p>Complete the remaining details for your asset.</p>
<div class="form-group" id="assetTypeCGroup" style="display: none;">
<label for="assetTypeC">Color Profile:</label>
<select class="form-control" id="assetTypeC">
<option value="">-- Select Color Profile --</option>
</select>
</div>
<div class="form-group" id="colorwayGroup" style="display: none;">
<label for="colorway">Colorway:</label>
<select class="form-control" id="colorway">
<option value="">-- Select Colorway --</option>
</select>
</div>
<div class="form-group">
<label for="customDescriptor">Custom Descriptor (Optional):</label>
<input type="text" class="form-control" id="customDescriptor"
placeholder="e.g., 250x250banner1, 300x300, EcomAsset1">
<small>Custom text descriptor (no underscores allowed). Will be moved to front of DAM filename.</small>
</div>
<button class="btn btn-success" onclick="generateFilename()">🎯 Generate Filename</button>
<div id="result" style="display: none;">
<h4>Your Generated Filename:</h4>
<div class="result-box">
<div class="result-filename" id="generatedFilename"></div>
</div>
<h4>📁 AEM DAM Folder Path:</h4>
<div class="result-box">
<div class="result-filename" id="generatedPath" style="font-size: 0.9rem; color: #666;"></div>
</div>
<h4>📄 AEM DAM File Name:</h4>
<div class="result-box">
<div class="result-filename" id="damFilename" style="font-size: 1rem; color: #155724;"></div>
</div>
<h4>📋 Metadata (Removed from DAM filename but in DAM metadata):</h4>
<div class="result-box" style="background: #fff3cd; border-color: #ffeaa7;">
<div id="metadataInfo" style="font-size: 0.9rem; color: #856404;"></div>
</div>
<div style="margin-top: 15px;">
<button class="btn btn-secondary" onclick="copyToClipboard()">📋 Copy Original Filename</button>
<button class="btn btn-secondary" onclick="copyDAMFilenameToClipboard()" style="margin-left: 10px;">📋 Copy DAM Filename</button>
<button class="btn btn-secondary" onclick="copyPathToClipboard()" style="margin-left: 10px;">📁 Copy Path</button>
</div>
</div>
</div>
</div>
<div class="navigation-buttons">
<button class="btn btn-secondary" id="prevBtn" onclick="previousStep()" style="display: none;">← Previous</button>
<button class="btn" id="nextBtn" onclick="nextStep()">Next →</button>
</div>
</div>
<!-- Decoder Tab -->
<div id="decoder" class="tab-content">
<h2>🔍 Filename Decoder</h2>
<p>Paste an existing filename to decode its components and see the folder structure.</p>
<div class="card">
<h3>Enter Filename to Decode</h3>
<div class="form-group">
<label for="filenameInput">Filename (with or without extension):</label>
<input type="text" class="form-control" id="filenameInput"
placeholder="5791356_dry_canister_p2829_pet-hair-eraser_imagery_hero_rgb_mambo-red.tif"
oninput="decodeFilename()">
</div>
<div id="decodedResult" style="margin-top: 20px;">
<!-- Results will appear here -->
</div>
</div>
</div>
<!-- Help Tab -->
<div id="help" class="tab-content">
<h2>Help & How-To Guide</h2>
<div class="help-section">
<h4>🎯 New Consolidated Structure</h4>
<p>This tool now uses the complete BISSELL product hierarchy from the Excel structure:</p>
<ol>
<li><strong>OMG Job Number:</strong> Prefixes production filename, moved to metadata for DAM</li>
<li><strong>Product Category:</strong> wet, dry, consumables, sanitaire, rug-doctor</li>
<li><strong>Product Type:</strong> canister, upright, stick, robot, etc.</li>
<li><strong>GPD Number:</strong> P#### product codes</li>
<li><strong>Product Name:</strong> crosswave, pet-hair-eraser, cleanview, etc.</li>
<li><strong>Asset Type A:</strong> digital, imagery, logo, parts</li>
<li><strong>Asset Type B:</strong> hero, lifestyle, banner, a-plus</li>
<li><strong>Color Profile & Colorway:</strong> rgb/cmyk + colorway (when applicable)</li>
<li><strong>Custom Descriptor:</strong> Optional free text (e.g., 250x250banner1, 300x300)</li>
</ol>
</div>
<div class="help-section">
<h4>📝 Filename Formats</h4>
<p><strong>Production Format:</strong> <span class="code">[OMG-JOB-NUMBER]_[product-hierarchy]_[colorway]_[customDescriptor].ext</span></p>
<p><strong>DAM Format:</strong> <span class="code">[customDescriptor]_[gpd-number]_[product-name]_[asset-types]_[colorway]_[omg-job-number].ext</span></p>
<h5>Examples:</h5>
<p><strong>Production:</strong> <span class="code">12345678_dry_robot_p2990_crosswave-robot_imagery_hero_NCW_250x250banner1.ext</span></p>
<p><strong>DAM Renamed:</strong> <span class="code">250x250banner1_p2990_crosswave-robot_imagery_hero.ext</span></p>
<p><strong>Key Changes for DAM:</strong></p>
<ul>
<li>Custom descriptor moves to front</li>
<li>Job number, "dry", "robot" removed from filename</li>
<li>"NCW" colorway removed from filename</li>
<li>Removed data preserved in metadata</li>
</ul>
</div>
<div class="help-section">
<h4>🏷️ Custom Descriptor Field</h4>
<p><strong>Purpose:</strong> Add custom text like "250x250banner1", "300x300", "EcomAsset1"</p>
<p><strong>Rules:</strong> No underscores allowed - use other characters only</p>
<p><strong>Behavior:</strong></p>
<ul>
<li>Appears at end of production filename</li>
<li>Moves to front of DAM filename</li>
<li>Visible in both versions (not removed)</li>
</ul>
<p><strong>Example:</strong> "250x250banner1" helps identify banner size/variant</p>
</div>
<div class="help-section">
<h4>🎨 Colorways & Folder Structure</h4>
<p><strong>Always Required:</strong> Must select a colorway for every asset</p>
<p><strong>Available Options:</strong> Product-specific colorways or "NCW" (No Colorway)</p>
<p><strong>Folder Behavior:</strong></p>
<ul>
<li>Real colorways create folders: <span class="code">/imagery/hero/rgb/mambo-red/</span></li>
<li>NCW does not create folder: <span class="code">/imagery/hero/rgb/</span></li>
</ul>
<p><strong>Filename Behavior:</strong></p>
<ul>
<li>Production filename always includes colorway</li>
<li>DAM filename excludes "NCW" but includes real colorways</li>
</ul>
</div>
<div class="help-section">
<h4>🔧 Consumables Workflow</h4>
<p><strong>Consumables Only:</strong> When working with consumables, you have "Generic" options available.</p>
<p><strong>GPD Numbers:</strong> For consumables, select "Generic / No Specific GPD" when working with broad asset categories.</p>
<p><strong>Product Names:</strong> For consumables, select "Generic / No Specific Product" or choose from all available products.</p>
<p><strong>Other Categories:</strong> All other product categories (dry, wet, sanitaire, etc.) follow strict hierarchy matching.</p>
</div>
<div class="help-section">
<h4>🔄 Cascading Selections</h4>
<p>Each level filters the options for the next level based on the Excel structure relationships. This ensures all combinations are valid and follow the established hierarchy.</p>
</div>
<div class="help-section">
<h4>⌨️ Keyboard Navigation</h4>
<p><strong>Press Enter:</strong> After making any selection or entering text, press Enter to automatically move to the next step.</p>
<p><strong>Faster Workflow:</strong> No need to click "Next" - just type or select and press Enter!</p>
<p><strong>Smart Validation:</strong> Only advances if the current field has a valid value.</p>
</div>
<div class="help-section">
<h4>🔧 JSON Data Management</h4>
<p><strong>Dynamic Loading:</strong> This tool loads product data from <span class="code">bissell-product-data.json</span></p>
<p><strong>Easy Updates:</strong> To add new products, update the Excel file and run the conversion script:</p>
<ol>
<li>Replace <span class="code">BISSELL_AEM Folder_Hierarchy6-30.xlsx</span> with new version</li>
<li>Run: <span class="code">python excel-to-json-converter.py</span></li>
<li>Refresh this tool to see new products</li>
</ol>
<p><strong>No Code Changes:</strong> Product updates require no HTML modifications - just update the JSON file.</p>
</div>
<div class="help-section">
<h4>🔍 Using the Decoder</h4>
<p><strong>Purpose:</strong> Analyze existing filenames to understand their structure and folder paths.</p>
<p><strong>How to Use:</strong></p>
<ol>
<li>Click the "🔍 Decode Filename" tab</li>
<li>Paste any filename (with or without extension)</li>
<li>See the breakdown of components and folder structure</li>
</ol>
<p><strong>Supports:</strong></p>
<ul>
<li>Legacy filenames without custom descriptors</li>
<li>New filenames with custom descriptors at the end</li>
<li>NCW colorway handling (no folder created)</li>
<li>Automatic detection of custom descriptors vs colorways</li>
</ul>
<p><strong>Example Inputs:</strong></p>
<p><span class="code">12345678_dry_robot_p2990_crosswave-robot_imagery_hero_NCW_250x250banner1.ext</span></p>
<p><span class="code">5791356_wet_wash_p3084_crosswave_imagery_hero_rgb_mambo-red.tif</span></p>
</div>
</div>
</div>
</div>
<script>
// Product hierarchy data - loaded from external JSON
let structureData = null;
// Load data from external JSON file
async function loadProductData() {
try {
const response = await fetch('bissell-product-data.json');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
structureData = await response.json();
console.log('Product data loaded successfully:', structureData);
return structureData;
} catch (error) {
console.error('Error loading product data:', error);
// Fallback to hardcoded data if JSON fails to load
structureData = {
productCategories: ["consumables", "dry", "rug-doctor", "sanitaire", "wet"],
dropdownRelationships: {
"0": {
"1-product-assets": ["consumables", "dry", "rug-doctor", "sanitaire", "wet"]
},
"1": {
"consumables": ["boost", "common", "great-value", "manual", "obsolete", "pdc", "rental", "steam", "super-concentrate", "udc", "wash"],
"dry": ["air", "canister", "common", "hand-vac", "robot", "stick", "sweeper", "uvac"],
"sanitaire": ["air-mover", "backpack", "cordless", "professional", "steam", "sweeper", "upright", "wash"],
"wet": ["common", "pdc", "robot", "steam", "udc", "wash"]
},
"2": {
"common": ["accessories", "digital", "illustrations", "imagery", "parts", "print", "research"],
"air": ["p2963", "p3004", "p3019", "p3029", "p3083"],
"canister": ["p263", "p2829", "p2831", "p2850", "p2873", "p2886", "p2887", "p3189", "p461", "p475"],
"hand-vac": ["p2862", "p2923", "p3008", "p481", "p483"],
"robot": ["p2848", "p2949", "p2950", "p2990", "p3001", "p3011", "p3049", "p3097", "p413", "p472", "p488"],
"stick": ["p2822", "p2862", "p2863", "p2878", "p2900", "p2902", "p2922", "p2960", "p2961", "p2969-p3051", "p2983", "p3025", "p3043", "p3114", "p3152", "p3180", "p3192", "p3202", "p429"],
"sweeper": ["p179", "p280", "p2929", "p806", "p826"],
"uvac": ["p2861-p3010", "p2881", "p2882", "p2884", "p2955", "p2959-p3044", "p3005", "p3024", "p3027", "p3062", "p3093", "p3116", "p3145", "p3161", "p3178", "p460", "p482", "p486"],
"pdc": ["p250-p3059", "p291", "p2931-p410", "p2932", "p3000", "p3071", "p3090", "p3109", "p3110", "p3118", "p3126", "p3127-p3205", "p3146", "p3153", "p3157", "p3167", "p321-p459"],
"steam": ["p259", "p2818", "p3128", "p411", "p412", "p421", "p466", "p487"],
"udc": ["p266-p372-p386", "p2933", "p2965", "p2973", "p2989", "p3061", "p3106", "p3143", "p3159", "p3173", "p3213", "p451"],
"wash": ["p2817", "p2883", "p2915", "p2985", "p3016", "p3047", "p3084", "p3091", "p3094", "p3113", "p3121", "p3123", "p3134", "p3150", "p3154", "p3174", "p3199", "p432"]
},
"3": {
"p2963": ["air220-air320-replacement-carbon-filter", "air320-replacement-high-efficiency-filter", "myair-replacement-filter"],
"p3004": ["myair-mini", "myair-pro", "small-air-purifier-hepa-and-carbon-filter"],
"p3019": ["air280", "air280-max"],
"p3029": ["air320-filter"],
"p3083": ["air180"],
"p263": ["garage-pro"],
"p2829": ["pet-hair-eraser", "smartclean", "smartclean-pet"],
"p2831": ["cleanview"],
"p2850": ["multiclean-wet-and-dry"],
"p2873": ["premium-compact", "smartclean"],
"p2886": ["easy-vac-bagless", "zing-bagless"],
"p2887": ["aeroswift-bagged-canister", "zing-bagged-cylinder-vacuum", "zing-canister-vacuum"],
"p3189": ["multiclean-turbo"],
"p461": ["hard-floor-expert-multicyclonic"],
"p475": ["bagless-filter-replacement-kit"],
"p2862": ["airram", "multi-hand-vacuum"],
"p2923": ["pet-hair-eraser-lithium-ion"],
"p3008": ["aeroslim"],
"p481": ["bolt-2-0-18v", "bolt-lithium-max-professional", "bolt-lithium-pet"],
"p483": ["automate", "pet-hair-eraser", "pet-hair-eraser-plus"],
"p2848": ["smartclean-plus"],
"p2949": ["ev675"],
"p2950": ["smartclean-connected"],
"p2990": ["crosswave-robot"],
"p3001": ["cleanview-connect"],
"p3011": ["spinwave-robot"],
"p3049": ["spinwave-r5"],
"p3097": ["readyclean-a3"],
"p413": ["replacement-vacuum-filter"],
"p472": ["smartclean"],
"p488": ["iconpet"],
"p2822": ["iconpet"],
"p2863": ["3-in-1", "featherweight", "featherweight-btc"],
"p2878": ["powerclean-corded"],
"p2900": ["multireach-cordless"],
"p2902": ["multireach-ion-xl-36v"],
"p2960": ["3-in-1-turbo"],
"p2961": ["icon-edge-professional", "iconpet-edge"],
"p2969-p3051": ["cleanview-pet-slim-corded", "pet-hair-eraser-slim-corded", "powerglide-pet-slim-corded"],
"p2983": ["cleanview-pet-slim-cordless", "multireach-active-pet-cordless", "powerglide-pet-slim-cordless", "slim-cordless-stick-21v"],
"p3025": ["icon-turbo-essential-25v", "iconpet-turbo-25v-cordless", "iconpet-turbo-edge", "iconpet-turbo-edge-252v", "iconpetturbo-252v", "omnipet-turbo-25v-cordless"],
"p3043": ["featherweight-powerbrush"],
"p3114": ["cleanview-xr", "cleanview-xr-pet"],
"p3152": ["powerclean-furfinder", "powerclean-furguard"],
"p3180": ["powerclean-dualbrush"],
"p3192": ["powerclean"],
"p3202": ["powerclean-corded"],
"p429": ["airram"],
"p179": ["refresh-manual-sweeper"],
"p280": ["natural-sweep"],
"p2929": ["SW200", "natural-sweep", "refresh-manual-sweeper"],
"p806": ["perfect-sweep-turbo"],
"p826": ["SW100", "sturdy-sweep"],
"p2861-p3010": ["cleanview-swivel", "cleanview-swivel-pet-plus", "cleanview-swivel-pet-rewind", "cleanview-swivel-pet-rewind-deluxe", "cleanview-swivel-rewind-pet", "powerclean-swivel-rewind-pet", "powerlifter-swivel-pet", "powerlifter-swivel-pet-rewind"],
"p2881": ["pet-hair-eraser-turbo", "pet-hair-eraser-turbo-plus", "pet-hair-eraser-turbo-pro"],
"p2882": ["cleanview-rewind-pet-deluxe", "powerclean-rewind"],
"p2884": ["cleanview-compact", "powerforce-compact", "powerforce-compact-turbo"],
"p2955": ["replacement-vacuum-filter"],
"p2959-p3044": ["cleanview-allergen", "multiclean-allergen", "pet-hair-eraser-allergen", "powerclean-allergen", "powerlifter-swivel"],
"p3005": ["cleanview", "cleanview-rewind", "cleanview-rewind-pet", "powerforce-helix", "powerforce-pet-deluxe", "powerforce-rewind-pet", "powerforce-rewind-pet-deluxe", "powerforce-turbo-pet"],
"p3024": ["cleanview-poweredge-rewind-pet", "cleanview-swivel-pet-reach", "cleanview-swivel-rewind-pet-reach", "common", "powerlifter-swivel-pet-reach", "powerlifter-swivel-rewind-pet-reach"],
"p3027": ["multiclean-allergen-pet-rewind", "powerlifter-allergen-pet-rewind", "powerlifter-surfacesense-lift-off", "surfacesense-lift-off", "surfacesense-lift-off-pet"],
"p3062": ["replacement-vacuum-filter"],
"p3093": ["replacement-vacuum-filter"],
"p3116": ["cleanview-swivel-pet", "cleanview-swivel-rewind-pet", "powerlifter-swivel-pet", "powerlifter-swivel-rewind-pet"],
"p3145": ["cleanview-max-furguard", "cleanview-max-lift-off", "pet-hair-eraser-allergen-lift-off", "powerlifter-allergen-lift-off"],
"p3161": ["cleanview-max-tangle-free", "cleanview-max-tangle-free-rewind"],
"p3178": ["replacement-vacuum-filter"],
"p460": ["powerglide-cordless"],
"p482": ["replacement-vacuum-filter"],
"p486": ["replacement-vacuum-filter"],
"p250-p3059": ["little-green", "little-green-autocare", "little-green-pet", "little-green-pet-deluxe", "little-green-select"],
"p291": ["little-green", "spotclean-c2", "spotclean-c3", "spotclean-c3-essential", "spotclean-essential", "spotclean-stainlift"],
"p2931-p410": ["replacement-vacuum-filter"],
"p2932": ["replacement-vacuum-filter"],
"p3000": ["replacement-vacuum-filter"],
"p3071": ["replacement-vacuum-filter"],
"p3090": ["replacement-vacuum-filter"],
"p3109": ["little-green-proheat", "little-green-proheat-pet", "little-green-proheat-smartmix", "spotclean-proheat", "spotclean-proheat-advanced", "spotclean-truheat"],
"p3110": ["little-green-deluxe-pet", "little-green-max-pet", "little-green-max-pet-smartmix", "little-green-multiclean", "spotclean", "spotclean-c5", "spotclean-c5-pet", "spotclean-professional", "spotclean-select"],
"p3118": ["spotclean-hydrosteam"],
"p3126": ["little-green-auto-pro", "little-green-pet-pro", "spotclean-c9", "spotclean-pro", "spotclean-turbo"],
"p3127-p3205": ["little-green-hydrosteam", "little-green-hydrosteam-pet", "spotclean-hydrosteam", "spotclean-hydrosteam-auto", "spotclean-hydrosteam-pro", "spotclean-hydrosteam-select"],
"p3146": ["little-green-mini", "spotclean-mini"],
"p3153": ["little-green-mini-cordless", "spotclean-mini-cordless"],
"p3157": ["replacement-vacuum-filter"],
"p3167": ["replacement-vacuum-filter"],
"p321-p459": ["replacement-vacuum-filter"],
"p259": ["steam-shot", "steam-shot-deluxe", "steam-shot-extended-reach", "steam-shot-omni", "steam-shot-omnireach"],
"p2818": ["replacement-vacuum-filter"],
"p3128": ["spinwave-smartsteam"],
"p411": ["replacement-vacuum-filter"],
"p412": ["replacement-vacuum-filter"],
"p421": ["replacement-vacuum-filter"],
"p466": ["replacement-vacuum-filter"],
"p487": ["powerfresh", "powerfresh-deluxe", "powerfresh-deluxe-pet", "powerfresh-sanitize-professional", "powerfresh-v"],
"p266-p372-p386": ["replacement-vacuum-filter"],
"p2933": ["replacement-vacuum-filter"],
"p2965": ["replacement-vacuum-filter"],
"p2973": ["replacement-vacuum-filter"],
"p2989": ["replacement-vacuum-filter"],
"p3061": ["replacement-vacuum-filter"],
"p3106": ["powerclean-2x", "powerclean-max", "powerforce-pet-cl", "powerwash-pet", "quickwash-powerbrush", "turboclean-pet-xl"],
"p3143": ["replacement-vacuum-filter"],
"p3159": ["cleanview-hydrosteam", "powerwash-hydrosteam", "revolution-hydrosteam"],
"p3173": ["proheat-2x-revolution-pet-pro", "proheat-2x-revolution-pet-pro-advanced", "proheat-2x-revolution-pet-pro-deluxe", "proheat-2x-revolution-pet-pro-plus"],
"p3213": ["proheat-2x-revolution-freshstart"],
"p451": ["replacement-vacuum-filter"],
"p2817": ["spinwave", "spinwave-hard-floor-expert", "spinwave-pet", "spinwave-plus"],
"p2883": ["replacement-vacuum-filter"],
"p2915": ["spinwave-cordless", "spinwave-cordless-hard-floor-expert"],
"p2985": ["BGFW13", "crosswave-commercial"],
"p3016": ["crosswave-hydrosteam", "crosswave-hydrosteam-deluxe", "crosswave-hydrosteam-pet", "crosswave-hydrosteam-pet-pro"],
"p3047": ["spinwave-plus-vac", "spinwave-vac-pet", "spinwave-vac-pet-pro"],
"p3084": ["crosswave", "crosswave-c3-pro", "crosswave-c3-select", "crosswave-pet", "crosswave-pet-pro"],
"p3091": ["crosswave-hf3-cordless", "crosswave-hf3-cordless-plus", "crosswave-hf3-cordless-pro", "crosswave-hf3-cordless-professional", "crosswave-hf3-cordless-select"],
"p3094": ["turboclean-hard-floors"],
"p3113": ["crosswave-hf5"],
"p3121": ["crosswave-omniforce"],
"p3123": ["crosswave-hf2", "crosswave-opp-corded", "hf2-corded"],
"p3134": ["crosswave-omniforce-edge", "crosswave-omniforce-edge-pro"],
"p3150": ["crosswave-omniedge", "crosswave-omnifind", "crosswave-omnifind-pro"],
"p3154": ["crosswave-edge"],
"p3174": ["crosswave-edgefind-pro"],
"p3199": ["crosswave-edgefind", "crosswave-hydroscrub", "crosswave-omniforce-edgefind-pro", "crosswave-omniforce-edgefind-select"],
"p432": ["crosswave"]
},
"4": {
"multi-hand-vacuum": ["mambo-red"],
"aeroslim": ["mambo-red", "titanium"],
"iconpet": ["copper-harbor", "electric-blue"],
"airram": ["cha-cha-lime", "electric-blue", "gun-metal-gray", "mambo-red", "pacific-purple"],
"3-in-1": ["baha-blue"],
"featherweight": ["black", "bossanova-blue", "brite-white", "disco-teal", "euro-red"],
"featherweight-btc": ["cha-cha-lime", "grapevine-purple", "la-bomba-pink", "samba-orange"],
"multireach-ion-xl-36v": ["sparkle-silver"],
"cleanview-xr": ["black"],
"cleanview-xr-pet": ["grapevine-purple"],
"powerclean-furguard": ["cobalt", "midnight", "silver", "titanium"],
"powerclean-furfinder": ["cobalt", "grapevine", "lake", "mambo-red", "midnight", "silver"],
"powerclean-dualbrush": ["garnet-titanium", "vineyard-gold"],
"powerclean": ["dusk", "mambo-red", "sparkling-silver", "warm-white-lake-blue", "white-lake-blue"],
"refresh-manual-sweeper": ["light-blue"],
"perfect-sweep-turbo": ["innovate-orange"],
"pet-hair-eraser": ["electric-blue", "mambo-red", "cha-cha-lime"],
"crosswave-edge": ["dove-dusk", "frost-blue", "frost-green", "peri-blue", "warm-white-lake-blue"],
"crosswave": ["bossanova-blue", "cha-cha-lime", "cobalt-blue", "disco-teal", "electric-blue", "grapevine-purple", "mambo-red", "samba-orange"],
"spinwave": ["bossanova-blue", "cha-cha-lime", "disco-teal"],
"cleanview": ["cobalt-blue", "euro-red"],
"little-green": ["cha-cha-lime", "emerald-green", "md-green"],
"steam-shot": ["bossanova-blue", "electric-blue"],
"powerfresh": ["bossanova-blue"]
},
"5": {
"light-blue": ["imagery"],
"electric-blue": ["digital", "imagery"],
"mambo-red": ["digital", "imagery"],
"cha-cha-lime": ["digital", "imagery"],
"gun-metal-gray": ["imagery"],
"pacific-purple": ["imagery"],
"dove-dusk": ["imagery"],
"frost-blue": ["imagery"],
"cobalt": ["imagery"],
"grapevine": ["imagery"],
"lake": ["imagery"],
"midnight": ["imagery"],
"silver": ["imagery"],
"black": ["imagery"],
"bossanova-blue": ["imagery"],
"brite-white": ["imagery"],
"disco-teal": ["imagery"],
"euro-red": ["imagery"],
"titanium": ["imagery"],
"copper-harbor": ["imagery"],
"grapevine-purple": ["imagery"],
"la-bomba-pink": ["imagery"],
"samba-orange": ["imagery"],
"sparkle-silver": ["imagery"],
"garnet-titanium": ["imagery"],
"vineyard-gold": ["imagery"],
"dusk": ["imagery"],
"sparkling-silver": ["imagery"],
"warm-white-lake-blue": ["imagery"],
"white-lake-blue": ["imagery"],
"innovate-orange": ["imagery"],
"peri-blue": ["imagery"],
"frost-green": ["imagery"],
"cobalt-blue": ["imagery"],
"emerald-green": ["imagery"],
"md-green": ["imagery"],
"baha-blue": ["imagery"]
},
"6": {
"imagery": ["hero", "ls"],
"digital": ["a-plus", "banner"]
},
"7": {
"hero": ["cmyk", "rgb"],
"ls": ["cmyk", "rgb"],
"a-plus": ["cmyk", "rgb"],
"banner": ["cmyk", "rgb"],
"standard": ["cmyk", "rgb"],
"diagram": ["cmyk", "rgb"]
}
}
};
return structureData;
}
}
let currentStep = 1;
const totalSteps = 8;
// Initialize the app
async function init() {
await loadProductData();
populateProductCategories();
updateStepDisplay();
}
// Tab management
function showTab(tabName) {
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
document.querySelectorAll('.nav-tab').forEach(tab => {
tab.classList.remove('active');
});
document.getElementById(tabName).classList.add('active');
event.target.classList.add('active');
}
// Step management
function nextStep() {
if (validateCurrentStep()) {
if (currentStep < totalSteps) {
currentStep++;
updateStepDisplay();
}
}
}
function previousStep() {
if (currentStep > 1) {
currentStep--;
updateStepDisplay();
}
}
function updateStepDisplay() {
document.querySelectorAll('.step').forEach(step => {
step.classList.remove('active');
});
document.getElementById(`step-${currentStep}`).classList.add('active');
for (let i = 1; i <= totalSteps; i++) {
const indicator = document.getElementById(`indicator-${i}`);
indicator.classList.remove('active', 'completed');
if (i < currentStep) {
indicator.classList.add('completed');
} else if (i === currentStep) {
indicator.classList.add('active');
}
}
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
prevBtn.style.display = currentStep > 1 ? 'inline-block' : 'none';
if (currentStep === totalSteps) {
nextBtn.style.display = 'none';
} else {
nextBtn.style.display = 'inline-block';
nextBtn.textContent = 'Next →';
}
// Auto-focus on the current step's input/select field for better keyboard navigation
setTimeout(() => {
const currentStepElement = document.getElementById(`step-${currentStep}`);
if (currentStepElement) {
const input = currentStepElement.querySelector('input, select');
if (input) {
input.focus();
}
}
}, 100); // Small delay to ensure step transition is complete
}
function validateCurrentStep() {
switch (currentStep) {
case 1:
const omgJobNumber = document.getElementById('omgJobNumber').value;
if (!omgJobNumber.trim()) {
alert('Please enter an OMG job number.');
return false;
}
break;
case 2:
if (!document.getElementById('productCategory').value) {
alert('Please select a product category.');
return false;
}
break;
case 3:
if (!document.getElementById('productType').value) {
alert('Please select a product type.');
return false;
}
break;
case 4:
if (!document.getElementById('gpdNumber').value) {
alert('Please select a GPD number.');
return false;
}
break;
case 5:
if (!document.getElementById('productName').value) {
alert('Please select a product name.');
return false;
}
break;
case 6:
if (!document.getElementById('assetTypeA').value) {
alert('Please select an asset type A.');
return false;
}
break;
case 7:
if (!document.getElementById('assetTypeB').value) {
alert('Please select an asset type B.');
return false;
}
break;
case 8:
// Final step - validation happens in generateFilename
break;
}
return true;
}
// Populate dropdowns
function populateProductCategories() {
if (!structureData) {
console.error('Structure data not loaded yet');
return;
}
const select = document.getElementById('productCategory');
structureData.productCategories.forEach(category => {
const option = document.createElement('option');
option.value = category;
option.textContent = category.charAt(0).toUpperCase() + category.slice(1);
select.appendChild(option);
});
}
function updateProductTypes() {
if (!structureData) return;
const category = document.getElementById('productCategory').value;
const select = document.getElementById('productType');
select.innerHTML = '<option value="">-- Select Product Type --</option>';
if (category && structureData.dropdownRelationships["1"][category]) {
structureData.dropdownRelationships["1"][category].forEach(type => {
const option = document.createElement('option');
option.value = type;
option.textContent = type.charAt(0).toUpperCase() + type.slice(1);
select.appendChild(option);
});
}
// Clear subsequent dropdowns
clearDropdowns(['gpdNumber', 'productName', 'assetTypeA', 'assetTypeB', 'assetTypeC']);
}
function updateGPDNumbers() {
if (!structureData) return;
const productType = document.getElementById('productType').value;
const productCategory = document.getElementById('productCategory').value;
const select = document.getElementById('gpdNumber');
select.innerHTML = '<option value="">-- Select GPD Number --</option>';
// Only add "Generic" option for consumables
if (productCategory === 'consumables') {
const genericOption = document.createElement('option');
genericOption.value = 'generic';
genericOption.textContent = 'Generic / No Specific GPD';
select.appendChild(genericOption);
}
if (productType && structureData.dropdownRelationships["2"][productType]) {
const items = structureData.dropdownRelationships["2"][productType];
// Check if this leads to asset types (like "common" consumables) or actual GPD numbers
const hasGPDNumbers = items.some(item => item.startsWith('p') && item.match(/p\d/));
items.forEach(item => {
const option = document.createElement('option');
option.value = item;
// Format display based on whether it's a GPD number or asset type
if (hasGPDNumbers || item.startsWith('p')) {
option.textContent = item.toUpperCase();
} else {
// This is probably an asset type, format it nicely
option.textContent = item.charAt(0).toUpperCase() + item.slice(1).replace(/-/g, ' ');
}
select.appendChild(option);
});
}
clearDropdowns(['productName', 'assetTypeA', 'assetTypeB', 'assetTypeC']);
}
function updateProductNames() {
if (!structureData) return;
const gpdNumber = document.getElementById('gpdNumber').value;
const productCategory = document.getElementById('productCategory').value;
const select = document.getElementById('productName');
select.innerHTML = '<option value="">-- Select Product Name --</option>';
// Only add "Generic" option for consumables
if (productCategory === 'consumables') {
const genericOption = document.createElement('option');
genericOption.value = 'generic';
genericOption.textContent = 'Generic / No Specific Product';
select.appendChild(genericOption);
}
// Collect product names based on category
const allProductNames = new Set();
// If we have a specific GPD selected, show its products
if (gpdNumber && gpdNumber !== 'generic' && structureData.dropdownRelationships["3"][gpdNumber]) {
structureData.dropdownRelationships["3"][gpdNumber].forEach(name => {
allProductNames.add(name);
});
}
// For consumables only, also show all available product names when using generic GPD
if (productCategory === 'consumables' && (gpdNumber === 'generic' || !gpdNumber)) {
// Add all product names from all GPD numbers
if (structureData.dropdownRelationships["3"]) {
Object.values(structureData.dropdownRelationships["3"]).forEach(names => {
if (Array.isArray(names)) {
names.forEach(name => allProductNames.add(name));
}
});
}
}
// Add all collected product names to the dropdown
const sortedNames = Array.from(allProductNames).sort();
sortedNames.forEach(name => {
const option = document.createElement('option');
option.value = name;
option.textContent = name.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
select.appendChild(option);
});
clearDropdowns(['assetTypeA', 'assetTypeB', 'assetTypeC']);
hideColorwayGroup();
updateAssetTypeA();
}
function updateAssetTypeA() {
if (!structureData) return;
const productName = document.getElementById('productName').value;
const select = document.getElementById('assetTypeA');
select.innerHTML = '<option value="">-- Select Asset Type A --</option>';
if (productName) {
// Always show all basic asset types for any product
const basicAssetTypes = ['digital', 'imagery', 'logo', 'parts'];
basicAssetTypes.forEach(assetType => {
const option = document.createElement('option');
option.value = assetType;
option.textContent = assetType.charAt(0).toUpperCase() + assetType.slice(1);
select.appendChild(option);
});
}
}
function updateAssetTypeB() {
if (!structureData) return;
const assetTypeA = document.getElementById('assetTypeA').value;
const select = document.getElementById('assetTypeB');
select.innerHTML = '<option value="">-- Select Asset Type B --</option>';
if (assetTypeA && structureData.dropdownRelationships["6"][assetTypeA]) {
structureData.dropdownRelationships["6"][assetTypeA].forEach(assetType => {
const option = document.createElement('option');
option.value = assetType;
option.textContent = assetType.charAt(0).toUpperCase() + assetType.slice(1);
select.appendChild(option);
});
}
// Always show colorway dropdown - it will show NCW if no colorways available
showColorwayGroup();
clearDropdowns(['assetTypeC']);
}
function updateAssetTypeC() {
if (!structureData) return;
const assetTypeB = document.getElementById('assetTypeB').value;
const select = document.getElementById('assetTypeC');
const group = document.getElementById('assetTypeCGroup');
select.innerHTML = '<option value="">-- Select Color Profile --</option>';
if (assetTypeB && structureData.dropdownRelationships["7"][assetTypeB]) {
if (structureData.dropdownRelationships["7"][assetTypeB].length > 0) {
group.style.display = 'block';
structureData.dropdownRelationships["7"][assetTypeB].forEach(colorProfile => {
const option = document.createElement('option');
option.value = colorProfile;
option.textContent = colorProfile.toUpperCase();
select.appendChild(option);
});
} else {
group.style.display = 'none';
}
} else {
group.style.display = 'none';
}
}
function showColorwayGroup() {
if (!structureData) return;
const productName = document.getElementById('productName').value;
const colorwayGroup = document.getElementById('colorwayGroup');
const colorwaySelect = document.getElementById('colorway');
if (productName) {
if (structureData.dropdownRelationships["4"][productName]) {
// Product has colorways - show them
colorwaySelect.innerHTML = '<option value="">-- Select Colorway (Optional) --</option>';
colorwaySelect.innerHTML += '<option value="NCW">NCW (No Colorway)</option>';
structureData.dropdownRelationships["4"][productName].forEach(color => {
const option = document.createElement('option');
option.value = color;
option.textContent = color.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
colorwaySelect.appendChild(option);
});
} else {
// Product has no colorways - default to NCW
colorwaySelect.innerHTML = '<option value="">-- Select Colorway --</option>';
colorwaySelect.innerHTML += '<option value="NCW">NCW (No Colorway)</option>';
}
colorwayGroup.style.display = 'block';
}
}
function hideColorwayGroup() {
const colorwayGroup = document.getElementById('colorwayGroup');
const colorwaySelect = document.getElementById('colorway');
colorwayGroup.style.display = 'none';
colorwaySelect.innerHTML = '<option value="">-- Select Colorway (Optional) --</option>';
}
function clearDropdowns(dropdownIds) {
dropdownIds.forEach(id => {
const select = document.getElementById(id);
if (select) {
let placeholder = '';
switch(id) {
case 'productName':
placeholder = '-- Select Product Name --';
break;
case 'assetTypeA':
placeholder = '-- Select Asset Type A --';
break;
case 'assetTypeB':
placeholder = '-- Select Asset Type B --';
break;
case 'assetTypeC':
placeholder = '-- Select Color Profile --';
break;
case 'colorway':
placeholder = '-- Select Colorway (Optional) --';
break;
default:
placeholder = `-- Select ${id.replace(/([A-Z])/g, ' $1').trim()} --`;
}
select.innerHTML = `<option value="">${placeholder}</option>`;
}
});
// Hide groups when clearing
if (dropdownIds.includes('assetTypeC')) {
document.getElementById('assetTypeCGroup').style.display = 'none';
}
if (dropdownIds.includes('colorway')) {
hideColorwayGroup();
}
}
function generateFilename() {
const omgJobNumber = document.getElementById('omgJobNumber').value;
const productCategory = document.getElementById('productCategory').value;
const productType = document.getElementById('productType').value;
const gpdNumber = document.getElementById('gpdNumber').value;
const productName = document.getElementById('productName').value;
const assetTypeA = document.getElementById('assetTypeA').value;
const assetTypeB = document.getElementById('assetTypeB').value;
const assetTypeC = document.getElementById('assetTypeC').value;
const colorway = document.getElementById('colorway').value;
const customDescriptor = document.getElementById('customDescriptor').value;
// Validate required fields (with flexibility for generic options)
if (!omgJobNumber || !productCategory || !productType || !assetTypeA || !assetTypeB || !colorway) {
alert('Please fill in all required fields.');
return;
}
// Validate custom descriptor doesn't contain underscores
if (customDescriptor && customDescriptor.includes('_')) {
alert('Custom descriptor cannot contain underscores. Please use other characters only.');
return;
}
// Handle cases where GPD or Product Name might be generic/missing
const effectiveGPD = gpdNumber || 'generic';
const effectiveProductName = productName || 'generic';
// Build the path components for folder structure
const folderComponents = [
'1-product-assets',
productCategory,
productType,
effectiveGPD,
effectiveProductName,
assetTypeA,
assetTypeB
];
// Build original filename components (without 1-product-assets)
const filenameComponents = [
productCategory,
productType,
effectiveGPD,
effectiveProductName,
assetTypeA,
assetTypeB
];
// Add Asset Type C if present
if (assetTypeC) {
folderComponents.push(assetTypeC);
filenameComponents.push(assetTypeC);
}
// Add colorway to folder path only if it's not NCW
if (colorway && colorway !== 'NCW') {
folderComponents.push(colorway);
}
// Always add colorway to original filename components
filenameComponents.push(colorway);
// Generate original filename
let filename = `${omgJobNumber}_${filenameComponents.join('_')}`;
// Add custom descriptor to original filename if provided
if (customDescriptor && customDescriptor.trim()) {
filename += '_' + customDescriptor.trim();
}
filename += '.ext';
// Generate DAM filename (renamed version)
let damFilenameComponents = [];
// Add custom descriptor first if provided
if (customDescriptor && customDescriptor.trim()) {
damFilenameComponents.push(customDescriptor.trim());
}
// Start from GPD number (skip job number, dry, robot)
damFilenameComponents.push(effectiveGPD);
damFilenameComponents.push(effectiveProductName);
damFilenameComponents.push(assetTypeA);
damFilenameComponents.push(assetTypeB);
if (assetTypeC) {
damFilenameComponents.push(assetTypeC);
}
// Add colorway only if it's not NCW
if (colorway && colorway !== 'NCW') {
damFilenameComponents.push(colorway);
}
// OMG job number is removed from DAM filename and goes to metadata
let damFilename = damFilenameComponents.join('_') + '.ext';
// Generate metadata info
let metadataItems = [];
metadataItems.push(`Original Job Number: ${omgJobNumber}`);
if (productCategory === 'dry' || productType === 'robot') {
metadataItems.push(`Removed from filename: ${productCategory === 'dry' ? 'dry' : ''} ${productType === 'robot' ? 'robot' : ''}`.trim());
}
if (colorway === 'NCW') {
metadataItems.push('Colorway: NCW (No Colorway) - removed from DAM filename');
}
// Custom descriptor is shown in both filenames, not removed
const folderPath = `/content/dam/bissell/${folderComponents.join('/')}/`;
document.getElementById('generatedFilename').textContent = filename;
document.getElementById('generatedPath').textContent = folderPath;
document.getElementById('damFilename').textContent = damFilename;
document.getElementById('metadataInfo').innerHTML = metadataItems.map(item => `${item}`).join('<br>');
document.getElementById('result').style.display = 'block';
}
function copyToClipboard() {
const filename = document.getElementById('generatedFilename').textContent;
navigator.clipboard.writeText(filename).then(() => {
alert('Original filename copied to clipboard!');
});
}
function copyDAMFilenameToClipboard() {
const damFilename = document.getElementById('damFilename').textContent;
navigator.clipboard.writeText(damFilename).then(() => {
alert('DAM filename copied to clipboard!');
});
}
function copyPathToClipboard() {
const path = document.getElementById('generatedPath').textContent;
navigator.clipboard.writeText(path).then(() => {
alert('DAM path copied to clipboard!');
});
}
// Decoder function
function decodeFilename() {
const filename = document.getElementById('filenameInput').value.trim();
const resultDiv = document.getElementById('decodedResult');
if (!filename) {
resultDiv.innerHTML = '';
return;
}
// Remove file extension if present
const nameWithoutExt = filename.replace(/\.[^/.]+$/, '');
// Check for description (pipe delimiter)
let description = '';
let mainFilename = nameWithoutExt;
if (nameWithoutExt.includes('|')) {
const pipeIndex = nameWithoutExt.indexOf('|');
mainFilename = nameWithoutExt.substring(0, pipeIndex);
description = nameWithoutExt.substring(pipeIndex + 1);
}
// Split by underscores
const parts = mainFilename.split('_');
if (parts.length < 6) {
resultDiv.innerHTML = `
<div class="card" style="border-left-color: #e74c3c;">
<h4 style="color: #e74c3c;">❌ Invalid Filename Format</h4>
<p>The filename should have at least 6 parts separated by underscores:</p>
<p><strong>Format:</strong> OMG-JOB-NUMBER_category_type_gpd_product_assetA_assetB_[colorProfile]_[colorway]_[customDescriptor]</p>
<p><strong>Example:</strong> 12345678_dry_robot_p2990_crosswave-robot_imagery_hero_rgb_NCW_250x250banner1.ext</p>
</div>
`;
return;
}
// Parse the components
const components = {
omgJobNumber: parts[0],
productCategory: parts[1],
productType: parts[2],
gpdNumber: parts[3],
productName: parts[4],
assetTypeA: parts[5],
assetTypeB: parts[6] || '',
assetTypeC: '',
colorway: '',
customDescriptor: ''
};
// Handle the last part - could be colorway or custom descriptor
if (parts.length > 6) {
// Check if the last part looks like a custom descriptor (contains letters/numbers but not typical colorway pattern)
const lastPart = parts[parts.length - 1];
const isCustomDescriptor = /\d/.test(lastPart) && !/^[a-z-]+$/.test(lastPart);
if (isCustomDescriptor) {
components.customDescriptor = lastPart;
// Colorway would be second to last if present
if (parts.length > 7) {
const secondToLast = parts[parts.length - 2];
if (secondToLast !== 'rgb' && secondToLast !== 'cmyk') {
components.colorway = secondToLast;
}
}
} else {
components.colorway = lastPart;
}
// Handle color profile (rgb/cmyk)
if (parts.length >= 8) {
let colorProfileIndex = -1;
for (let i = parts.length - 1; i >= 6; i--) {
if (parts[i] === 'rgb' || parts[i] === 'cmyk') {
components.assetTypeC = parts[i];
colorProfileIndex = i;
break;
}
}
// AssetTypeB is typically at index 6
if (parts.length > 6) {
components.assetTypeB = parts[6];
}
}
}
// Build folder path (NCW colorway doesn't create a folder)
const folderComponents = [
'1-product-assets',
components.productCategory,
components.productType,
components.gpdNumber,
components.productName,
components.assetTypeA,
components.assetTypeB
];
if (components.assetTypeC) {
folderComponents.push(components.assetTypeC);
}
if (components.colorway && components.colorway !== 'NCW') {
folderComponents.push(components.colorway);
}
const folderPath = folderComponents.join('/');
// Build result HTML
resultDiv.innerHTML = `
<div class="card" style="border-left-color: #27ae60;">
<h4 style="color: #27ae60;">✅ Decoded Successfully</h4>
<div style="margin: 15px 0;">
<h5>📁 Folder Structure:</h5>
<div class="code" style="background: #f8f9fa; padding: 10px; border-radius: 5px; font-family: monospace;">
${folderPath}/
</div>
</div>
<div style="margin: 15px 0;">
<h5>📄 Filename Components:</h5>
<table style="width: 100%; border-collapse: collapse; margin-top: 10px;">
<tr style="background: #f8f9fa;">
<td style="padding: 8px; border: 1px solid #ddd; font-weight: bold;">Component</td>
<td style="padding: 8px; border: 1px solid #ddd; font-weight: bold;">Value</td>
<td style="padding: 8px; border: 1px solid #ddd; font-weight: bold;">Description</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;">OMG Job Number</td>
<td style="padding: 8px; border: 1px solid #ddd; font-family: monospace;">${components.omgJobNumber}</td>
<td style="padding: 8px; border: 1px solid #ddd;">Project identifier prefix</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;">Product Category</td>
<td style="padding: 8px; border: 1px solid #ddd; font-family: monospace;">${components.productCategory}</td>
<td style="padding: 8px; border: 1px solid #ddd;">Main product line (wet, dry, consumables, etc.)</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;">Product Type</td>
<td style="padding: 8px; border: 1px solid #ddd; font-family: monospace;">${components.productType}</td>
<td style="padding: 8px; border: 1px solid #ddd;">Specific product type (canister, upright, etc.)</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;">GPD Number</td>
<td style="padding: 8px; border: 1px solid #ddd; font-family: monospace;">${components.gpdNumber}</td>
<td style="padding: 8px; border: 1px solid #ddd;">Product development number</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;">Product Name</td>
<td style="padding: 8px; border: 1px solid #ddd; font-family: monospace;">${components.productName}</td>
<td style="padding: 8px; border: 1px solid #ddd;">Specific product model name</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;">Asset Type A</td>
<td style="padding: 8px; border: 1px solid #ddd; font-family: monospace;">${components.assetTypeA}</td>
<td style="padding: 8px; border: 1px solid #ddd;">Primary asset category (digital, imagery, etc.)</td>
</tr>
${components.assetTypeB ? `
<tr>
<td style="padding: 8px; border: 1px solid #ddd;">Asset Type B</td>
<td style="padding: 8px; border: 1px solid #ddd; font-family: monospace;">${components.assetTypeB}</td>
<td style="padding: 8px; border: 1px solid #ddd;">Secondary asset category (hero, banner, etc.)</td>
</tr>
` : ''}
${components.assetTypeC ? `
<tr>
<td style="padding: 8px; border: 1px solid #ddd;">Color Profile</td>
<td style="padding: 8px; border: 1px solid #ddd; font-family: monospace;">${components.assetTypeC}</td>
<td style="padding: 8px; border: 1px solid #ddd;">Color space (RGB or CMYK)</td>
</tr>
` : ''}
${components.colorway ? `
<tr>
<td style="padding: 8px; border: 1px solid #ddd;">Colorway</td>
<td style="padding: 8px; border: 1px solid #ddd; font-family: monospace;">${components.colorway}</td>
<td style="padding: 8px; border: 1px solid #ddd;">Product color variant or NCW (No Colorway)</td>
</tr>
` : ''}
${components.customDescriptor ? `
<tr>
<td style="padding: 8px; border: 1px solid #ddd;">Custom Descriptor</td>
<td style="padding: 8px; border: 1px solid #ddd; font-family: monospace;">${components.customDescriptor}</td>
<td style="padding: 8px; border: 1px solid #ddd;">Custom text (moves to front in DAM filename)</td>
</tr>
` : ''}
${description ? `
<tr>
<td style="padding: 8px; border: 1px solid #ddd;">Description</td>
<td style="padding: 8px; border: 1px solid #ddd; font-family: monospace;">${description}</td>
<td style="padding: 8px; border: 1px solid #ddd;">Legacy description field</td>
</tr>
` : ''}
</table>
</div>
</div>
`;
}
// Add global keyboard navigation
document.addEventListener('keydown', function(event) {
if (event.key === 'Enter') {
// Handle decoder input
if (event.target && event.target.id === 'filenameInput') {
event.preventDefault();
decodeFilename();
return;
}
// Check if we're in the naming wizard tab
const namingTab = document.getElementById('naming-wizard');
if (!namingTab || !namingTab.classList.contains('active')) {
return; // Not in naming wizard, let default behavior happen
}
// Find which step we're currently on
let currentStepNum = currentStep;
// Get the current step's input/select element
const currentStepElement = document.getElementById(`step-${currentStepNum}`);
if (!currentStepElement) return;
const activeInput = currentStepElement.querySelector('input, select');
if (!activeInput) return;
// Check if the current element is focused or if Enter was pressed on it
if (document.activeElement === activeInput || event.target === activeInput) {
event.preventDefault();
// Validate current field has content
if (activeInput.tagName === 'INPUT' && !activeInput.value.trim()) {
console.log('Input field is empty, not advancing');
return;
}
if (activeInput.tagName === 'SELECT' && !activeInput.value) {
console.log('No selection made, not advancing');
return;
}
console.log(`Advancing from step ${currentStepNum} via Enter key`);
// Move to next step
if (currentStepNum < totalSteps) {
nextStep();
} else {
// Final step - generate filename
generateFilename();
}
}
}
});
// Initialize the app when page loads
document.addEventListener('DOMContentLoaded', async function() {
await init();
});
</script>
</body>
</html>