Release v1.1.0: Reports, Scheduling, and Mobile Optimization
This commit is contained in:
251
src/modals.ts
251
src/modals.ts
@@ -292,6 +292,257 @@ export class EditTaskModal extends Modal {
|
||||
});
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Report Modal ============
|
||||
|
||||
export class ReportModal extends Modal {
|
||||
plugin: ImmersePlugin;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
selectedListIds: string[] = [];
|
||||
|
||||
constructor(app: App, plugin: ImmersePlugin) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
|
||||
// Default to last 7 days
|
||||
const today = new Date();
|
||||
this.endDate = today.toISOString().split('T')[0];
|
||||
const weekAgo = new Date(today);
|
||||
weekAgo.setDate(weekAgo.getDate() - 7);
|
||||
this.startDate = weekAgo.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.addClass('immerse-modal', 'immerse-report-modal');
|
||||
contentEl.empty();
|
||||
|
||||
// Header
|
||||
const header = contentEl.createEl('div', { cls: 'immerse-report-header' });
|
||||
header.createEl('h2', { text: '📊 Reports', cls: 'immerse-report-title' });
|
||||
|
||||
// Filters section
|
||||
this.renderFilters(contentEl);
|
||||
|
||||
// Generate button
|
||||
const generateBtn = contentEl.createEl('button', {
|
||||
text: '🔄 Generate Report',
|
||||
cls: 'immerse-btn immerse-btn-primary immerse-report-generate-btn'
|
||||
});
|
||||
generateBtn.addEventListener('click', () => this.renderReport(contentEl));
|
||||
|
||||
// Initial report render
|
||||
this.renderReport(contentEl);
|
||||
}
|
||||
|
||||
renderFilters(container: Element) {
|
||||
const filtersSection = container.createEl('div', { cls: 'immerse-report-filters' });
|
||||
|
||||
// Date range
|
||||
const dateRow = filtersSection.createEl('div', { cls: 'immerse-report-filter-row' });
|
||||
|
||||
new Setting(dateRow)
|
||||
.setName('Start Date')
|
||||
.addText(text => {
|
||||
text.setValue(this.startDate)
|
||||
.onChange(value => this.startDate = value);
|
||||
text.inputEl.type = 'date';
|
||||
});
|
||||
|
||||
new Setting(dateRow)
|
||||
.setName('End Date')
|
||||
.addText(text => {
|
||||
text.setValue(this.endDate)
|
||||
.onChange(value => this.endDate = value);
|
||||
text.inputEl.type = 'date';
|
||||
});
|
||||
|
||||
// Quick filters
|
||||
const quickFilters = filtersSection.createEl('div', { cls: 'immerse-report-quick-filters' });
|
||||
quickFilters.createEl('span', { text: 'Quick select: ', cls: 'immerse-filter-label' });
|
||||
|
||||
const filters = [
|
||||
{ label: 'Today', days: 0 },
|
||||
{ label: 'Last 7 days', days: 7 },
|
||||
{ label: 'Last 30 days', days: 30 },
|
||||
{ label: 'Last 90 days', days: 90 },
|
||||
];
|
||||
|
||||
filters.forEach(filter => {
|
||||
const btn = quickFilters.createEl('button', {
|
||||
text: filter.label,
|
||||
cls: 'immerse-quick-filter-btn'
|
||||
});
|
||||
btn.addEventListener('click', () => {
|
||||
const today = new Date();
|
||||
this.endDate = today.toISOString().split('T')[0];
|
||||
const startDate = new Date(today);
|
||||
startDate.setDate(startDate.getDate() - filter.days);
|
||||
this.startDate = startDate.toISOString().split('T')[0];
|
||||
this.renderReport(container);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
renderReport(container: Element) {
|
||||
// Remove old report if exists
|
||||
const oldReport = container.querySelector('.immerse-report-content');
|
||||
if (oldReport) oldReport.remove();
|
||||
|
||||
// Generate report data
|
||||
const filters: import('./types').ReportFilters = {
|
||||
startDate: this.startDate,
|
||||
endDate: this.endDate,
|
||||
listIds: this.selectedListIds.length > 0 ? this.selectedListIds : undefined,
|
||||
};
|
||||
|
||||
const reportData = this.plugin.generateReport(filters);
|
||||
|
||||
// Create report content
|
||||
const reportContent = container.createEl('div', { cls: 'immerse-report-content' });
|
||||
|
||||
// Check if we have data
|
||||
if (reportData.totalTasks === 0) {
|
||||
reportContent.createEl('div', {
|
||||
text: 'No data available for the selected period. Complete some tasks to see your stats!',
|
||||
cls: 'immerse-no-data-message'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Summary stats
|
||||
this.renderSummaryStats(reportContent, reportData);
|
||||
|
||||
// Time by list (donut chart)
|
||||
if (reportData.timeByList.length > 0) {
|
||||
this.renderTimeByList(reportContent, reportData);
|
||||
}
|
||||
|
||||
// Productivity insights
|
||||
this.renderInsights(reportContent, reportData);
|
||||
|
||||
// Daily breakdown (bar chart - simplified text version)
|
||||
if (reportData.dailyBreakdown.length > 0) {
|
||||
this.renderDailyBreakdown(reportContent, reportData);
|
||||
}
|
||||
}
|
||||
|
||||
renderSummaryStats(container: Element, data: import('./types').ReportData) {
|
||||
const statsGrid = container.createEl('div', { cls: 'immerse-stats-grid' });
|
||||
|
||||
const stats = [
|
||||
{ label: 'TASKS DONE', value: data.totalTasks.toString(), icon: '✓' },
|
||||
{ label: 'TASKS PER DAY', value: data.tasksPerDay.toFixed(1), icon: '📅' },
|
||||
{ label: 'HOURS PER DAY', value: data.hoursPerDay.toFixed(1), icon: '⏰' },
|
||||
{ label: 'MINS PER TASK', value: data.minsPerTask.toString(), icon: '⏱️' },
|
||||
{ label: 'DAY STREAK', value: data.currentStreak.toString(), icon: '🔥' },
|
||||
{ label: 'TOTAL HOURS', value: (data.totalMinutes / 60).toFixed(1), icon: '⌚' },
|
||||
];
|
||||
|
||||
stats.forEach(stat => {
|
||||
const statCard = statsGrid.createEl('div', { cls: 'immerse-stat-card' });
|
||||
statCard.createEl('div', { text: stat.label, cls: 'immerse-stat-label' });
|
||||
const valueRow = statCard.createEl('div', { cls: 'immerse-stat-value-row' });
|
||||
valueRow.createEl('span', { text: stat.icon, cls: 'immerse-stat-icon' });
|
||||
valueRow.createEl('span', { text: stat.value, cls: 'immerse-stat-value' });
|
||||
});
|
||||
}
|
||||
|
||||
renderTimeByList(container: Element, data: import('./types').ReportData) {
|
||||
const section = container.createEl('div', { cls: 'immerse-report-section' });
|
||||
section.createEl('h3', { text: 'Time by List', cls: 'immerse-report-section-title' });
|
||||
|
||||
const listContainer = section.createEl('div', { cls: 'immerse-time-by-list' });
|
||||
|
||||
data.timeByList.forEach(item => {
|
||||
const listItem = listContainer.createEl('div', { cls: 'immerse-list-stat-item' });
|
||||
|
||||
// List info
|
||||
const listInfo = listItem.createEl('div', { cls: 'immerse-list-info' });
|
||||
listInfo.createEl('span', { text: item.listIcon, cls: 'immerse-list-icon' });
|
||||
listInfo.createEl('span', { text: item.listName, cls: 'immerse-list-name' });
|
||||
|
||||
// Progress bar
|
||||
const progressBar = listItem.createEl('div', { cls: 'immerse-list-progress' });
|
||||
const progress = progressBar.createEl('div', { cls: 'immerse-list-progress-fill' });
|
||||
progress.style.width = `${item.percentage}%`;
|
||||
progress.style.backgroundColor = item.listColor;
|
||||
|
||||
// Stats
|
||||
const stats = listItem.createEl('div', { cls: 'immerse-list-stats' });
|
||||
stats.createEl('span', {
|
||||
text: `${item.taskCount} tasks`,
|
||||
cls: 'immerse-list-stat-text'
|
||||
});
|
||||
stats.createEl('span', {
|
||||
text: `${this.plugin.formatTimeHuman(item.minutes)}`,
|
||||
cls: 'immerse-list-stat-text'
|
||||
});
|
||||
stats.createEl('span', {
|
||||
text: `${item.percentage.toFixed(1)}%`,
|
||||
cls: 'immerse-list-stat-percentage'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
renderInsights(container: Element, data: import('./types').ReportData) {
|
||||
const section = container.createEl('div', { cls: 'immerse-report-section' });
|
||||
section.createEl('h3', { text: 'Productivity Insights', cls: 'immerse-report-section-title' });
|
||||
|
||||
const insightsGrid = section.createEl('div', { cls: 'immerse-insights-grid' });
|
||||
|
||||
if (data.mostProductiveHour !== undefined) {
|
||||
const card = insightsGrid.createEl('div', { cls: 'immerse-insight-card' });
|
||||
card.createEl('div', { text: 'MOST PRODUCTIVE HOUR', cls: 'immerse-insight-label' });
|
||||
card.createEl('div', {
|
||||
text: `${data.mostProductiveHour}:00 - ${data.mostProductiveHour + 1}:00`,
|
||||
cls: 'immerse-insight-value'
|
||||
});
|
||||
}
|
||||
|
||||
if (data.mostProductiveDay) {
|
||||
const card = insightsGrid.createEl('div', { cls: 'immerse-insight-card' });
|
||||
card.createEl('div', { text: 'MOST PRODUCTIVE DAY', cls: 'immerse-insight-label' });
|
||||
card.createEl('div', { text: data.mostProductiveDay, cls: 'immerse-insight-value' });
|
||||
}
|
||||
|
||||
if (data.mostProductiveMonth) {
|
||||
const card = insightsGrid.createEl('div', { cls: 'immerse-insight-card' });
|
||||
card.createEl('div', { text: 'MOST PRODUCTIVE MONTH', cls: 'immerse-insight-label' });
|
||||
card.createEl('div', { text: data.mostProductiveMonth, cls: 'immerse-insight-value' });
|
||||
}
|
||||
}
|
||||
|
||||
renderDailyBreakdown(container: Element, data: import('./types').ReportData) {
|
||||
const section = container.createEl('div', { cls: 'immerse-report-section' });
|
||||
section.createEl('h3', { text: 'Daily Breakdown', cls: 'immerse-report-section-title' });
|
||||
|
||||
const table = section.createEl('table', { cls: 'immerse-daily-table' });
|
||||
const thead = table.createEl('thead');
|
||||
const headerRow = thead.createEl('tr');
|
||||
headerRow.createEl('th', { text: 'Date' });
|
||||
headerRow.createEl('th', { text: 'Tasks' });
|
||||
headerRow.createEl('th', { text: 'Hours' });
|
||||
headerRow.createEl('th', { text: 'Pomodoros' });
|
||||
|
||||
const tbody = table.createEl('tbody');
|
||||
// Show last 14 days max
|
||||
const recentData = data.dailyBreakdown.slice(-14);
|
||||
recentData.forEach(day => {
|
||||
const row = tbody.createEl('tr');
|
||||
row.createEl('td', { text: day.date });
|
||||
row.createEl('td', { text: day.tasks.toString() });
|
||||
row.createEl('td', { text: day.hours.toFixed(1) });
|
||||
row.createEl('td', { text: day.pomodoros.toString() });
|
||||
});
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
Reference in New Issue
Block a user