Add Copy as Markdown button for easy email sharing

- New copyMarkdown() function generates clean Markdown with verdict, key dates table, and calculation table
- Uses clipboard API with textarea fallback for broad browser support
- Button shows brief "Copied!" confirmation feedback
- Green emerald button sits alongside existing Calendar and CSV exports

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alessandro Benedetti Admin 2026-03-06 17:03:25 +00:00
parent 006e5e6690
commit 29ad845ce0
2 changed files with 94 additions and 1 deletions

View file

@ -540,7 +540,11 @@
<div class="flex flex-wrap justify-between gap-2 mt-4 no-print">
<button onclick="goToStep(3)" class="px-6 py-2.5 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 rounded-lg font-medium text-sm transition-colors">&larr; Adjust Dates</button>
<div class="flex gap-2">
<div class="flex flex-wrap gap-2">
<button onclick="copyMarkdown()" id="btnCopyMd" class="px-5 py-2.5 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg font-medium text-sm transition-colors flex items-center gap-2">
<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="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/></svg>
Copy as Markdown
</button>
<button onclick="exportICal()" class="px-5 py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium text-sm transition-colors flex items-center gap-2">
<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="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>
Add to Calendar

View file

@ -748,6 +748,95 @@ function csvEscape(str) {
return str;
}
// ---- Copy as Markdown ----
function copyMarkdown() {
if (!lastCalculationData) return;
const data = lastCalculationData;
const briefType = document.getElementById('briefType').selectedOptions[0]?.text || '';
const lines = [];
// Header & summary
lines.push(`## SLA Summary — ${data.briefName || 'Untitled'}`);
lines.push('');
lines.push(`**Brief Type:** ${briefType}`);
lines.push(`**Kick-Off Date:** ${formatDateLong(data.kickOff)}`);
lines.push(`**Required Go-Live:** ${data.goLive ? formatDateLong(data.goLive) : 'Not set'}`);
lines.push(`**Planned End Date (SLA):** ${formatDateLong(data.projectEndDate)}`);
lines.push(`**Suggested Go-Live (SLA):** ${formatDateLong(data.suggestedGoLive)}`);
if (data.canMeet === null) {
lines.push('**Verdict:** N/A — No Go-Live date set');
} else if (data.canMeet) {
lines.push('**Verdict:** ✅ YES — Can meet the deadline');
} else {
const gap = businessDaysBetween(data.goLive, data.projectEndDate);
lines.push(`**Verdict:** ❌ NO — Cannot meet deadline (${gap} business days over)`);
}
// Key Dates table
lines.push('');
lines.push('### Key Dates');
lines.push('');
lines.push('| Stage | Active | Stage Kick-Off | WIP 1 To Approval | Receive Feedback By | Rounds | Stage Complete By |');
lines.push('|---|---|---|---|---|---|---|');
data.stages.forEach(s => {
const active = s.active ? '✅ Y' : '⬜ N';
const kickOff = s.active ? formatDate(s.kickOffDate) : 'n.a.';
const wip = s.active ? (s.noFeedback ? 'n/r' : formatDate(s.wipCompleteDate)) : 'n.a.';
const feedback = s.active ? (s.noFeedback ? 'n/r' : (s.feedbackByDate ? formatDate(s.feedbackByDate) : formatDate(s.wipCompleteDate))) : 'n.a.';
const rounds = s.active ? (s.noFeedback ? 'n/r' : s.rounds) : 'n.a.';
const complete = s.active ? formatDate(s.completeDate) : 'n.a.';
lines.push(`| ${s.label} | ${active} | ${kickOff} | ${wip} | ${feedback} | ${rounds} | ${complete} |`);
});
// Calculation of Days table
lines.push('');
lines.push('### Calculation of Days');
lines.push('');
lines.push('| Stage | Active | Handover | WIP 1 | Feedback | Revisions | Complete By |');
lines.push('|---|---|---|---|---|---|---|');
data.stages.forEach(s => {
const active = s.active ? '✅ Y' : '⬜ N';
const handover = s.active ? s.handover : 0;
const wip = s.active ? s.wip : 0;
const feedback = s.active ? (s.noFeedback ? 'n/r' : s.feedback) : 0;
const revisions = s.active ? (s.noFeedback ? 'n/r' : s.revisions) : 0;
const complete = formatDate(s.completeDate);
lines.push(`| ${s.label} | ${active} | ${handover} | ${wip} | ${feedback} | ${revisions} | ${complete} |`);
});
const markdown = lines.join('\n');
// Copy to clipboard with fallback
const showCopied = () => {
const btn = document.getElementById('btnCopyMd');
const originalHTML = btn.innerHTML;
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="M5 13l4 4L19 7"/></svg> Copied!`;
setTimeout(() => { btn.innerHTML = originalHTML; }, 2000);
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(markdown).then(showCopied).catch(() => {
fallbackCopy(markdown);
showCopied();
});
} else {
fallbackCopy(markdown);
showCopied();
}
}
function fallbackCopy(text) {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
// ---- iCal Export ----
function exportICal() {
if (!lastCalculationData) return;