PROJECTED
${metrics.projectedDate}
CURRENT
${state.unitPrefix}${metrics.currentValue.toLocaleString()}
`;
dashboardGrid.appendChild(card);
createGoalChart(document.getElementById(`chart-${goal.id}`), goal, metrics);
});
}
};
const createGoalChart = (canvas, goal, metrics) => {
const { sortedProgress, projectedDate } = metrics;
const today = new Date();
const targetDate = new Date(goal.targetDate);
const progressData = sortedProgress.map(p => ({ x: new Date(p.date), y: p.value }));
const targetData = [{ x: progressData.length > 0 ? progressData[0].x : today, y: goal.targetValue }, { x: targetDate, y: goal.targetValue }];
const projectionData = [];
if (metrics.currentRate > 0 && new Date() < targetDate) {
const lastProgressPoint = progressData[progressData.length - 1] || { x: today, y: goal.startValue };
projectionData.push({x: lastProgressPoint.x, y: lastProgressPoint.y}); // Start from last known point
if (projectedDate !== 'N/A' && projectedDate !== 'Achieved!') {
projectionData.push({ x: new Date(projectedDate), y: goal.targetValue });
}
}
charts[goal.id] = new Chart(canvas.getContext('2d'), {
type: 'line',
data: {
datasets: [{
label: 'Progress',
data: progressData,
borderColor: '#007ace',
backgroundColor: '#007ace',
fill: false,
tension: 0.1
}, {
label: 'Target',
data: targetData,
borderColor: '#28a745',
borderDash: [5, 5],
fill: false,
pointRadius: 0
}, {
label: 'Projection',
data: projectionData,
borderColor: '#ff9f40',
borderDash: [10, 5],
fill: false,
}]
},
options: {
scales: {
x: { type: 'time', time: { unit: 'month' } },
y: { ticks: { callback: value => state.unitPrefix + value } }
}
}
});
};
const renderManagementView = () => {
// Populate goals table
goalsBody.innerHTML = '';
state.goals.forEach(goal => {
goalsBody.innerHTML += `
| ${goal.name} |
|
`;
});
// Populate progress dropdown
progressGoalSelect.innerHTML = state.goals.map(g => `
`).join('');
renderProgressHistory();
};
const renderProgressHistory = () => {
const selectedGoalId = progressGoalSelect.value;
progressBody.innerHTML = '';
const goal = state.goals.find(g => g.id == selectedGoalId);
if (goal) {
const { sortedProgress } = calculateOutlook(goal);
sortedProgress.reverse().forEach(p => {
progressBody.innerHTML += `
| ${new Date(p.date).toLocaleDateString()} | ${state.unitPrefix}${p.value.toLocaleString()} |
`;
});
}
};
// --- Event Handlers ---
unitPrefixInput.addEventListener('input', () => {
state.unitPrefix = unitPrefixInput.value;
render();
});
goalForm.addEventListener('submit', (e) => {
e.preventDefault();
const goalData = {
id: goalIdInput.value ? Number(goalIdInput.value) : Date.now(),
name: goalNameInput.value,
startValue: parseFloat(startValueInput.value),
targetValue: parseFloat(targetValueInput.value),
targetDate: targetDateInput.value,
progress: []
};
const existingIndex = state.goals.findIndex(g => g.id === goalData.id);
if(existingIndex > -1) { // Editing
goalData.progress = state.goals[existingIndex].progress; // Keep existing progress
state.goals[existingIndex] = goalData;
} else { // Adding
goalData.progress.push({ date: new Date().toISOString().slice(0,10), value: goalData.startValue });
state.goals.push(goalData);
}
goalForm.reset();
goalIdInput.value = '';
goalFormTitle.textContent = 'Add a New Goal';
render();
});
clearGoalFormBtn.addEventListener('click', () => {
goalForm.reset();
goalIdInput.value = '';
goalFormTitle.textContent = 'Add a New Goal';
});
window.handleEditGoal = (id) => {
const goal = state.goals.find(g => g.id == id);
if (goal) {
goalIdInput.value = goal.id;
goalNameInput.value = goal.name;
startValueInput.value = goal.startValue;
targetValueInput.value = goal.targetValue;
targetDateInput.value = goal.targetDate;
goalFormTitle.textContent = 'Edit Goal';
goalNameInput.focus();
}
};
window.handleDeleteGoal = (id) => {
if (confirm('Are you sure you want to delete this goal and all its progress?')) {
state.goals = state.goals.filter(g => g.id != id);
render();
}
};
progressGoalSelect.addEventListener('change', renderProgressHistory);
progressForm.addEventListener('submit', e => {
e.preventDefault();
const goal = state.goals.find(g => g.id == progressGoalSelect.value);
if (goal) {
goal.progress.push({
date: progressDateInput.value,
value: parseFloat(progressValueInput.value)
});
progressForm.reset();
render();
}
});
// --- PDF Generation ---
downloadPdfBtn.addEventListener('click', async () => {
const { jsPDF } = jspdf;
const doc = new jsPDF({ orientation: 'p', unit: 'mm', format: 'a4' });
let yPos = 20;
doc.setFontSize(20);
doc.text("Future Outlook Report", doc.internal.pageSize.getWidth() / 2, yPos, { align: 'center' });
yPos += 15;
for (const goal of state.goals) {
if (yPos > 240) { // Check for page break
doc.addPage();
yPos = 20;
}
const metrics = calculateOutlook(goal);
doc.setFontSize(16);
doc.text(goal.name, 14, yPos);
yPos += 8;
// Add chart
const chartCanvas = document.getElementById(`chart-${goal.id}`);
doc.addImage(chartCanvas.toDataURL('image/png'), 'PNG', 14, yPos, 180, 90);
yPos += 100;
// Add metrics table
const tableData = [
['Status', 'Value'],
['Percent Complete', `${metrics.percentComplete.toFixed(1)}%`],
['Current Value', `${state.unitPrefix}${metrics.currentValue.toLocaleString()}`],
['Days Remaining', metrics.daysRemaining.toLocaleString()],
['Projected Completion', metrics.projectedDate]
];
jspdf.autoTable(doc, {
head: [['Metric', 'Value']],
body: tableData.slice(1),
startY: yPos,
theme: 'striped',
headStyles: { fillColor: [0, 122, 206] }
});
yPos = doc.lastAutoTable.finalY + 15;
}
doc.save(`Future-Outlook-Report-${new Date().toISOString().slice(0,10)}.pdf`);
});
// --- Initial Load ---
loadState();
render();
progressDateInput.valueAsDate = new Date();
});