Calculated Weekly Mileage: ${data.totals.totalMileage.toFixed(1)} miles
Goal Status: ${data.totals.totalMileage >= data.totals.peakMileage ? 'GOAL MET' : 'BELOW TARGET'}
Key Workouts: ${escapeHTML(data.keyWorkouts).replace(/\n/g, '
')}
2. Detailed Weekly Schedule
| Day |
Distance |
Workout Type |
${scheduleHTML}
`;
};
const downloadTxt = () => {
const data = getPlanData();
const totals = data.totals;
let content = `RUNNING TRAINING SCHEDULE: ${data.raceName.toUpperCase()}\n`;
content += "========================================================\n\n";
content += "1. GOALS & SUMMARY\n";
content += "--------------------------------------------------------\n";
content += `Plan Duration: ${data.duration} Weeks\n`;
content += `Target Peak Mileage: ${data.goalMileage} miles\n`;
content += `Calculated Weekly Mileage: ${totals.totalMileage.toFixed(1)} miles\n`;
content += `Status: ${totals.totalMileage >= totals.peakMileage ? 'GOAL MET' : 'BELOW TARGET'}\n`;
content += `Key Workouts:\n${data.keyWorkouts}\n\n`;
content += "2. WEEKLY SCHEDULE\n";
content += "--------------------------------------------------------\n";
data.schedule.forEach(item => {
content += `${item.day.padEnd(12)}: ${item.distance.toFixed(1).padEnd(5)} miles | ${WORKOUT_TYPES.find(wt => wt.key === item.type)?.label || item.type}\n`;
});
content += "========================================================\n";
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `training_plan_${data.raceName.replace(/ /g, '_')}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(a.href);
};
const downloadPDF = () => {
if (typeof window.jspdf === 'undefined' || typeof window.jspdf.jsPDF === 'undefined') {
alert('Error: jsPDF library not loaded.');
return;
}
const { jsPDF } = window.jspdf;
const doc = new jsPDF('p', 'mm', 'a4');
const data = getPlanData();
const totals = data.totals;
const margin = 15;
let yPos = 20;
const pageWidth = doc.internal.pageSize.getWidth();
const addTitle = (text, size, color, style = 'bold') => {
if (yPos > 280) { doc.addPage(); yPos = 20; }
doc.setFontSize(size);
doc.setFont(undefined, style);
doc.setTextColor(color[0], color[1], color[2]);
doc.text(text, margin, yPos);
yPos += size / 2 + 3;
};
const addText = (text, size = 10, style = 'normal', indent = 0) => {
doc.setFontSize(size);
doc.setFont(undefined, style);
doc.setTextColor(52, 73, 94);
const lines = doc.splitTextToSize(text, pageWidth - margin * 2 - indent);
if (lines.length * 5 + yPos > 280) { doc.addPage(); yPos = 20; }
doc.text(lines, margin + indent, yPos);
yPos += (lines.length * 5) + 3;
};
// 1. Header
doc.setFontSize(20);
doc.setFont(undefined, 'bold');
doc.setTextColor(44, 62, 80);
doc.text(`Running Training Schedule`, pageWidth / 2, yPos, { align: 'center' });
yPos += 8;
doc.setFontSize(14);
doc.setTextColor(22, 160, 133);
doc.text(data.raceName, pageWidth / 2, yPos, { align: 'center' });
yPos += 10;
// 2. Goals Summary
addTitle("1. Key Goals & Summary", 12, [22, 160, 133]);
const totalStats = [
['Plan Duration:', `${data.duration} Weeks`],
['Target Peak Mileage:', `${data.goalMileage} miles`],
['Calculated Weekly Mileage:', `${totals.totalMileage.toFixed(1)} miles`],
['Status:', totals.totalMileage >= totals.peakMileage ? 'GOAL MET' : 'BELOW TARGET']
];
doc.autoTable({
startY: yPos,
body: totalStats,
theme: 'plain',
styles: { fontSize: 10, cellPadding: 2, textColor: [52, 73, 94] },
columnStyles: { 0: { fontStyle: 'bold', cellWidth: 50 }, 1: { fontStyle: 'bold', textColor: [22, 160, 133] } },
margin: { left: margin, right: pageWidth - 100 }
});
yPos = doc.autoTable.previous.finalY + 5;
addText("Key Workouts:", 10, 'bold');
addText(data.keyWorkouts, 10, 'normal', 5);
yPos += 3;
// 3. Detailed Weekly Schedule
addTitle("2. Detailed Weekly Schedule", 12, [22, 160, 133]);
const scheduleHead = [['Day', 'Distance (Miles)', 'Workout Type']];
const scheduleBody = data.schedule.map(item => [
item.day,
item.distance.toFixed(1),
WORKOUT_TYPES.find(wt => wt.key === item.type)?.label || item.type
]);
doc.autoTable({
startY: yPos,
head: scheduleHead,
body: scheduleBody,
theme: 'grid',
styles: { fontSize: 10, cellPadding: 3, textColor: [52, 73, 94] },
headStyles: { fillColor: [22, 160, 133], textColor: [255, 255, 255] },
columnStyles: { 0: { fontStyle: 'bold', cellWidth: 40 } },
margin: { left: margin, right: margin }
});
yPos = doc.autoTable.previous.finalY + 5;
doc.save(`training_plan_${data.raceName.replace(/ /g, '_')}.pdf`);
};
// --- Calculation Helper ---
const calculateTotals = () => {
const totalMileage = weeklySchedule.reduce((sum, item) => sum + item.distance, 0);
const peakMileage = parseFloat(goalInputs.goalMileage.value) || 0;
return { totalMileage, peakMileage };
};
// --- Event Listeners ---
// Tab Buttons
tabButtons.forEach((btn, index) => {
btn.addEventListener('click', () => showTab(index + 1));
});
// Next/Prev Navigation
nextBtn.addEventListener('click', () => showTab(currentTab + 1));
prevBtn.addEventListener('click', () => showTab(currentTab - 1));
// Tab 2: Live interaction for schedule changes
scheduleGrid.addEventListener('change', (e) => {
if (e.target.dataset.day) {
updateSchedule(e.target.dataset.day, e.target.dataset.field, e.target.value);
}
});
scheduleGrid.addEventListener('keyup', (e) => {
if (e.target.dataset.day && e.target.type === 'number') {
updateSchedule(e.target.dataset.day, e.target.dataset.field, e.target.value);
}
});
// Tab 3 Actions
downloadPdfBtn.addEventListener('click', downloadPDF);
downloadTxtBtn.addEventListener('click', downloadTxt);
// --- Initialization ---
showTab(1);
});