`).join('');
bristolSelector.querySelectorAll('.bristol-type').forEach(el => {
el.addEventListener('click', () => {
bristolSelector.querySelectorAll('.bristol-type').forEach(i => i.classList.remove('selected'));
el.classList.add('selected');
document.getElementById('bristolType').value = el.dataset.type;
});
});
};
const renderHistory = () => {
historyTableBody.innerHTML = '';
if (diaryEntries.length === 0) {
historyTableBody.innerHTML = `No entries found. `;
return;
}
diaryEntries.forEach(entry => {
const row = document.createElement('tr');
row.innerHTML = `
${new Date(entry.date).toLocaleDateString()}
${entry.bloating}/${entry.pain}/${entry.gas}
Type ${entry.bristolType || 'N/A'}
${entry.stress}
`;
row.querySelector('button[data-id][class*="text-blue-600"]').addEventListener('click', () => editEntry(entry.id));
row.querySelector('button[data-id][class*="text-red-600"]').addEventListener('click', () => deleteEntry(entry.id));
historyTableBody.appendChild(row);
});
};
const renderDashboard = () => {
const hasData = diaryEntries.length > 0;
noDataMessage.style.display = hasData ? 'none' : 'block';
document.getElementById('dashboard-content').querySelectorAll('.chart-container').forEach(c => c.style.display = hasData ? 'block' : 'none');
if (!hasData) {
Object.values(charts).forEach(chart => chart.destroy());
charts = {};
return;
}
const sortedEntries = [...diaryEntries].sort((a, b) => new Date(a.date) - new Date(b.date));
const labels = sortedEntries.map(e => new Date(e.date));
// Symptom Trend Chart
const symptomData = {
labels,
datasets: [
{ label: 'Bloating', data: sortedEntries.map(e => e.bloating), borderColor: '#3b82f6', tension: 0.1 },
{ label: 'Pain', data: sortedEntries.map(e => e.pain), borderColor: '#ef4444', tension: 0.1 },
{ label: 'Gas', data: sortedEntries.map(e => e.gas), borderColor: '#f97316', tension: 0.1 },
]
};
renderChart('symptomTrendChart', 'line', symptomData, 'Symptom Severity Over Time');
// Stress vs Avg Symptom Chart
const stressData = {
labels,
datasets: [
{ label: 'Stress Level', data: sortedEntries.map(e => e.stress), borderColor: '#8b5cf6', tension: 0.1, yAxisID: 'y' },
{ label: 'Avg Symptom', data: sortedEntries.map(e => (e.bloating + e.pain + e.gas) / 3), borderColor: '#22c55e', tension: 0.1, yAxisID: 'y' },
]
};
renderChart('stressSymptomChart', 'line', stressData, 'Stress vs. Average Symptoms');
// Stool Type Chart
const stoolCounts = bristolData.map(b => ({...b, count: 0}));
diaryEntries.forEach(e => {
const type = parseInt(e.bristolType);
if(type >= 1 && type <= 7) stoolCounts[type - 1].count++;
});
const stoolChartData = {
labels: stoolCounts.map(s => `Type ${s.type}`),
datasets: [{
label: 'Frequency',
data: stoolCounts.map(s => s.count),
backgroundColor: ['#A52A2A', '#8B4513', '#D2691E', '#CD853F', '#F4A460', '#DEB887', '#FFE4B5'],
}]
};
renderChart('stoolTypeChart', 'bar', stoolChartData, 'Bristol Stool Type Frequency');
};
const renderChart = (canvasId, type, data, title) => {
if (charts[canvasId]) charts[canvasId].destroy();
const ctx = document.getElementById(canvasId).getContext('2d');
charts[canvasId] = new Chart(ctx, {
type: type,
data: data,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { title: { display: true, text: title, font: { size: 16 } } },
scales: {
x: type === 'line' ? { type: 'time', time: { unit: 'day' } } : {},
y: { beginAtZero: true, max: 10 }
}
}
});
};
const updateAllViews = () => {
renderHistory();
renderDashboard();
};
// --- Form & Entry Logic ---
const handleFormSubmit = (e) => {
e.preventDefault();
const id = document.getElementById('entryId').value;
const entry = {
id: id || Date.now().toString(),
date: document.getElementById('entryDate').value,
bloating: parseInt(document.getElementById('bloating').value),
pain: parseInt(document.getElementById('pain').value),
gas: parseInt(document.getElementById('gas').value),
bristolType: document.getElementById('bristolType').value,
bmFrequency: parseInt(document.getElementById('bmFrequency').value),
stress: parseInt(document.getElementById('stress').value),
sleep: parseInt(document.getElementById('sleep').value),
meals: {
breakfast: document.getElementById('mealBreakfast').value,
lunch: document.getElementById('mealLunch').value,
dinner: document.getElementById('mealDinner').value,
snacks: document.getElementById('mealSnacks').value,
},
medications: document.getElementById('medications').value,
notes: document.getElementById('notes').value,
};
if (!entry.date) {
alert('Please select a date.');
return;
}
if (id) { // Update existing
const index = diaryEntries.findIndex(e => e.id === id);
diaryEntries[index] = entry;
} else { // Add new
diaryEntries.push(entry);
}
saveEntries();
clearForm();
updateAllViews();
updateTabs(3); // Switch to history view
};
const clearForm = () => {
form.reset();
document.getElementById('entryId').value = '';
document.getElementById('entryDate').valueAsDate = new Date();
bristolSelector.querySelectorAll('.bristol-type').forEach(i => i.classList.remove('selected'));
['bloating', 'pain', 'gas', 'stress', 'sleep'].forEach(id => {
document.getElementById(`${id}Value`).textContent = document.getElementById(id).value;
});
};
const editEntry = (id) => {
const entry = diaryEntries.find(e => e.id === id);
if (!entry) return;
document.getElementById('entryId').value = entry.id;
document.getElementById('entryDate').value = entry.date;
['bloating', 'pain', 'gas', 'stress', 'sleep', 'bristolType', 'bmFrequency', 'medications', 'notes'].forEach(key => {
const el = document.getElementById(key);
if (el) el.value = entry[key] || '';
});
['breakfast', 'lunch', 'dinner', 'snacks'].forEach(meal => {
document.getElementById(`meal${meal.charAt(0).toUpperCase() + meal.slice(1)}`).value = entry.meals[meal] || '';
});
// Update sliders and their displays
['bloating', 'pain', 'gas', 'stress', 'sleep'].forEach(id => {
const slider = document.getElementById(id);
const display = document.getElementById(`${id}Value`);
if(slider && display) {
slider.value = entry[id] || 5;
display.textContent = slider.value;
}
});
// Select bristol type
bristolSelector.querySelectorAll('.bristol-type').forEach(i => i.classList.remove('selected'));
const bristolEl = bristolSelector.querySelector(`[data-type='${entry.bristolType}']`);
if (bristolEl) bristolEl.classList.add('selected');
updateTabs(2); // Switch to form
};
const deleteEntry = (id) => {
if (confirm('Are you sure you want to delete this entry?')) {
diaryEntries = diaryEntries.filter(e => e.id !== id);
saveEntries();
updateAllViews();
}
};
// --- PDF Generation ---
const generatePDF = () => {
const { jsPDF } = window.jspdf;
const pdf = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value;
let filteredEntries = diaryEntries;
if (startDate && endDate) {
filteredEntries = diaryEntries.filter(e => e.date >= startDate && e.date <= endDate);
}
if (filteredEntries.length === 0) {
alert('No entries found for the selected date range.');
return;
}
const margin = 15;
const pdfWidth = pdf.internal.pageSize.getWidth();
const pageHeight = pdf.internal.pageSize.getHeight();
let currentY = margin;
const addHeader = () => {
pdf.setFont('helvetica', 'bold');
pdf.setFontSize(22);
pdf.text('IBS Symptom Diary Report', pdfWidth / 2, currentY, { align: 'center' });
currentY += 8;
pdf.setFont('helvetica', 'normal');
pdf.setFontSize(11);
const dateRange = startDate && endDate ? `${startDate} to ${endDate}` : 'All Entries';
pdf.text(`Date Range: ${dateRange}`, pdfWidth / 2, currentY, { align: 'center' });
currentY += 10;
pdf.setDrawColor(200, 200, 200);
pdf.line(margin, currentY, pdfWidth - margin, currentY);
currentY += 10;
};
const addFooter = (page) => {
pdf.setPage(page);
pdf.setFontSize(9);
pdf.setFont('helvetica', 'italic');
pdf.text(`Page ${page}`, pdfWidth / 2, pageHeight - 10, { align: 'center' });
pdf.text('This is not medical advice. Consult a professional.', pdfWidth - margin, pageHeight - 10, { align: 'right' });
};
const checkNewPage = (spaceNeeded) => {
if (currentY + spaceNeeded > pageHeight - 20) {
addFooter(pdf.internal.getNumberOfPages());
pdf.addPage();
currentY = margin;
return true;
}
return false;
};
addHeader();
filteredEntries.forEach((entry, index) => {
checkNewPage(60); // Estimate space for an entry
pdf.setFontSize(14);
pdf.setFont('helvetica', 'bold');
pdf.setFillColor(240, 240, 240);
pdf.rect(margin, currentY, pdfWidth - (margin*2), 8, 'F');
pdf.text(new Date(entry.date).toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }), margin + 2, currentY + 6);
currentY += 12;
pdf.setFontSize(11);
pdf.setFont('helvetica', 'normal');
let textBlock = `Symptoms: Bloating (${entry.bloating}/10), Pain (${entry.pain}/10), Gas (${entry.gas}/10)\n`;
textBlock += `Bowel: Type ${entry.bristolType}, Frequency: ${entry.bmFrequency}\n`;
textBlock += `Triggers: Stress (${entry.stress}/10), Sleep (${entry.sleep}/10)\n`;
textBlock += `Meals:\n B: ${entry.meals.breakfast || 'N/A'}\n L: ${entry.meals.lunch || 'N/A'}\n D: ${entry.meals.dinner || 'N/A'}\n S: ${entry.meals.snacks || 'N/A'}\n`;
textBlock += `Meds: ${entry.medications || 'N/A'}\n`;
textBlock += `Notes: ${entry.notes || 'N/A'}`;
const lines = pdf.splitTextToSize(textBlock, pdfWidth - (margin * 2) - 5);
checkNewPage(lines.length * 5);
pdf.text(lines, margin + 2, currentY);
currentY += lines.length * 5 + 5;
pdf.setDrawColor(220, 220, 220);
pdf.line(margin, currentY, pdfWidth - margin, currentY);
currentY += 5;
});
addFooter(pdf.internal.getNumberOfPages());
pdf.save('IBS-Symptom-Report.pdf');
};
init();
});
