// ============================================================================= // SLA Brief Advisor — Client-Facing Form // Client answers practical questions → we determine the brief type // and calculate the submission deadline from their go-live date. // All business rules loaded from config.json. // ============================================================================= let CONFIG = null; // ---- Bootstrap ---- document.addEventListener('DOMContentLoaded', async () => { await loadConfig(); initDarkMode(); initDatePicker(); bindEvents(); }); async function loadConfig() { try { const res = await fetch('config.json?v=2026031302'); CONFIG = await res.json(); } catch (e) { console.error('Failed to load config.json:', e); } } // ---- Dark Mode ---- function initDarkMode() { const saved = localStorage.getItem('sla-dark-mode'); if (saved === 'true' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches)) { document.documentElement.classList.add('dark'); } updateDarkIcons(); document.getElementById('darkToggle').addEventListener('click', () => { document.documentElement.classList.toggle('dark'); localStorage.setItem('sla-dark-mode', document.documentElement.classList.contains('dark')); updateDarkIcons(); }); } function updateDarkIcons() { const isDark = document.documentElement.classList.contains('dark'); document.getElementById('sunIcon').classList.toggle('hidden', !isDark); document.getElementById('moonIcon').classList.toggle('hidden', isDark); } // ---- Date Picker ---- function initDatePicker() { flatpickr('#golive', { dateFormat: 'Y-m-d', altInput: true, altFormat: 'd M Y', disableMobile: true, minDate: 'today', disable: [function(date) { return date.getDay() === 0 || date.getDay() === 6; }], onChange: () => validateForm() }); } // ---- Event Bindings ---- function bindEvents() { document.getElementById('contentType').addEventListener('change', validateForm); document.getElementById('golive').addEventListener('change', validateForm); document.getElementById('clientForm').addEventListener('submit', onSubmit); document.getElementById('resetBtn').addEventListener('click', resetForm); } function validateForm() { const contentType = document.getElementById('contentType').value; const golive = document.getElementById('golive').value; document.getElementById('calcBtn').disabled = !(contentType && golive); } function onSubmit(e) { e.preventDefault(); calculateAndRender(); } function resetForm() { document.getElementById('contentType').value = ''; document.getElementById('assetVolume').value = '0_30'; document.getElementById('needsStatic').checked = false; document.getElementById('needsVideo').checked = false; document.getElementById('needsHTML').checked = false; document.getElementById('needsTranslation').checked = false; document.getElementById('needsSyndication').checked = false; const goliveEl = document.getElementById('golive'); if (goliveEl._flatpickr) goliveEl._flatpickr.clear(); document.getElementById('calcBtn').disabled = true; document.getElementById('results').classList.add('hidden'); window.scrollTo({ top: 0, behavior: 'smooth' }); } // ---- Determine Brief Type from Form Answers ---- // Maps practical client needs → config brief type ID function determineBriefType() { const contentType = document.getElementById('contentType').value; // pdp | eventing const needsStatic = document.getElementById('needsStatic').checked; const needsVideo = document.getElementById('needsVideo').checked; const needsHTML = document.getElementById('needsHTML').checked; const needsTranslation = document.getElementById('needsTranslation').checked; const needsSyndication = document.getElementById('needsSyndication').checked; const needsProduction = needsStatic || needsVideo || needsHTML; // Syndication only — no production, no translation if (needsSyndication && !needsProduction && !needsTranslation) { return 'country_retailer_request'; } // Has production needs if (needsProduction) { // Video/motion or HTML → Adaptation if (needsVideo || needsHTML) return 'country_pull_adaptation'; // Static only → Creation return 'country_pull_creation'; } // Translation only or simple resize/crop if (needsTranslation) { return 'country_pull_simple'; } // Syndication + translation but no production if (needsSyndication) { if (contentType === 'pdp') return 'local_push_pdp'; return 'local_push_eventing'; } // Fallback: simple pull return 'country_pull_simple'; } // ---- Business Day Arithmetic ---- function addBusinessDays(startDate, days) { const result = new Date(startDate); let added = 0; while (added < days) { result.setDate(result.getDate() + 1); if (result.getDay() !== 0 && result.getDay() !== 6) added++; } return result; } function subtractBusinessDays(endDate, days) { const result = new Date(endDate); let removed = 0; while (removed < days) { result.setDate(result.getDate() - 1); if (result.getDay() !== 0 && result.getDay() !== 6) removed++; } return result; } function businessDaysBetween(start, end) { let count = 0; const d = new Date(start); while (d < end) { d.setDate(d.getDate() + 1); if (d.getDay() !== 0 && d.getDay() !== 6) count++; } return count; } function formatDate(date) { if (!date) return '--'; const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; return date.getDate() + ' ' + months[date.getMonth()] + ' ' + date.getFullYear(); } // ---- Calculation Engine ---- function calculateAndRender() { const briefId = determineBriefType(); const assetVolume = document.getElementById('assetVolume').value; const goliveStr = document.getElementById('golive').value; const needsTranslation = document.getElementById('needsTranslation').checked; const needsSyndication = document.getElementById('needsSyndication').checked; if (!briefId || !goliveStr) return; const goLive = new Date(goliveStr + 'T00:00:00'); const cfg = CONFIG.stageConfigs; const handoverDays = CONFIG.meta.handoverDays; // Get brief type info const bt = CONFIG.briefTypes.find(b => b.id === briefId); // Start from the stage matrix, then overlay client toggles const matrix = [...CONFIG.stageMatrix[briefId]]; // Force always-active stages (Opera Upload) CONFIG.stages.forEach((stage, i) => { const sCfg = cfg['stage' + stage.number]; if (sCfg && sCfg.alwaysActive) matrix[i] = true; }); // Client toggles can activate translation/syndication even if the base // brief type doesn't include them if (needsTranslation) { matrix[3] = true; // Translation (Salsify PDP) matrix[4] = true; // Translation (Asset) } if (needsSyndication && !matrix[7]) { matrix[7] = true; // Syndication } // Calculate total project days using a temp start date const tempStart = new Date('2026-01-05T00:00:00'); // Arbitrary Monday const stages = []; let currentDate = new Date(tempStart); // Stage 1: DMI Asset Creation const s1 = calcStage(0, matrix[0], currentDate, () => { const opt = cfg.stage1.fields.complexity.options.find(o => o.value === cfg.stage1.fields.complexity.default); return { handover: handoverDays, wip: opt ? opt.days : 0, feedback: cfg.stage1.fields.marketApprovalDays.default, revisions: cfg.stage1.fields.revisionRounds.default * cfg.stage1.fields.revisionDays.default }; }); stages.push(s1); currentDate = s1.end; // Stage 2: Mastering / Copy const s2 = calcStage(1, matrix[1], currentDate, () => { const masterOpt = cfg.stage2.fields.masteringComplexity.options.find(o => o.value === cfg.stage2.fields.masteringComplexity.default); const copyOpt = cfg.stage2.fields.copyComplexity.options.find(o => o.value === cfg.stage2.fields.copyComplexity.default); return { handover: handoverDays, wip: (masterOpt ? masterOpt.days : 0) + (copyOpt ? copyOpt.days : 0), feedback: 0, revisions: 0 }; }); stages.push(s2); currentDate = s2.end; // Stage 3: Global Rollout const s3 = calcStage(2, matrix[2], currentDate, () => { return { handover: handoverDays, wip: cfg.stage3.fields.rolloutDays.default, feedback: 0, revisions: 0 }; }); stages.push(s3); currentDate = s3.end; // Stages 4 & 5: Translation (parallel) const translationActive = matrix[3] || matrix[4]; const s45 = calcStage(3, translationActive, currentDate, () => { const wcOpt = cfg.stage4_5.fields.wordCount.options.find(o => o.value === cfg.stage4_5.fields.wordCount.default); return { handover: handoverDays, wip: wcOpt ? wcOpt.days : 0, feedback: cfg.stage4_5.fields.marketApprovalDays.default, revisions: cfg.stage4_5.fields.revisionRounds.default * cfg.stage4_5.fields.revisionDays.default }; }); stages.push({ ...s45, label: 'Translation' }); currentDate = s45.end; // Stage 6: Production const s6 = calcStage(5, matrix[5], currentDate, () => { const complexity = cfg.stage6.fields.complexity.default; const key = complexity + '_' + assetVolume; const table = cfg.stage6.fields.crossReferenceTable; return { handover: handoverDays, wip: table[key] || 0, feedback: cfg.stage6.fields.marketApprovalDays.default, revisions: cfg.stage6.fields.revisionRounds.default * cfg.stage6.fields.revisionDays.default }; }); stages.push(s6); currentDate = s6.end; // Stage 7: Opera Upload const s7 = calcStage(6, matrix[6], currentDate, () => { const daysMap = cfg.stage7.daysByAssetVolume; return { handover: handoverDays, wip: daysMap[assetVolume] || cfg.stage7.defaultDays, feedback: 0, revisions: 0 }; }); stages.push(s7); currentDate = s7.end; // Stage 8: Syndication const s8 = calcStage(7, matrix[7], currentDate, () => { const complexity = cfg.stage8.fields.complexity.default; const eanVolume = cfg.stage8.fields.eanVolume.default; const key = complexity + '_' + eanVolume; const table = cfg.stage8.fields.crossReferenceTable; return { handover: handoverDays, wip: table[key] || 0, feedback: 0, revisions: 0 }; }); stages.push(s8); // Total business days const totalBizDays = businessDaysBetween(tempStart, s8.end); // Add syndication buffer if applicable let totalWithBuffer = totalBizDays; if (matrix[7]) { totalWithBuffer += CONFIG.meta.syndicationBufferDays; } // Work backwards from go-live const submitBy = subtractBusinessDays(goLive, totalWithBuffer); const estimatedCompletion = addBusinessDays(submitBy, totalBizDays); renderResults({ briefType: bt, stages: stages.filter(s => s.active), goLive, submitBy, estimatedCompletion, totalBizDays, totalWithBuffer, syndicationActive: matrix[7] }); } function calcStage(stageIndex, isActive, prevDate, getParams) { const stage = CONFIG.stages[stageIndex]; if (!isActive) { return { index: stageIndex, label: stage ? stage.label : '', active: false, days: 0, end: new Date(prevDate) }; } const p = getParams(); const totalDays = p.handover + p.wip + p.feedback + p.revisions; return { index: stageIndex, label: stage ? stage.label : '', active: true, days: totalDays, end: addBusinessDays(prevDate, totalDays) }; } // ---- Render Results ---- function renderResults(data) { const resultsEl = document.getElementById('results'); resultsEl.classList.remove('hidden'); // Brief type document.getElementById('resultBriefType').textContent = data.briefType.label; const desc = Array.isArray(data.briefType.description) ? data.briefType.description.join(' · ') : data.briefType.description; document.getElementById('resultBriefDesc').textContent = desc; // Deadline banner const banner = document.getElementById('deadlineBanner'); const today = new Date(); today.setHours(0, 0, 0, 0); const submitDate = new Date(data.submitBy); submitDate.setHours(0, 0, 0, 0); const isPast = submitDate < today; if (isPast) { banner.className = 'rounded-xl p-5 mb-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800'; banner.innerHTML = `

The submission deadline has already passed

To go live on ${formatDate(data.goLive)}, the brief needed to be submitted by ${formatDate(data.submitBy)}.

Please discuss an adjusted timeline with your project manager.

`; } else { banner.className = 'rounded-xl p-5 mb-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800'; banner.innerHTML = `

Submit your brief by ${formatDate(data.submitBy)}

This gives us ${data.totalBizDays} business days to deliver before your go-live on ${formatDate(data.goLive)}.

${data.syndicationActive ? `

Includes ${CONFIG.meta.syndicationBufferDays}-day syndication buffer for retailer requirements.

` : ''}
`; } // Timeline summary const summary = document.getElementById('timelineSummary'); summary.innerHTML = '

What we need to do

'; data.stages.forEach(s => { const row = document.createElement('div'); row.className = 'flex items-center justify-between py-2.5 px-3 rounded-lg bg-gray-50 dark:bg-gray-700/40 text-sm'; row.innerHTML = `
${s.label}
${s.days} business day${s.days !== 1 ? 's' : ''} `; summary.appendChild(row); }); if (data.syndicationActive) { const bufferRow = document.createElement('div'); bufferRow.className = 'flex items-center justify-between py-2.5 px-3 rounded-lg bg-amber-50 dark:bg-amber-900/20 text-sm'; bufferRow.innerHTML = `
Syndication Buffer
${CONFIG.meta.syndicationBufferDays} business days `; summary.appendChild(bufferRow); } // Summary cards document.getElementById('summarySubmitBy').textContent = formatDate(data.submitBy); document.getElementById('summaryCompletion').textContent = formatDate(data.estimatedCompletion); document.getElementById('summaryDays').textContent = data.totalWithBuffer; resultsEl.scrollIntoView({ behavior: 'smooth', block: 'start' }); }