From d19f7e78646b574cdf97694356d30ae89c775df9 Mon Sep 17 00:00:00 2001 From: Phil Dore Date: Thu, 23 Apr 2026 12:13:09 +0100 Subject: [PATCH] Brief Advisor: amends from Internal Ops Review + asset bracket expansion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename 'live by' label to 'When do you need Oliver tasks completed by?' - Remove 2-week syndication buffer from calculation and all caveat text - Add 'Brief Accepted By' + 'Submit Brief By' (2 days earlier) dual lozenges - Update caveat to include 'assume suitable working files have been provided' - Add rounds of amends inputs per content type (Static, Video, HTML, Translation) - Remove handover days from all stage calculations - Port Gantt sub-phase view to Brief Advisor (WIP/Feedback/Revision, Download PNG) - Add hidden SLA one-pager link placeholder (#slaPagerLink) - Restore 200–300 and 300–400 asset volume brackets in Brief Advisor dropdown Co-Authored-By: Claude Sonnet 4.6 --- config.json | 4 +- market-script.js | 334 +++++++++++++++++++++++++++++++++++++---------- market.html | 78 +++++++++-- 3 files changed, 333 insertions(+), 83 deletions(-) diff --git a/config.json b/config.json index ee15020..21822a1 100755 --- a/config.json +++ b/config.json @@ -350,7 +350,9 @@ "daysByAssetVolume": { "0_50": 1, "50_100": 2, - "100_200": 3 + "100_200": 3, + "200_300": 4, + "300_400": 5 }, "defaultDays": 1 }, diff --git a/market-script.js b/market-script.js index dc9d958..ff1daf8 100644 --- a/market-script.js +++ b/market-script.js @@ -162,7 +162,7 @@ function resetForm() { const panel = document.getElementById(cb.dataset.feedback); if (panel) panel.classList.add('hidden'); }); - const feedbackDefaults = { feedbackDaysStatic: 3, feedbackDaysVideo: 3, feedbackDaysHTML: 3, feedbackDaysTranslation: 5 }; + const feedbackDefaults = { feedbackDaysStatic: 3, feedbackDaysVideo: 3, feedbackDaysHTML: 3, feedbackDaysTranslation: 5, amendRoundsStatic: 1, amendRoundsVideo: 1, amendRoundsHTML: 1, amendRoundsTranslation: 1 }; Object.entries(feedbackDefaults).forEach(([id, val]) => { const el = document.getElementById(id); if (el) el.value = val; @@ -265,17 +265,22 @@ function formatDate(date) { return date.getDate() + ' ' + months[date.getMonth()] + ' ' + date.getFullYear(); } -// ---- Read user-supplied feedback days (defaults to 0 if toggle is off) ---- +// ---- Read user-supplied feedback days and amend rounds (defaults to 0/1 if toggle is off) ---- function getUserFeedbackDays() { const videoChecked = document.getElementById('needsVideo').checked; const videoRadio = document.querySelector('input[name="videoWorkType"]:checked'); const videoWorkDays = videoChecked ? (videoRadio && videoRadio.value === 'adapting' ? 4 : 6) : 0; + const getRounds = (id) => parseInt(document.getElementById(id)?.value) || 1; return { - static: document.getElementById('needsStatic').checked ? parseFloat(document.getElementById('feedbackDaysStatic').value) || 0 : 0, - video: videoChecked ? parseFloat(document.getElementById('feedbackDaysVideo').value) || 0 : 0, + static: document.getElementById('needsStatic').checked ? parseFloat(document.getElementById('feedbackDaysStatic').value) || 0 : 0, + staticRounds: document.getElementById('needsStatic').checked ? getRounds('amendRoundsStatic') : 0, + video: videoChecked ? parseFloat(document.getElementById('feedbackDaysVideo').value) || 0 : 0, + videoRounds: videoChecked ? getRounds('amendRoundsVideo') : 0, videoWorkDays, - html: document.getElementById('needsHTML').checked ? parseFloat(document.getElementById('feedbackDaysHTML').value) || 0 : 0, - translation: document.getElementById('needsTranslation').checked ? parseFloat(document.getElementById('feedbackDaysTranslation').value) || 0 : 0 + html: document.getElementById('needsHTML').checked ? parseFloat(document.getElementById('feedbackDaysHTML').value) || 0 : 0, + htmlRounds: document.getElementById('needsHTML').checked ? getRounds('amendRoundsHTML') : 0, + translation: document.getElementById('needsTranslation').checked ? parseFloat(document.getElementById('feedbackDaysTranslation').value) || 0 : 0, + translationRounds: document.getElementById('needsTranslation').checked ? getRounds('amendRoundsTranslation') : 0, }; } @@ -292,7 +297,6 @@ function calculateAndRender(overrideBriefId) { 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); @@ -329,10 +333,14 @@ function calculateAndRender(overrideBriefId) { const stages = []; let currentDate = new Date(tempStart); + // Combined rounds from production toggles — use the max across static/video/html + const productionRounds = Math.max(fb.staticRounds || 0, fb.videoRounds || 0, fb.htmlRounds || 0); + // 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 }; + const rounds = cfg.stage1.fields.revisionRounds.default; + return { wip: opt ? opt.days : 0, feedback: cfg.stage1.fields.marketApprovalDays.default, revisions: rounds * cfg.stage1.fields.revisionDays.default, rounds }; }); stages.push(s1); currentDate = s1.end; @@ -341,14 +349,14 @@ function calculateAndRender(overrideBriefId) { 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 }; + return { wip: (masterOpt ? masterOpt.days : 0) + (copyOpt ? copyOpt.days : 0), feedback: 0, revisions: 0, rounds: 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 }; + return { wip: cfg.stage3.fields.rolloutDays.default, feedback: 0, revisions: 0, rounds: 0 }; }); stages.push(s3); currentDate = s3.end; @@ -358,7 +366,8 @@ function calculateAndRender(overrideBriefId) { const s45 = calcStage(3, translationActive, currentDate, () => { const wcOpt = cfg.stage4_5.fields.wordCount.options.find(o => o.value === cfg.stage4_5.fields.wordCount.default); const translationFeedback = fb.translation > 0 ? fb.translation : cfg.stage4_5.fields.marketApprovalDays.default; - return { handover: handoverDays, wip: wcOpt ? wcOpt.days : 0, feedback: translationFeedback, revisions: cfg.stage4_5.fields.revisionRounds.default * cfg.stage4_5.fields.revisionDays.default }; + const rounds = fb.translationRounds || cfg.stage4_5.fields.revisionRounds.default; + return { wip: wcOpt ? wcOpt.days : 0, feedback: translationFeedback, revisions: rounds * cfg.stage4_5.fields.revisionDays.default, rounds }; }); stages.push({ ...s45, label: 'Translation' }); currentDate = s45.end; @@ -366,14 +375,14 @@ function calculateAndRender(overrideBriefId) { // Stage 6: Production — use user feedback days from production toggles const s6 = calcStage(5, matrix[5], currentDate, () => { if (isUrgentScenario) { - // 6-hour production (1 business day) + brief confirmation time - return { handover: 0, wip: 1, feedback: fb.static, revisions: 0 }; + return { wip: 1, feedback: fb.static, revisions: 0, rounds: 0 }; } const complexity = cfg.stage6.fields.complexity.default; const key = complexity + '_' + assetVolume; const table = cfg.stage6.fields.crossReferenceTable; const prodFeedback = productionFeedback > 0 ? productionFeedback : cfg.stage6.fields.marketApprovalDays.default; - return { handover: handoverDays, wip: (table[key] || 0) + (fb.videoWorkDays || 0), feedback: prodFeedback, revisions: cfg.stage6.fields.revisionRounds.default * cfg.stage6.fields.revisionDays.default }; + const rounds = productionRounds > 0 ? productionRounds : cfg.stage6.fields.revisionRounds.default; + return { wip: (table[key] || 0) + (fb.videoWorkDays || 0), feedback: prodFeedback, revisions: rounds * cfg.stage6.fields.revisionDays.default, rounds }; }); if (isUrgentScenario) s6.label = 'Production (6 hrs + confirmation)'; stages.push(s6); @@ -382,7 +391,7 @@ function calculateAndRender(overrideBriefId) { // 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 }; + return { wip: daysMap[assetVolume] || cfg.stage7.defaultDays, feedback: 0, revisions: 0, rounds: 0 }; }); stages.push(s7); currentDate = s7.end; @@ -393,33 +402,29 @@ function calculateAndRender(overrideBriefId) { 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 }; + return { wip: table[key] || 0, feedback: 0, revisions: 0, rounds: 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 — no assumed syndication buffer + const briefAcceptedBy = subtractBusinessDays(goLive, totalBizDays); + const submitBy = subtractBusinessDays(briefAcceptedBy, 2); + const estimatedCompletion = addBusinessDays(briefAcceptedBy, totalBizDays); - // Work backwards from go-live - const submitBy = subtractBusinessDays(goLive, totalWithBuffer); - const estimatedCompletion = addBusinessDays(submitBy, totalBizDays); - - // Earliest go-live: if brief submitted today, when could we finish? + // Earliest completion: if brief accepted today, when could we finish? const today = new Date(); today.setHours(0, 0, 0, 0); - const earliestGoLive = addBusinessDays(today, totalWithBuffer); + const earliestCompletion = addBusinessDays(today, totalBizDays); - // Compute real calendar dates for each active stage (relative to submitBy) + // Compute real calendar dates + phases for each active stage (relative to briefAcceptedBy) const activeStages = stages.filter(s => s.active); - let runningDate = new Date(submitBy); + let runningDate = new Date(briefAcceptedBy); activeStages.forEach(s => { s.startDate = new Date(runningDate); s.endDate = addBusinessDays(runningDate, s.days); + s.phases = buildPhases(s.params, s.startDate); runningDate = s.endDate; }); @@ -429,12 +434,11 @@ function calculateAndRender(overrideBriefId) { suggestedBriefId: determineBriefType(), stages: activeStages, goLive, + briefAcceptedBy, submitBy, estimatedCompletion, totalBizDays, - totalWithBuffer, - syndicationActive: matrix[7], - earliestGoLive, + earliestCompletion, needsVideo: document.getElementById('needsVideo').checked, isUrgentScenario }); @@ -443,11 +447,41 @@ function calculateAndRender(overrideBriefId) { 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) }; + return { index: stageIndex, label: stage ? stage.label : '', active: false, days: 0, end: new Date(prevDate), params: null, phases: [] }; } 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) }; + const totalDays = p.wip + p.feedback + p.revisions; + return { index: stageIndex, label: stage ? stage.label : '', active: true, days: totalDays, end: addBusinessDays(prevDate, totalDays), params: p, phases: [] }; +} + +function buildPhases(params, startDate) { + if (!params) return []; + const phases = []; + let cursor = new Date(startDate); + + if (params.wip > 0) { + const wipEnd = addBusinessDays(cursor, params.wip); + phases.push({ label: 'WIP 1', type: 'wip', start: new Date(cursor), end: wipEnd }); + cursor = wipEnd; + } + + const rounds = params.rounds || 0; + if (params.feedback > 0) { + const feedbackEnd = addBusinessDays(cursor, params.feedback); + phases.push({ label: rounds > 0 ? 'Feedback 1' : 'Feedback', type: 'feedback', start: new Date(cursor), end: feedbackEnd }); + cursor = feedbackEnd; + } + + if (rounds > 0 && params.revisions > 0) { + const revDaysPerRound = params.revisions / rounds; + for (let r = 1; r <= rounds; r++) { + const revEnd = addBusinessDays(cursor, revDaysPerRound); + phases.push({ label: `WIP ${r + 1}`, type: 'revision', start: new Date(cursor), end: revEnd }); + cursor = revEnd; + } + } + + return phases; } // ---- Render Results ---- @@ -494,8 +528,8 @@ function renderResults(data) { banner.innerHTML = ''; } else { 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; + const acceptDate = new Date(data.briefAcceptedBy); acceptDate.setHours(0, 0, 0, 0); + const isPast = acceptDate < 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'; @@ -505,8 +539,8 @@ function renderResults(data) {
-

The submission deadline has already passed

-

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

+

The brief acceptance deadline has already passed

+

To complete work by ${formatDate(data.goLive)}, the brief needed to be accepted by ${formatDate(data.briefAcceptedBy)}.

Please discuss an adjusted timeline with your project manager.

`; @@ -518,9 +552,8 @@ function renderResults(data) {
-

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.

` : ''} +

Brief must be accepted by ${formatDate(data.briefAcceptedBy)}

+

Submit by ${formatDate(data.submitBy)} to allow processing time. This gives us ${data.totalBizDays} business days to complete by ${formatDate(data.goLive)}.

`; } @@ -583,25 +616,16 @@ function renderResults(data) { 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 — always populated (used by copy-for-email) document.getElementById('summarySubmitBy').textContent = formatDate(data.submitBy); + document.getElementById('summaryBriefAcceptedBy').textContent = formatDate(data.briefAcceptedBy); document.getElementById('summaryCompletion').textContent = formatDate(data.estimatedCompletion); - document.getElementById('summaryDays').textContent = data.totalWithBuffer; - document.getElementById('summaryEarliest').textContent = formatDate(data.earliestGoLive); + document.getElementById('summaryDays').textContent = data.totalBizDays; + document.getElementById('summaryEarliest').textContent = formatDate(data.earliestCompletion); + + renderGantt(); resultsEl.scrollIntoView({ behavior: 'smooth', block: 'start' }); } @@ -626,11 +650,12 @@ function copyForEmail() { let html = `
`; html += `

SLA Brief Advisor — Summary

`; html += `

Brief Type: ${data.briefType.label}${manualNote}

`; - html += `

Required Go-Live: ${formatDate(data.goLive)}

`; + html += `

Oliver Tasks Completed By: ${formatDate(data.goLive)}

`; html += `

Submit Brief By: ${formatDate(data.submitBy)}

`; + html += `

Brief Accepted By: ${formatDate(data.briefAcceptedBy)}

`; html += `

Estimated Completion: ${formatDate(data.estimatedCompletion)}

`; - html += `

Total Business Days: ${data.totalWithBuffer}

`; - html += `

Earliest Go-Live (if submitted today): ${formatDate(data.earliestGoLive)}

`; + html += `

Total Business Days: ${data.totalBizDays}

`; + html += `

Earliest Completion (if accepted today): ${formatDate(data.earliestCompletion)}

`; // Timeline table html += `

Timeline Breakdown

`; @@ -639,29 +664,26 @@ function copyForEmail() { data.stages.forEach(s => { html += `${s.label}${s.days}${formatDate(s.startDate)}${formatDate(s.endDate)}`; }); - if (data.syndicationActive) { - html += `Syndication Buffer${CONFIG.meta.syndicationBufferDays}`; - } html += ``; // Disclaimer - html += `

Assumes 2-week lead time between syndication and Live Date.

`; - html += `

These timings are for reference only. Please confirm final deadlines with your BCM after studio evaluation. Creative requests follow ad-hoc production timelines.

`; + html += `

These timings are for reference only and assume suitable working files have been provided.

`; + html += `

Please confirm final deadlines with your BCM after studio evaluation. Creative requests follow ad-hoc production timelines.

`; html += `
`; // Plain text fallback let plain = `SLA Brief Advisor — Summary\n`; plain += `Brief Type: ${data.briefType.label}${manualNote}\n`; - plain += `Required Go-Live: ${formatDate(data.goLive)}\n`; + plain += `Oliver Tasks Completed By: ${formatDate(data.goLive)}\n`; plain += `Submit Brief By: ${formatDate(data.submitBy)}\n`; + plain += `Brief Accepted By: ${formatDate(data.briefAcceptedBy)}\n`; plain += `Estimated Completion: ${formatDate(data.estimatedCompletion)}\n`; - plain += `Total Business Days: ${data.totalWithBuffer}\n`; - plain += `Earliest Go-Live: ${formatDate(data.earliestGoLive)}\n\n`; + plain += `Total Business Days: ${data.totalBizDays}\n`; + plain += `Earliest Completion (if accepted today): ${formatDate(data.earliestCompletion)}\n\n`; plain += `Timeline:\n`; data.stages.forEach(s => { plain += ` ${s.label}: ${s.days} days (${formatDate(s.startDate)} → ${formatDate(s.endDate)})\n`; }); - if (data.syndicationActive) plain += ` Syndication Buffer: ${CONFIG.meta.syndicationBufferDays} days\n`; - plain += `\nAssumes 2-week lead time between syndication and Live Date.\n`; - plain += `These timings are for reference only. Please confirm final deadlines with your BCM after studio evaluation.\n`; + plain += `\nThese timings are for reference only and assume suitable working files have been provided.\n`; + plain += `Please confirm final deadlines with your BCM after studio evaluation.\n`; const btn = document.getElementById('btnCopyEmail'); const originalHTML = btn.innerHTML; @@ -689,6 +711,178 @@ function copyForEmail() { } } +// ---- Gantt Chart ---- +function toggleGantt() { + const panel = document.getElementById('ganttView'); + const btn = document.getElementById('toggleGanttBtn'); + const isHidden = panel.classList.contains('hidden'); + panel.classList.toggle('hidden', !isHidden); + btn.textContent = isHidden ? 'Hide Gantt' : 'Show Gantt'; + if (!isHidden) { + btn.innerHTML = ` Show Gantt`; + } else { + btn.innerHTML = ` Hide Gantt`; + } +} + +function renderGantt() { + if (!lastResultData) return; + const data = lastResultData; + const container = document.getElementById('ganttChart'); + if (!container) return; + + const activeStgs = data.stages; + if (!activeStgs || activeStgs.length === 0) { + container.innerHTML = '

No active stages to display.

'; + return; + } + + const DAY_W = 28; + const LABEL_W = 176; + + const minDate = data.briefAcceptedBy; + const maxDate = data.goLive; + + const gridStart = new Date(minDate); + gridStart.setHours(0, 0, 0, 0); + gridStart.setDate(gridStart.getDate() - gridStart.getDay()); + + const gridEnd = new Date(maxDate); + gridEnd.setHours(0, 0, 0, 0); + gridEnd.setDate(gridEnd.getDate() + 7); + + const days = []; + const cur = new Date(gridStart); + while (cur <= gridEnd) { + days.push(new Date(cur)); + cur.setDate(cur.getDate() + 1); + } + const timelineWidth = days.length * DAY_W; + + const toDayIdx = (date) => { + const d = new Date(date); + d.setHours(0, 0, 0, 0); + return Math.max(0, Math.round((d - gridStart) / 86400000)); + }; + + const phaseColors = { + wip: 'bg-violet-600 dark:bg-violet-700', + revision: 'bg-violet-400 dark:bg-violet-500', + feedback: 'bg-amber-400 dark:bg-amber-500', + }; + + const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; + const DAY_LETTERS = ['S','M','T','W','T','F','S']; + + let weekRow = ''; + for (let i = 0; i < days.length; i += 7) { + const day = days[i]; + weekRow += `
${day.getDate()} ${MONTHS[day.getMonth()]}
`; + } + + let dayRow = ''; + let weekendBands = ''; + days.forEach((day, i) => { + const isWeekend = day.getDay() === 0 || day.getDay() === 6; + const letter = DAY_LETTERS[day.getDay()]; + const textCls = isWeekend ? 'text-gray-300 dark:text-gray-600 font-medium' : 'text-gray-400 dark:text-gray-500 font-medium'; + dayRow += `
${letter}
`; + if (isWeekend) { + weekendBands += `
`; + } + }); + + let rows = ''; + activeStgs.forEach((s, idx) => { + let barsHtml = ''; + if (s.phases && s.phases.length > 0) { + s.phases.forEach(phase => { + const leftPx = toDayIdx(phase.start) * DAY_W; + const widthDays = Math.max(toDayIdx(phase.end) - toDayIdx(phase.start), 1); + const widthPx = widthDays * DAY_W; + const color = phaseColors[phase.type] || 'bg-violet-600 dark:bg-violet-700'; + barsHtml += `
${phase.label}
`; + }); + } else { + const leftPx = toDayIdx(s.startDate) * DAY_W; + const widthPx = Math.max((toDayIdx(s.endDate) - toDayIdx(s.startDate)) * DAY_W, DAY_W); + barsHtml = `
${s.label}
`; + } + + rows += ` +
+
+ ${s.label} +
+
+ ${weekendBands} + ${barsHtml} +
+
`; + }); + + const goLivePx = toDayIdx(data.goLive) * DAY_W; + const goLiveMarker = ` +
+
+
DEADLINE
+
`; + + container.innerHTML = ` +
+
+
+
+
${weekRow}
+
+
+
+
${dayRow}
+
+
+ ${goLiveMarker} + ${rows} +
+
+
+
+ WIP + Feedback + Revision WIP + Weekend + Deadline +
+ `; +} + +function downloadGanttPNG() { + const el = document.getElementById('ganttChart'); + if (!el || !lastResultData) return; + + const isDark = document.documentElement.classList.contains('dark'); + const btn = el.closest('#ganttView')?.querySelector('button[onclick="downloadGanttPNG()"]'); + if (btn) btn.style.visibility = 'hidden'; + + html2canvas(el, { + backgroundColor: isDark ? '#1f2937' : '#ffffff', + scale: 2, + useCORS: true, + logging: false, + }).then(canvas => { + if (btn) btn.style.visibility = ''; + const a = document.createElement('a'); + const briefLabel = (lastResultData.briefType?.label || 'SLA_Brief').replace(/\s+/g, '_'); + a.download = briefLabel + '_Gantt.png'; + a.href = canvas.toDataURL('image/png'); + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + }).catch(() => { + if (btn) btn.style.visibility = ''; + alert('Could not generate image. Please try again.'); + }); +} + function fallbackCopyHtml(html) { const el = document.createElement('div'); el.innerHTML = html; diff --git a/market.html b/market.html index b131589..6ae7a76 100644 --- a/market.html +++ b/market.html @@ -20,6 +20,7 @@ +