Brief Advisor: amends from Internal Ops Review + asset bracket expansion
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
7d54c040d5
commit
d19f7e7864
3 changed files with 333 additions and 83 deletions
|
|
@ -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
|
||||
},
|
||||
|
|
|
|||
334
market-script.js
334
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) {
|
|||
<svg class="w-8 h-8" fill="none" stroke="white" stroke-width="3" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-bold text-red-800 dark:text-red-300 text-lg">The submission deadline has already passed</p>
|
||||
<p class="text-sm text-red-600 dark:text-red-400 mt-0.5">To go live on <strong>${formatDate(data.goLive)}</strong>, the brief needed to be submitted by <strong>${formatDate(data.submitBy)}</strong>.</p>
|
||||
<p class="font-bold text-red-800 dark:text-red-300 text-lg">The brief acceptance deadline has already passed</p>
|
||||
<p class="text-sm text-red-600 dark:text-red-400 mt-0.5">To complete work by <strong>${formatDate(data.goLive)}</strong>, the brief needed to be accepted by <strong>${formatDate(data.briefAcceptedBy)}</strong>.</p>
|
||||
<p class="text-xs text-red-500 dark:text-red-400 mt-1">Please discuss an adjusted timeline with your project manager.</p>
|
||||
</div>
|
||||
</div>`;
|
||||
|
|
@ -518,9 +552,8 @@ function renderResults(data) {
|
|||
<svg class="w-8 h-8" fill="none" stroke="white" stroke-width="2.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-bold text-green-800 dark:text-green-300 text-lg">Submit your brief by ${formatDate(data.submitBy)}</p>
|
||||
<p class="text-sm text-green-600 dark:text-green-400 mt-0.5">This gives us <strong>${data.totalBizDays} business days</strong> to deliver before your go-live on <strong>${formatDate(data.goLive)}</strong>.</p>
|
||||
${data.syndicationActive ? `<p class="text-xs text-green-500 dark:text-green-400 mt-1">Includes ${CONFIG.meta.syndicationBufferDays}-day syndication buffer for retailer requirements.</p>` : ''}
|
||||
<p class="font-bold text-green-800 dark:text-green-300 text-lg">Brief must be accepted by ${formatDate(data.briefAcceptedBy)}</p>
|
||||
<p class="text-sm text-green-600 dark:text-green-400 mt-0.5">Submit by <strong>${formatDate(data.submitBy)}</strong> to allow processing time. This gives us <strong>${data.totalBizDays} business days</strong> to complete by <strong>${formatDate(data.goLive)}</strong>.</p>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
|
@ -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 = `
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-2 h-2 rounded-full bg-amber-500"></span>
|
||||
<span class="font-medium text-amber-700 dark:text-amber-300">Syndication Buffer</span>
|
||||
</div>
|
||||
<span class="text-amber-600 dark:text-amber-400 text-xs">${CONFIG.meta.syndicationBufferDays} business days</span>
|
||||
`;
|
||||
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 = `<div style="font-family:Calibri,Arial,sans-serif;font-size:14px;color:#1f2937;">`;
|
||||
html += `<h2 style="margin:0 0 12px 0;font-size:18px;">SLA Brief Advisor — Summary</h2>`;
|
||||
html += `<p style="margin:3px 0;"><strong>Brief Type:</strong> ${data.briefType.label}${manualNote}</p>`;
|
||||
html += `<p style="margin:3px 0;"><strong>Required Go-Live:</strong> ${formatDate(data.goLive)}</p>`;
|
||||
html += `<p style="margin:3px 0;"><strong>Oliver Tasks Completed By:</strong> ${formatDate(data.goLive)}</p>`;
|
||||
html += `<p style="margin:3px 0;"><strong>Submit Brief By:</strong> ${formatDate(data.submitBy)}</p>`;
|
||||
html += `<p style="margin:3px 0;"><strong>Brief Accepted By:</strong> ${formatDate(data.briefAcceptedBy)}</p>`;
|
||||
html += `<p style="margin:3px 0;"><strong>Estimated Completion:</strong> ${formatDate(data.estimatedCompletion)}</p>`;
|
||||
html += `<p style="margin:3px 0;"><strong>Total Business Days:</strong> ${data.totalWithBuffer}</p>`;
|
||||
html += `<p style="margin:3px 0 16px 0;"><strong>Earliest Go-Live (if submitted today):</strong> ${formatDate(data.earliestGoLive)}</p>`;
|
||||
html += `<p style="margin:3px 0;"><strong>Total Business Days:</strong> ${data.totalBizDays}</p>`;
|
||||
html += `<p style="margin:3px 0 16px 0;"><strong>Earliest Completion (if accepted today):</strong> ${formatDate(data.earliestCompletion)}</p>`;
|
||||
|
||||
// Timeline table
|
||||
html += `<h3 style="margin:0 0 6px 0;font-size:15px;">Timeline Breakdown</h3>`;
|
||||
|
|
@ -639,29 +664,26 @@ function copyForEmail() {
|
|||
data.stages.forEach(s => {
|
||||
html += `<tr><td style="${tdL}">${s.label}</td><td style="${td}">${s.days}</td><td style="${td}">${formatDate(s.startDate)}</td><td style="${td}">${formatDate(s.endDate)}</td></tr>`;
|
||||
});
|
||||
if (data.syndicationActive) {
|
||||
html += `<tr><td style="${tdL}">Syndication Buffer</td><td style="${td}">${CONFIG.meta.syndicationBufferDays}</td></tr>`;
|
||||
}
|
||||
html += `</table>`;
|
||||
|
||||
// Disclaimer
|
||||
html += `<p style="margin:12px 0 0;font-size:12px;color:#92400e;"><strong>Assumes 2-week lead time between syndication and Live Date.</strong></p>`;
|
||||
html += `<p style="margin:4px 0;font-size:12px;color:#92400e;"><strong>These timings are for reference only. Please confirm final deadlines with your BCM after studio evaluation. Creative requests follow ad-hoc production timelines.</strong></p>`;
|
||||
html += `<p style="margin:12px 0 0;font-size:12px;color:#92400e;"><strong>These timings are for reference only and assume suitable working files have been provided.</strong></p>`;
|
||||
html += `<p style="margin:4px 0;font-size:12px;color:#92400e;"><strong>Please confirm final deadlines with your BCM after studio evaluation. Creative requests follow ad-hoc production timelines.</strong></p>`;
|
||||
html += `</div>`;
|
||||
|
||||
// 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 = `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2"/></svg> Show Gantt`;
|
||||
} else {
|
||||
btn.innerHTML = `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2"/></svg> 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 = '<p class="text-gray-500 text-sm">No active stages to display.</p>';
|
||||
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 += `<div class="absolute border-l border-gray-200 dark:border-gray-600 text-[10px] font-semibold text-gray-500 dark:text-gray-400 pl-1 truncate leading-4" style="left:${i * DAY_W}px;width:${7 * DAY_W}px">${day.getDate()} ${MONTHS[day.getMonth()]}</div>`;
|
||||
}
|
||||
|
||||
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 += `<div class="absolute border-l border-gray-100 dark:border-gray-700/60 text-[9px] text-center leading-4 ${textCls}" style="left:${i * DAY_W}px;width:${DAY_W}px">${letter}</div>`;
|
||||
if (isWeekend) {
|
||||
weekendBands += `<div class="absolute top-0 bottom-0 bg-gray-100 dark:bg-gray-800/40 pointer-events-none" style="left:${i * DAY_W}px;width:${DAY_W}px"></div>`;
|
||||
}
|
||||
});
|
||||
|
||||
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 += `<div class="absolute ${color} rounded h-full flex items-center px-1.5 text-white text-[10px] font-semibold shadow-sm overflow-hidden" style="left:${leftPx}px;width:${widthPx}px" title="${phase.label}: ${formatDate(phase.start)} → ${formatDate(phase.end)}">${phase.label}</div>`;
|
||||
});
|
||||
} else {
|
||||
const leftPx = toDayIdx(s.startDate) * DAY_W;
|
||||
const widthPx = Math.max((toDayIdx(s.endDate) - toDayIdx(s.startDate)) * DAY_W, DAY_W);
|
||||
barsHtml = `<div class="absolute bg-violet-600 dark:bg-violet-700 rounded h-full flex items-center px-1.5 text-white text-[10px] font-semibold shadow-sm overflow-hidden" style="left:${leftPx}px;width:${widthPx}px" title="${s.label}: ${formatDate(s.startDate)} → ${formatDate(s.endDate)}">${s.label}</div>`;
|
||||
}
|
||||
|
||||
rows += `
|
||||
<div class="flex items-center ${idx > 0 ? 'border-t border-gray-100 dark:border-gray-700/50' : ''}">
|
||||
<div class="shrink-0 text-right pr-3 py-2" style="width:${LABEL_W}px">
|
||||
<span class="text-[11px] font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wide leading-tight">${s.label}</span>
|
||||
</div>
|
||||
<div class="relative overflow-hidden" style="width:${timelineWidth}px;height:28px">
|
||||
${weekendBands}
|
||||
${barsHtml}
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
const goLivePx = toDayIdx(data.goLive) * DAY_W;
|
||||
const goLiveMarker = `
|
||||
<div class="absolute top-0 bottom-0 z-10 pointer-events-none" style="left:${goLivePx}px">
|
||||
<div class="w-0 h-full border-l-2 border-dashed border-red-400 dark:border-red-500"></div>
|
||||
<div class="absolute -top-5 -translate-x-1/2 text-[9px] font-bold text-red-500 dark:text-red-400 whitespace-nowrap">DEADLINE</div>
|
||||
</div>`;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="overflow-x-auto">
|
||||
<div style="min-width:${LABEL_W + timelineWidth}px">
|
||||
<div class="flex items-stretch">
|
||||
<div class="shrink-0" style="width:${LABEL_W}px"></div>
|
||||
<div class="relative" style="width:${timelineWidth}px;height:16px">${weekRow}</div>
|
||||
</div>
|
||||
<div class="flex items-stretch mb-1">
|
||||
<div class="shrink-0" style="width:${LABEL_W}px"></div>
|
||||
<div class="relative" style="width:${timelineWidth}px;height:14px">${dayRow}</div>
|
||||
</div>
|
||||
<div class="relative border-t border-gray-200 dark:border-gray-700">
|
||||
${goLiveMarker}
|
||||
${rows}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-4 mt-4 pt-3 border-t border-gray-200 dark:border-gray-700 text-xs text-gray-400 dark:text-gray-500">
|
||||
<span class="flex items-center gap-1.5"><span class="w-3 h-3 rounded bg-violet-600 inline-block"></span> WIP</span>
|
||||
<span class="flex items-center gap-1.5"><span class="w-3 h-3 rounded bg-amber-400 inline-block"></span> Feedback</span>
|
||||
<span class="flex items-center gap-1.5"><span class="w-3 h-3 rounded bg-violet-400 inline-block"></span> Revision WIP</span>
|
||||
<span class="flex items-center gap-1.5"><span class="w-3 h-3 rounded bg-gray-100 border border-gray-200 inline-block"></span> Weekend</span>
|
||||
<span class="flex items-center gap-1.5"><span class="w-4 border-t-2 border-dashed border-red-400 inline-block"></span> Deadline</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
|
|||
78
market.html
78
market.html
|
|
@ -20,6 +20,7 @@
|
|||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/themes/airbnb.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
|
||||
<style>
|
||||
.fade-in { animation: fadeIn 0.3s ease-in-out; }
|
||||
@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
||||
|
|
@ -50,7 +51,7 @@
|
|||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<p class="text-sm font-bold text-amber-800 dark:text-amber-200">These timings are for reference only.</p>
|
||||
<p class="text-sm font-bold text-amber-800 dark:text-amber-200">These timings are for reference only and assume suitable working files have been provided.</p>
|
||||
<p class="text-sm font-semibold text-amber-700 dark:text-amber-300 mt-1">Please confirm final deadlines with your BCM after studio evaluation. Creative requests follow ad-hoc production timelines.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -83,6 +84,8 @@
|
|||
<option value="0_50">0 – 50</option>
|
||||
<option value="50_100">50 – 100</option>
|
||||
<option value="100_200">100 – 200</option>
|
||||
<option value="200_300">200 – 300</option>
|
||||
<option value="300_400">300 – 400</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
|
@ -127,6 +130,11 @@
|
|||
<span class="text-xs font-medium text-gray-600 dark:text-gray-400">Days needed for feedback <span class="font-normal text-gray-400 dark:text-gray-500">(30 min* 0.3 response time for urgent brief)</span></span>
|
||||
<input type="number" id="feedbackDaysStatic" min="0" max="30" step="0.1" value="3" class="w-16 text-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1 text-sm focus:ring-2 focus:ring-brand-500 outline-none">
|
||||
</label>
|
||||
<!-- Rounds of amends -->
|
||||
<label class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium text-gray-600 dark:text-gray-400">Rounds of amends</span>
|
||||
<input type="number" id="amendRoundsStatic" min="1" max="10" step="1" value="1" class="w-16 text-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1 text-sm focus:ring-2 focus:ring-brand-500 outline-none">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -163,6 +171,11 @@
|
|||
<span class="text-xs font-medium text-gray-600 dark:text-gray-400">Days needed for feedback</span>
|
||||
<input type="number" id="feedbackDaysVideo" min="0" max="30" step="0.1" value="3" class="w-16 text-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1 text-sm focus:ring-2 focus:ring-brand-500 outline-none">
|
||||
</label>
|
||||
<!-- Rounds of amends -->
|
||||
<label class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium text-gray-600 dark:text-gray-400">Rounds of amends</span>
|
||||
<input type="number" id="amendRoundsVideo" min="1" max="10" step="1" value="1" class="w-16 text-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1 text-sm focus:ring-2 focus:ring-brand-500 outline-none">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -179,11 +192,15 @@
|
|||
<div class="absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full shadow peer-checked:translate-x-5 transition-transform"></div>
|
||||
</div>
|
||||
</label>
|
||||
<div id="feedbackHTML" class="hidden border-t border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/30 px-3 py-2.5">
|
||||
<div id="feedbackHTML" class="hidden border-t border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/30 px-3 py-2.5 space-y-3">
|
||||
<label class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium text-gray-600 dark:text-gray-400">Days needed for feedback</span>
|
||||
<input type="number" id="feedbackDaysHTML" min="0" max="30" step="0.1" value="3" class="w-16 text-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1 text-sm focus:ring-2 focus:ring-brand-500 outline-none">
|
||||
</label>
|
||||
<label class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium text-gray-600 dark:text-gray-400">Rounds of amends</span>
|
||||
<input type="number" id="amendRoundsHTML" min="1" max="10" step="1" value="1" class="w-16 text-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1 text-sm focus:ring-2 focus:ring-brand-500 outline-none">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -200,11 +217,15 @@
|
|||
<div class="absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full shadow peer-checked:translate-x-5 transition-transform"></div>
|
||||
</div>
|
||||
</label>
|
||||
<div id="feedbackTranslation" class="hidden border-t border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/30 px-3 py-2.5">
|
||||
<div id="feedbackTranslation" class="hidden border-t border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/30 px-3 py-2.5 space-y-3">
|
||||
<label class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium text-gray-600 dark:text-gray-400">Days needed for feedback</span>
|
||||
<input type="number" id="feedbackDaysTranslation" min="0" max="30" step="0.1" value="5" class="w-16 text-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1 text-sm focus:ring-2 focus:ring-brand-500 outline-none">
|
||||
</label>
|
||||
<label class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium text-gray-600 dark:text-gray-400">Rounds of amends</span>
|
||||
<input type="number" id="amendRoundsTranslation" min="1" max="10" step="1" value="1" class="w-16 text-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1 text-sm focus:ring-2 focus:ring-brand-500 outline-none">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -226,7 +247,7 @@
|
|||
|
||||
<!-- Go-Live Date -->
|
||||
<div>
|
||||
<label for="golive" class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1.5">When do you need this live?</label>
|
||||
<label for="golive" class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1.5">When do you need Oliver tasks completed by?</label>
|
||||
<input id="golive" type="text" placeholder="Select your go-live date…" class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2.5 text-sm focus:ring-2 focus:ring-brand-500 focus:border-brand-500 outline-none transition-colors" readonly>
|
||||
</div>
|
||||
|
||||
|
|
@ -268,8 +289,8 @@
|
|||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<p class="text-sm font-bold text-amber-800 dark:text-amber-200">Assumes 2-week lead time between syndication and Live Date.</p>
|
||||
<p class="text-sm font-semibold text-amber-700 dark:text-amber-300 mt-1">These timings are for reference only. Please confirm final deadlines with your BCM after studio evaluation. Creative requests follow ad-hoc production timelines.</p>
|
||||
<p class="text-sm font-bold text-amber-800 dark:text-amber-200">These timings are for reference only and assume suitable working files have been provided.</p>
|
||||
<p class="text-sm font-semibold text-amber-700 dark:text-amber-300 mt-1">Please confirm final deadlines with your BCM after studio evaluation. Creative requests follow ad-hoc production timelines.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -278,10 +299,15 @@
|
|||
<div id="timelineSummary" class="space-y-3"></div>
|
||||
|
||||
<!-- Summary Row -->
|
||||
<div id="summaryCards" class="mt-6 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<div id="summaryCards" class="mt-6 grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
|
||||
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-3 text-center">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">Submit Brief By</p>
|
||||
<p id="summarySubmitBy" class="text-sm font-bold text-brand-600 dark:text-brand-400"></p>
|
||||
<p id="summarySubmitBy" class="text-sm font-bold text-gray-700 dark:text-gray-200"></p>
|
||||
<p class="text-[10px] text-gray-400 dark:text-gray-500 mt-0.5">2 days before acceptance</p>
|
||||
</div>
|
||||
<div class="bg-brand-50 dark:bg-brand-900/20 border border-brand-200 dark:border-brand-800 rounded-lg p-3 text-center">
|
||||
<p class="text-xs text-brand-600 dark:text-brand-400 mb-1">Brief Accepted By</p>
|
||||
<p id="summaryBriefAcceptedBy" class="text-sm font-bold text-brand-700 dark:text-brand-300"></p>
|
||||
</div>
|
||||
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-3 text-center">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">Estimated Completion</p>
|
||||
|
|
@ -291,12 +317,40 @@
|
|||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">Total Business Days</p>
|
||||
<p id="summaryDays" class="text-sm font-bold"></p>
|
||||
</div>
|
||||
<div class="bg-brand-50 dark:bg-brand-900/20 border border-brand-200 dark:border-brand-800 rounded-lg p-3 text-center">
|
||||
<p class="text-xs text-brand-600 dark:text-brand-400 mb-1">Earliest Go-Live</p>
|
||||
<p id="summaryEarliest" class="text-sm font-bold text-brand-700 dark:text-brand-300"></p>
|
||||
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-3 text-center">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">Earliest Completion</p>
|
||||
<p id="summaryEarliest" class="text-sm font-bold"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gantt Toggle -->
|
||||
<div class="mt-4 no-print">
|
||||
<button type="button" id="toggleGanttBtn" onclick="toggleGantt()" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 text-sm font-medium hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2"/></svg>
|
||||
Show Gantt
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Gantt Panel -->
|
||||
<div id="ganttView" class="hidden mt-4 bg-gray-50 dark:bg-gray-700/30 rounded-xl border border-gray-200 dark:border-gray-700 p-4">
|
||||
<div class="flex justify-between items-center mb-3 no-print">
|
||||
<h3 class="text-sm font-bold text-gray-700 dark:text-gray-300 uppercase tracking-wide">Timeline Gantt</h3>
|
||||
<button onclick="downloadGanttPNG()" class="px-3 py-1.5 bg-gray-900 dark:bg-white dark:text-gray-900 hover:bg-gray-700 dark:hover:bg-gray-100 text-white rounded-lg font-medium text-xs transition-colors flex items-center gap-1.5">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
||||
Download PNG
|
||||
</button>
|
||||
</div>
|
||||
<div id="ganttChart"></div>
|
||||
</div>
|
||||
|
||||
<!-- SLA one-pager link (hidden until URL is available) -->
|
||||
<div id="slaPagerLink" class="hidden mt-4 text-center">
|
||||
<a href="#" class="inline-flex items-center gap-1.5 text-sm text-brand-600 dark:text-brand-400 hover:underline font-medium">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
||||
Download our SLA guide →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Toggle for urgent brief detail view -->
|
||||
<div id="toggleDetails" class="hidden mt-4 text-center"></div>
|
||||
|
||||
|
|
@ -320,6 +374,6 @@
|
|||
</p>
|
||||
</main>
|
||||
|
||||
<script src="market-script.js?v=2026032401"></script>
|
||||
<script src="market-script.js?v=2026042301"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue