Release v1.1.0: Reports, Scheduling, and Mobile Optimization
This commit is contained in:
261
src/main.ts
261
src/main.ts
@@ -21,6 +21,7 @@ import {
|
||||
} from './types';
|
||||
|
||||
import { ImmerseView } from './view';
|
||||
import { ReportView, VIEW_TYPE_REPORT } from './reportView';
|
||||
import { QuickAddTaskModal } from './modals';
|
||||
|
||||
// ============ Main Plugin Class ============
|
||||
@@ -65,12 +66,17 @@ export default class ImmersePlugin extends Plugin {
|
||||
}
|
||||
});
|
||||
|
||||
// Register the main view
|
||||
// Register views
|
||||
this.registerView(
|
||||
VIEW_TYPE_IMMERSE,
|
||||
(leaf) => new ImmerseView(leaf, this)
|
||||
);
|
||||
|
||||
this.registerView(
|
||||
VIEW_TYPE_REPORT,
|
||||
(leaf) => new ReportView(leaf, this)
|
||||
);
|
||||
|
||||
// Add ribbon icon
|
||||
this.addRibbonIcon('zap', 'Open Immerse', () => {
|
||||
this.activateView();
|
||||
@@ -107,6 +113,12 @@ export default class ImmersePlugin extends Plugin {
|
||||
callback: () => this.completeActiveTask(),
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'view-reports',
|
||||
name: 'View Reports',
|
||||
callback: () => this.activateReportView(),
|
||||
});
|
||||
|
||||
// Add settings tab
|
||||
this.addSettingTab(new ImmerseSettingTab(this.app, this));
|
||||
|
||||
@@ -191,6 +203,26 @@ export default class ImmersePlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
async activateReportView() {
|
||||
const { workspace } = this.app;
|
||||
|
||||
// Check if report view is already open
|
||||
const existingLeaves = workspace.getLeavesOfType(VIEW_TYPE_REPORT);
|
||||
|
||||
if (existingLeaves.length > 0) {
|
||||
// If already open, just reveal it
|
||||
workspace.revealLeaf(existingLeaves[0]);
|
||||
} else {
|
||||
// Open in a new tab in the main area
|
||||
const leaf = workspace.getLeaf('tab');
|
||||
await leaf.setViewState({
|
||||
type: VIEW_TYPE_REPORT,
|
||||
active: true,
|
||||
});
|
||||
workspace.revealLeaf(leaf);
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Task Management ============
|
||||
|
||||
createTask(text: string, estimatedMinutes: number = this.settings.defaultEstimateMinutes, list: string = 'work'): ImmerseTask {
|
||||
@@ -242,30 +274,36 @@ export default class ImmersePlugin extends Plugin {
|
||||
task.completed = true;
|
||||
task.completedAt = Date.now();
|
||||
task.isActive = false;
|
||||
|
||||
|
||||
this.data.completedToday++;
|
||||
this.data.lastActiveDate = new Date().toDateString();
|
||||
|
||||
|
||||
// Archive task for historical reporting
|
||||
this.archiveCompletedTask(task);
|
||||
|
||||
// Update daily stats
|
||||
this.updateDailyStats(task);
|
||||
|
||||
// Show celebration
|
||||
if (this.settings.enableCelebrations) {
|
||||
this.showCelebration(task);
|
||||
}
|
||||
|
||||
|
||||
// Play sound
|
||||
if (this.settings.enableSounds) {
|
||||
this.playCompletionSound();
|
||||
}
|
||||
|
||||
|
||||
// Log to daily note
|
||||
if (this.settings.logToDaily) {
|
||||
this.logTaskToDailyNote(task);
|
||||
}
|
||||
|
||||
|
||||
if (this.activeTaskId === taskId) {
|
||||
this.stopTimer();
|
||||
this.activeTaskId = null;
|
||||
}
|
||||
|
||||
|
||||
this.saveAllData();
|
||||
this.refreshView();
|
||||
}
|
||||
@@ -279,6 +317,215 @@ export default class ImmersePlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Data Archiving & Statistics ============
|
||||
|
||||
archiveCompletedTask(task: ImmerseTask) {
|
||||
// Check if task was overdue
|
||||
const wasOverdue = task.scheduledDate && task.scheduledTime &&
|
||||
new Date(`${task.scheduledDate}T${task.scheduledTime}`).getTime() < (task.completedAt || Date.now());
|
||||
|
||||
const record: import('./types').CompletedTaskRecord = {
|
||||
id: task.id,
|
||||
text: task.text,
|
||||
list: task.list,
|
||||
estimatedMinutes: task.estimatedMinutes,
|
||||
actualMinutes: task.actualMinutes,
|
||||
createdAt: task.createdAt,
|
||||
completedAt: task.completedAt || Date.now(),
|
||||
scheduledDate: task.scheduledDate,
|
||||
wasOverdue: wasOverdue || false,
|
||||
};
|
||||
|
||||
this.data.completedTasksArchive.push(record);
|
||||
}
|
||||
|
||||
updateDailyStats(task: ImmerseTask) {
|
||||
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
||||
|
||||
// Find or create today's stats
|
||||
let todayStats = this.data.dailyStats.find(s => s.date === today);
|
||||
|
||||
if (!todayStats) {
|
||||
todayStats = {
|
||||
date: today,
|
||||
tasksCompleted: 0,
|
||||
totalMinutes: 0,
|
||||
pomodorosCompleted: 0,
|
||||
tasksByList: {},
|
||||
minutesByList: {},
|
||||
};
|
||||
this.data.dailyStats.push(todayStats);
|
||||
}
|
||||
|
||||
// Update stats
|
||||
todayStats.tasksCompleted++;
|
||||
todayStats.totalMinutes += task.actualMinutes;
|
||||
todayStats.pomodorosCompleted = this.data.pomodorosCompleted;
|
||||
|
||||
// Update list-specific stats
|
||||
todayStats.tasksByList[task.list] = (todayStats.tasksByList[task.list] || 0) + 1;
|
||||
todayStats.minutesByList[task.list] = (todayStats.minutesByList[task.list] || 0) + task.actualMinutes;
|
||||
|
||||
// Keep only last 365 days of stats to prevent data bloat
|
||||
if (this.data.dailyStats.length > 365) {
|
||||
this.data.dailyStats.sort((a, b) => b.date.localeCompare(a.date));
|
||||
this.data.dailyStats = this.data.dailyStats.slice(0, 365);
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Report Generation ============
|
||||
|
||||
generateReport(filters: import('./types').ReportFilters): import('./types').ReportData {
|
||||
const { startDate, endDate, listIds } = filters;
|
||||
|
||||
// Filter daily stats by date range
|
||||
const filteredStats = this.data.dailyStats.filter(stat => {
|
||||
return stat.date >= startDate && stat.date <= endDate;
|
||||
});
|
||||
|
||||
// Filter completed tasks by date range and lists
|
||||
const filteredTasks = this.data.completedTasksArchive.filter(task => {
|
||||
const taskDate = new Date(task.completedAt).toISOString().split('T')[0];
|
||||
const inDateRange = taskDate >= startDate && taskDate <= endDate;
|
||||
const inList = !listIds || listIds.includes(task.list);
|
||||
return inDateRange && inList;
|
||||
});
|
||||
|
||||
// Calculate summary stats
|
||||
const totalTasks = filteredTasks.length;
|
||||
const totalMinutes = filteredTasks.reduce((sum, task) => sum + task.actualMinutes, 0);
|
||||
const totalPomodoros = filteredStats.reduce((sum, stat) => sum + stat.pomodorosCompleted, 0);
|
||||
|
||||
// Calculate number of days with data
|
||||
const daysWithData = filteredStats.length || 1; // Avoid division by zero
|
||||
|
||||
const tasksPerDay = totalTasks / daysWithData;
|
||||
const hoursPerDay = (totalMinutes / 60) / daysWithData;
|
||||
const minsPerTask = totalTasks > 0 ? totalMinutes / totalTasks : 0;
|
||||
|
||||
// Time breakdown by list
|
||||
const timeByListMap: Record<string, { minutes: number; taskCount: number }> = {};
|
||||
filteredTasks.forEach(task => {
|
||||
if (!timeByListMap[task.list]) {
|
||||
timeByListMap[task.list] = { minutes: 0, taskCount: 0 };
|
||||
}
|
||||
timeByListMap[task.list].minutes += task.actualMinutes;
|
||||
timeByListMap[task.list].taskCount++;
|
||||
});
|
||||
|
||||
const timeByList = this.settings.lists.map(list => {
|
||||
const data = timeByListMap[list.id] || { minutes: 0, taskCount: 0 };
|
||||
const percentage = totalMinutes > 0 ? (data.minutes / totalMinutes) * 100 : 0;
|
||||
return {
|
||||
listId: list.id,
|
||||
listName: list.name,
|
||||
listIcon: list.icon,
|
||||
listColor: list.color,
|
||||
minutes: data.minutes,
|
||||
taskCount: data.taskCount,
|
||||
percentage: Math.round(percentage * 10) / 10, // Round to 1 decimal
|
||||
};
|
||||
}).filter(item => item.minutes > 0); // Only show lists with data
|
||||
|
||||
// Daily breakdown
|
||||
const dailyBreakdown = filteredStats.map(stat => ({
|
||||
date: stat.date,
|
||||
tasks: stat.tasksCompleted,
|
||||
hours: Math.round((stat.totalMinutes / 60) * 10) / 10,
|
||||
pomodoros: stat.pomodorosCompleted,
|
||||
}));
|
||||
|
||||
// Productivity insights
|
||||
const mostProductiveHour = this.calculateMostProductiveHour(filteredTasks);
|
||||
const mostProductiveDay = this.calculateMostProductiveDay(filteredStats);
|
||||
const mostProductiveMonth = this.calculateMostProductiveMonth(filteredStats);
|
||||
|
||||
return {
|
||||
totalTasks,
|
||||
totalMinutes,
|
||||
totalPomodoros,
|
||||
tasksPerDay: Math.round(tasksPerDay * 10) / 10,
|
||||
hoursPerDay: Math.round(hoursPerDay * 10) / 10,
|
||||
minsPerTask: Math.round(minsPerTask),
|
||||
currentStreak: this.data.streak,
|
||||
timeByList,
|
||||
dailyBreakdown,
|
||||
mostProductiveHour,
|
||||
mostProductiveDay,
|
||||
mostProductiveMonth,
|
||||
};
|
||||
}
|
||||
|
||||
calculateMostProductiveHour(tasks: import('./types').CompletedTaskRecord[]): number | undefined {
|
||||
if (tasks.length === 0) return undefined;
|
||||
|
||||
const hourCounts: Record<number, number> = {};
|
||||
tasks.forEach(task => {
|
||||
const hour = new Date(task.completedAt).getHours();
|
||||
hourCounts[hour] = (hourCounts[hour] || 0) + 1;
|
||||
});
|
||||
|
||||
let maxHour = 0;
|
||||
let maxCount = 0;
|
||||
for (const [hour, count] of Object.entries(hourCounts)) {
|
||||
if (count > maxCount) {
|
||||
maxCount = count;
|
||||
maxHour = parseInt(hour);
|
||||
}
|
||||
}
|
||||
|
||||
return maxCount > 0 ? maxHour : undefined;
|
||||
}
|
||||
|
||||
calculateMostProductiveDay(stats: import('./types').DailyStats[]): string | undefined {
|
||||
if (stats.length === 0) return undefined;
|
||||
|
||||
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
const dayCounts: Record<string, number> = {};
|
||||
|
||||
stats.forEach(stat => {
|
||||
const dayOfWeek = new Date(stat.date).getDay();
|
||||
const dayName = dayNames[dayOfWeek];
|
||||
dayCounts[dayName] = (dayCounts[dayName] || 0) + stat.tasksCompleted;
|
||||
});
|
||||
|
||||
let maxDay = '';
|
||||
let maxCount = 0;
|
||||
for (const [day, count] of Object.entries(dayCounts)) {
|
||||
if (count > maxCount) {
|
||||
maxCount = count;
|
||||
maxDay = day;
|
||||
}
|
||||
}
|
||||
|
||||
return maxCount > 0 ? maxDay : undefined;
|
||||
}
|
||||
|
||||
calculateMostProductiveMonth(stats: import('./types').DailyStats[]): string | undefined {
|
||||
if (stats.length === 0) return undefined;
|
||||
|
||||
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December'];
|
||||
const monthCounts: Record<string, number> = {};
|
||||
|
||||
stats.forEach(stat => {
|
||||
const month = new Date(stat.date).getMonth();
|
||||
const monthName = monthNames[month];
|
||||
monthCounts[monthName] = (monthCounts[monthName] || 0) + stat.tasksCompleted;
|
||||
});
|
||||
|
||||
let maxMonth = '';
|
||||
let maxCount = 0;
|
||||
for (const [month, count] of Object.entries(monthCounts)) {
|
||||
if (count > maxCount) {
|
||||
maxCount = count;
|
||||
maxMonth = month;
|
||||
}
|
||||
}
|
||||
|
||||
return maxCount > 0 ? maxMonth : undefined;
|
||||
}
|
||||
|
||||
// ============ Timer Management ============
|
||||
|
||||
// Sync timer based on timestamp when app returns from background
|
||||
|
||||
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();
|
||||
|
||||
394
src/reportView.ts
Normal file
394
src/reportView.ts
Normal file
@@ -0,0 +1,394 @@
|
||||
import {
|
||||
ItemView,
|
||||
WorkspaceLeaf,
|
||||
Setting,
|
||||
} from 'obsidian';
|
||||
|
||||
import { ReportFilters, ReportData } from './types';
|
||||
import ImmersePlugin from './main';
|
||||
|
||||
export const VIEW_TYPE_REPORT = 'immerse-report-view';
|
||||
|
||||
export class ReportView extends ItemView {
|
||||
plugin: ImmersePlugin;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
selectedListIds: string[] = [];
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: ImmersePlugin) {
|
||||
super(leaf);
|
||||
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];
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
return VIEW_TYPE_REPORT;
|
||||
}
|
||||
|
||||
getDisplayText(): string {
|
||||
return '📊 Reports';
|
||||
}
|
||||
|
||||
getIcon(): string {
|
||||
return 'bar-chart-2';
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
const container = this.containerEl.children[1];
|
||||
container.empty();
|
||||
container.addClass('immerse-report-view');
|
||||
|
||||
this.renderContent();
|
||||
}
|
||||
|
||||
async onClose() {
|
||||
// Nothing to clean up
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const container = this.containerEl.children[1];
|
||||
container.empty();
|
||||
|
||||
// Header
|
||||
const header = container.createEl('div', { cls: 'immerse-report-header' });
|
||||
header.createEl('h2', { text: '📊 Reports', cls: 'immerse-report-title' });
|
||||
|
||||
// Filters section
|
||||
this.renderFilters(container);
|
||||
|
||||
// Generate button
|
||||
const generateBtn = container.createEl('button', {
|
||||
text: '🔄 Generate Report',
|
||||
cls: 'immerse-btn immerse-btn-primary immerse-report-generate-btn'
|
||||
});
|
||||
generateBtn.addEventListener('click', () => this.renderReport(container));
|
||||
|
||||
// Initial report render
|
||||
this.renderReport(container);
|
||||
}
|
||||
|
||||
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: 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
|
||||
if (reportData.timeByList.length > 0) {
|
||||
this.renderTimeByList(reportContent, reportData);
|
||||
}
|
||||
|
||||
// Productivity insights
|
||||
this.renderInsights(reportContent, reportData);
|
||||
|
||||
// Daily breakdown
|
||||
if (reportData.dailyBreakdown.length > 0) {
|
||||
this.renderDailyBreakdown(reportContent, reportData);
|
||||
}
|
||||
}
|
||||
|
||||
renderSummaryStats(container: Element, data: 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: 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.background = 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: 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('span', { text: '🕐', cls: 'immerse-insight-icon' });
|
||||
card.createEl('span', { text: 'MOST PRODUCTIVE HOUR', cls: 'immerse-insight-label' });
|
||||
card.createEl('span', {
|
||||
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('span', { text: '📅', cls: 'immerse-insight-icon' });
|
||||
card.createEl('span', { text: 'MOST PRODUCTIVE DAY', cls: 'immerse-insight-label' });
|
||||
card.createEl('span', { text: data.mostProductiveDay, cls: 'immerse-insight-value' });
|
||||
}
|
||||
|
||||
if (data.mostProductiveMonth) {
|
||||
const card = insightsGrid.createEl('div', { cls: 'immerse-insight-card' });
|
||||
card.createEl('span', { text: '🗓️', cls: 'immerse-insight-icon' });
|
||||
card.createEl('span', { text: 'MOST PRODUCTIVE MONTH', cls: 'immerse-insight-label' });
|
||||
card.createEl('span', { text: data.mostProductiveMonth, cls: 'immerse-insight-value' });
|
||||
}
|
||||
}
|
||||
|
||||
renderPieChart(container: Element, data: ReportData) {
|
||||
const pieContainer = container.createEl('div', { cls: 'immerse-daily-pie-container' });
|
||||
|
||||
// Calculate totals - normalize to minutes for fair comparison
|
||||
const totalTasks = data.totalTasks;
|
||||
const totalMinutes = data.totalMinutes;
|
||||
const totalPomodoros = data.totalPomodoros;
|
||||
|
||||
// For pie chart, let's use minutes as the base unit
|
||||
// Assume average task takes data.minsPerTask minutes
|
||||
const taskMinutes = totalTasks * data.minsPerTask;
|
||||
const pomodoroMinutes = totalPomodoros * 25; // Standard pomodoro is 25 minutes
|
||||
|
||||
const totalTime = taskMinutes + totalMinutes + pomodoroMinutes;
|
||||
|
||||
if (totalTime === 0) return;
|
||||
|
||||
// Calculate percentages based on time
|
||||
const tasksPercent = (taskMinutes / totalTime) * 100;
|
||||
const hoursPercent = (totalMinutes / totalTime) * 100;
|
||||
const pomodorosPercent = (pomodoroMinutes / totalTime) * 100;
|
||||
|
||||
// Calculate degrees for conic gradient
|
||||
const tasksDeg = (tasksPercent / 100) * 360;
|
||||
const hoursDeg = tasksDeg + (hoursPercent / 100) * 360;
|
||||
|
||||
// Create pie chart with segments
|
||||
const pieChart = pieContainer.createEl('div', { cls: 'immerse-daily-pie-chart' });
|
||||
|
||||
// Build conic-gradient string with explicit color stops
|
||||
const gradient = `conic-gradient(from 0deg, #6366f1 0deg ${tasksDeg}deg, #22c55e ${tasksDeg}deg ${hoursDeg}deg, #f59e0b ${hoursDeg}deg 360deg)`;
|
||||
pieChart.style.background = gradient;
|
||||
|
||||
// Center circle with total
|
||||
const center = pieChart.createEl('div', { cls: 'immerse-daily-pie-center' });
|
||||
center.createEl('div', { text: data.totalTasks.toString(), cls: 'immerse-daily-pie-center-value' });
|
||||
center.createEl('div', { text: 'TOTAL TASKS', cls: 'immerse-daily-pie-center-label' });
|
||||
|
||||
// Legend
|
||||
const legend = pieContainer.createEl('div', { cls: 'immerse-daily-pie-legend' });
|
||||
|
||||
// Tasks legend item
|
||||
const tasksItem = legend.createEl('div', { cls: 'immerse-daily-pie-legend-item' });
|
||||
const tasksColor = tasksItem.createEl('div', { cls: 'immerse-daily-pie-legend-color' });
|
||||
tasksColor.style.background = '#6366f1';
|
||||
const tasksInfo = tasksItem.createEl('div', { cls: 'immerse-daily-pie-legend-info' });
|
||||
const tasksLabel = tasksInfo.createEl('div', { cls: 'immerse-daily-pie-legend-label' });
|
||||
tasksLabel.createEl('span', { text: '✓' });
|
||||
tasksLabel.appendText('Tasks Completed');
|
||||
tasksInfo.createEl('div', { text: totalTasks.toString(), cls: 'immerse-daily-pie-legend-value' });
|
||||
tasksInfo.createEl('div', { text: `${tasksPercent.toFixed(1)}%`, cls: 'immerse-daily-pie-legend-percentage' });
|
||||
|
||||
// Hours legend item
|
||||
const hoursItem = legend.createEl('div', { cls: 'immerse-daily-pie-legend-item' });
|
||||
const hoursColor = hoursItem.createEl('div', { cls: 'immerse-daily-pie-legend-color' });
|
||||
hoursColor.style.background = '#22c55e';
|
||||
const hoursInfo = hoursItem.createEl('div', { cls: 'immerse-daily-pie-legend-info' });
|
||||
const hoursLabel = hoursInfo.createEl('div', { cls: 'immerse-daily-pie-legend-label' });
|
||||
hoursLabel.createEl('span', { text: '⏱️' });
|
||||
hoursLabel.appendText('Total Hours');
|
||||
hoursInfo.createEl('div', { text: (totalMinutes / 60).toFixed(1), cls: 'immerse-daily-pie-legend-value' });
|
||||
hoursInfo.createEl('div', { text: `${hoursPercent.toFixed(1)}%`, cls: 'immerse-daily-pie-legend-percentage' });
|
||||
|
||||
// Pomodoros legend item
|
||||
const pomodorosItem = legend.createEl('div', { cls: 'immerse-daily-pie-legend-item' });
|
||||
const pomodorosColor = pomodorosItem.createEl('div', { cls: 'immerse-daily-pie-legend-color' });
|
||||
pomodorosColor.style.background = '#f59e0b';
|
||||
const pomodorosInfo = pomodorosItem.createEl('div', { cls: 'immerse-daily-pie-legend-info' });
|
||||
const pomodorosLabel = pomodorosInfo.createEl('div', { cls: 'immerse-daily-pie-legend-label' });
|
||||
pomodorosLabel.createEl('span', { text: '🍅' });
|
||||
pomodorosLabel.appendText('Pomodoros');
|
||||
pomodorosInfo.createEl('div', { text: totalPomodoros.toString(), cls: 'immerse-daily-pie-legend-value' });
|
||||
pomodorosInfo.createEl('div', { text: `${pomodorosPercent.toFixed(1)}%`, cls: 'immerse-daily-pie-legend-percentage' });
|
||||
}
|
||||
|
||||
renderDailyBreakdown(container: Element, data: ReportData) {
|
||||
const section = container.createEl('div', { cls: 'immerse-report-section' });
|
||||
section.createEl('h3', { text: 'Daily Breakdown', cls: 'immerse-report-section-title' });
|
||||
|
||||
// Add pie chart for overall summary
|
||||
this.renderPieChart(section, data);
|
||||
|
||||
const breakdownContainer = section.createEl('div', { cls: 'immerse-daily-breakdown-container' });
|
||||
|
||||
// Show last 10 days max
|
||||
const recentData = data.dailyBreakdown.slice(-10);
|
||||
|
||||
// Find max values for scaling bars
|
||||
const maxTasks = Math.max(...recentData.map(d => d.tasks), 1);
|
||||
const maxHours = Math.max(...recentData.map(d => d.hours), 1);
|
||||
const maxPomodoros = Math.max(...recentData.map(d => d.pomodoros), 1);
|
||||
|
||||
recentData.forEach(day => {
|
||||
const row = breakdownContainer.createEl('div', { cls: 'immerse-daily-row' });
|
||||
|
||||
// Date column with formatted date
|
||||
const dateEl = row.createEl('div', { cls: 'immerse-daily-date' });
|
||||
const date = new Date(day.date + 'T00:00:00');
|
||||
const dayName = date.toLocaleDateString('en-US', { weekday: 'short' }).toUpperCase();
|
||||
const monthDay = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||||
dateEl.createEl('div', { text: dayName, cls: 'immerse-daily-date-day' });
|
||||
dateEl.createEl('div', { text: monthDay, cls: 'immerse-daily-date-num' });
|
||||
|
||||
// Bars column
|
||||
const barsContainer = row.createEl('div', { cls: 'immerse-daily-bars' });
|
||||
|
||||
// Tasks bar
|
||||
const tasksRow = barsContainer.createEl('div', { cls: 'immerse-daily-bar-row' });
|
||||
const tasksLabel = tasksRow.createEl('span', { cls: 'immerse-daily-bar-label' });
|
||||
tasksLabel.createEl('span', { text: '✓', cls: 'immerse-daily-bar-icon' });
|
||||
tasksLabel.appendText('Tasks');
|
||||
const tasksTrack = tasksRow.createEl('div', { cls: 'immerse-daily-bar-track' });
|
||||
const tasksFill = tasksTrack.createEl('div', { cls: 'immerse-daily-bar-fill tasks' });
|
||||
tasksFill.style.width = `${(day.tasks / maxTasks) * 100}%`;
|
||||
tasksRow.createEl('span', { text: day.tasks.toString(), cls: 'immerse-daily-bar-value' });
|
||||
|
||||
// Hours bar
|
||||
const hoursRow = barsContainer.createEl('div', { cls: 'immerse-daily-bar-row' });
|
||||
const hoursLabel = hoursRow.createEl('span', { cls: 'immerse-daily-bar-label' });
|
||||
hoursLabel.createEl('span', { text: '⏱️', cls: 'immerse-daily-bar-icon' });
|
||||
hoursLabel.appendText('Hours');
|
||||
const hoursTrack = hoursRow.createEl('div', { cls: 'immerse-daily-bar-track' });
|
||||
const hoursFill = hoursTrack.createEl('div', { cls: 'immerse-daily-bar-fill hours' });
|
||||
hoursFill.style.width = `${(day.hours / maxHours) * 100}%`;
|
||||
hoursRow.createEl('span', { text: day.hours.toFixed(1), cls: 'immerse-daily-bar-value' });
|
||||
|
||||
// Pomodoros bar
|
||||
const pomodorosRow = barsContainer.createEl('div', { cls: 'immerse-daily-bar-row' });
|
||||
const pomodorosLabel = pomodorosRow.createEl('span', { cls: 'immerse-daily-bar-label' });
|
||||
pomodorosLabel.createEl('span', { text: '🍅', cls: 'immerse-daily-bar-icon' });
|
||||
pomodorosLabel.appendText('Pomodoros');
|
||||
const pomodorosTrack = pomodorosRow.createEl('div', { cls: 'immerse-daily-bar-track' });
|
||||
const pomodorosFill = pomodorosTrack.createEl('div', { cls: 'immerse-daily-bar-fill pomodoros' });
|
||||
pomodorosFill.style.width = `${(day.pomodoros / maxPomodoros) * 100}%`;
|
||||
pomodorosRow.createEl('span', { text: day.pomodoros.toString(), cls: 'immerse-daily-bar-value' });
|
||||
});
|
||||
}
|
||||
}
|
||||
71
src/types.ts
71
src/types.ts
@@ -41,6 +41,29 @@ export interface ImmerseSettings {
|
||||
defaultReminderMinutes: number; // Default minutes before task to remind
|
||||
}
|
||||
|
||||
// Daily statistics snapshot
|
||||
export interface DailyStats {
|
||||
date: string; // YYYY-MM-DD format
|
||||
tasksCompleted: number;
|
||||
totalMinutes: number;
|
||||
pomodorosCompleted: number;
|
||||
tasksByList: Record<string, number>; // listId -> count
|
||||
minutesByList: Record<string, number>; // listId -> minutes
|
||||
}
|
||||
|
||||
// Archived completed task (for historical reporting)
|
||||
export interface CompletedTaskRecord {
|
||||
id: string;
|
||||
text: string;
|
||||
list: string;
|
||||
estimatedMinutes: number;
|
||||
actualMinutes: number;
|
||||
createdAt: number;
|
||||
completedAt: number;
|
||||
scheduledDate?: string;
|
||||
wasOverdue: boolean; // was it completed after scheduled time?
|
||||
}
|
||||
|
||||
export interface ImmerseData {
|
||||
tasks: ImmerseTask[];
|
||||
completedToday: number;
|
||||
@@ -48,6 +71,9 @@ export interface ImmerseData {
|
||||
streak: number;
|
||||
lastActiveDate: string;
|
||||
pomodorosCompleted: number;
|
||||
// Historical data for reporting
|
||||
dailyStats: DailyStats[]; // Array of daily statistics
|
||||
completedTasksArchive: CompletedTaskRecord[]; // All completed tasks history
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: ImmerseSettings = {
|
||||
@@ -79,10 +105,55 @@ export const DEFAULT_DATA: ImmerseData = {
|
||||
streak: 0,
|
||||
lastActiveDate: '',
|
||||
pomodorosCompleted: 0,
|
||||
dailyStats: [],
|
||||
completedTasksArchive: [],
|
||||
};
|
||||
|
||||
export const VIEW_TYPE_IMMERSE = 'immerse-view';
|
||||
|
||||
// ============ Reporting Types ============
|
||||
|
||||
export interface ReportFilters {
|
||||
startDate: string; // YYYY-MM-DD
|
||||
endDate: string; // YYYY-MM-DD
|
||||
listIds?: string[]; // Filter by specific lists (undefined = all)
|
||||
}
|
||||
|
||||
export interface ReportData {
|
||||
// Summary stats
|
||||
totalTasks: number;
|
||||
totalMinutes: number;
|
||||
totalPomodoros: number;
|
||||
tasksPerDay: number;
|
||||
hoursPerDay: number;
|
||||
minsPerTask: number;
|
||||
currentStreak: number;
|
||||
|
||||
// Time breakdown by list
|
||||
timeByList: Array<{
|
||||
listId: string;
|
||||
listName: string;
|
||||
listIcon: string;
|
||||
listColor: string;
|
||||
minutes: number;
|
||||
taskCount: number;
|
||||
percentage: number;
|
||||
}>;
|
||||
|
||||
// Daily breakdown for charts
|
||||
dailyBreakdown: Array<{
|
||||
date: string; // YYYY-MM-DD
|
||||
tasks: number;
|
||||
hours: number;
|
||||
pomodoros: number;
|
||||
}>;
|
||||
|
||||
// Productivity insights
|
||||
mostProductiveHour?: number; // 0-23
|
||||
mostProductiveDay?: string; // day name
|
||||
mostProductiveMonth?: string; // month name
|
||||
}
|
||||
|
||||
// ============ Celebration Messages ============
|
||||
|
||||
export const CELEBRATION_MESSAGES = [
|
||||
|
||||
10
src/view.ts
10
src/view.ts
@@ -106,7 +106,13 @@ export class ImmerseView extends ItemView {
|
||||
titleSection.createEl('div', { text: dateStr, cls: 'immerse-date' });
|
||||
|
||||
const actions = header.createEl('div', { cls: 'immerse-header-actions' });
|
||||
|
||||
|
||||
const reportsBtn = actions.createEl('button', { cls: 'immerse-btn' });
|
||||
reportsBtn.innerHTML = '📊 Reports';
|
||||
reportsBtn.addEventListener('click', () => {
|
||||
this.plugin.activateReportView();
|
||||
});
|
||||
|
||||
const addBtn = actions.createEl('button', { cls: 'immerse-btn immerse-btn-primary' });
|
||||
addBtn.innerHTML = '+ Add Task';
|
||||
addBtn.addEventListener('click', () => {
|
||||
@@ -121,7 +127,7 @@ export class ImmerseView extends ItemView {
|
||||
const statItems = [
|
||||
{ label: 'Pending', value: stats.pendingCount.toString(), icon: '📋' },
|
||||
{ label: 'Done Today', value: stats.completedToday.toString(), icon: '✅' },
|
||||
{ label: 'Focus Time', value: this.plugin.formatTimeHuman(stats.totalFocusMinutesToday), icon: '⏱️' },
|
||||
{ label: 'Today\'s Focus', value: this.plugin.formatTimeHuman(stats.totalFocusMinutesToday), icon: '⏱️' },
|
||||
{ label: 'Streak', value: `${stats.streak} days`, icon: '🔥' },
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user