550 lines
17 KiB
TypeScript
550 lines
17 KiB
TypeScript
import {
|
|
App,
|
|
Modal,
|
|
Notice,
|
|
Setting,
|
|
} from 'obsidian';
|
|
|
|
import { ImmerseTask } from './types';
|
|
import ImmersePlugin from './main';
|
|
|
|
// ============ Quick Add Task Modal ============
|
|
|
|
export class QuickAddTaskModal extends Modal {
|
|
plugin: ImmersePlugin;
|
|
taskText: string = '';
|
|
estimatedMinutes: number;
|
|
selectedList: string = 'work';
|
|
scheduledDate: string = '';
|
|
scheduledTime: string = '';
|
|
reminderMinutes: number = 0;
|
|
|
|
constructor(app: App, plugin: ImmersePlugin) {
|
|
super(app);
|
|
this.plugin = plugin;
|
|
this.estimatedMinutes = plugin.settings.defaultEstimateMinutes;
|
|
this.reminderMinutes = plugin.settings.defaultReminderMinutes;
|
|
if (plugin.settings.lists.length > 0) {
|
|
this.selectedList = plugin.settings.lists[0].id;
|
|
}
|
|
}
|
|
|
|
onOpen() {
|
|
const { contentEl } = this;
|
|
contentEl.addClass('immerse-modal');
|
|
|
|
contentEl.createEl('h2', { text: '⚡ Add New Task' });
|
|
|
|
// Task text input
|
|
new Setting(contentEl)
|
|
.setName('Task')
|
|
.addText(text => {
|
|
text.setPlaceholder('What do you need to do?')
|
|
.onChange(value => this.taskText = value);
|
|
text.inputEl.focus();
|
|
text.inputEl.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' && this.taskText.trim()) {
|
|
this.submitTask();
|
|
}
|
|
});
|
|
});
|
|
|
|
// Time estimate
|
|
new Setting(contentEl)
|
|
.setName('Estimated Time')
|
|
.setDesc('How long do you think this will take?')
|
|
.addDropdown(dropdown => {
|
|
const options: Record<string, string> = {
|
|
'5': '5 min',
|
|
'10': '10 min',
|
|
'15': '15 min',
|
|
'20': '20 min',
|
|
'25': '25 min (1 pomodoro)',
|
|
'30': '30 min',
|
|
'45': '45 min',
|
|
'50': '50 min (2 pomodoros)',
|
|
'60': '1 hour',
|
|
'90': '1.5 hours',
|
|
'120': '2 hours',
|
|
'180': '3 hours',
|
|
};
|
|
Object.entries(options).forEach(([value, label]) => {
|
|
dropdown.addOption(value, label);
|
|
});
|
|
dropdown.setValue(this.estimatedMinutes.toString());
|
|
dropdown.onChange(value => this.estimatedMinutes = parseInt(value));
|
|
});
|
|
|
|
// List selection
|
|
new Setting(contentEl)
|
|
.setName('List')
|
|
.addDropdown(dropdown => {
|
|
this.plugin.settings.lists.forEach(list => {
|
|
dropdown.addOption(list.id, `${list.icon} ${list.name}`);
|
|
});
|
|
dropdown.setValue(this.selectedList);
|
|
dropdown.onChange(value => this.selectedList = value);
|
|
});
|
|
|
|
// Scheduled date
|
|
new Setting(contentEl)
|
|
.setName('📅 Scheduled Date')
|
|
.setDesc('Optional: When do you plan to work on this?')
|
|
.addText(text => {
|
|
text.setPlaceholder('YYYY-MM-DD')
|
|
.setValue(this.scheduledDate)
|
|
.onChange(value => this.scheduledDate = value);
|
|
text.inputEl.type = 'date';
|
|
});
|
|
|
|
// Scheduled time
|
|
new Setting(contentEl)
|
|
.setName('⏰ Scheduled Time')
|
|
.setDesc('Optional: What time?')
|
|
.addText(text => {
|
|
text.setPlaceholder('HH:mm')
|
|
.setValue(this.scheduledTime)
|
|
.onChange(value => this.scheduledTime = value);
|
|
text.inputEl.type = 'time';
|
|
});
|
|
|
|
// Reminder
|
|
if (this.plugin.settings.enableReminders) {
|
|
new Setting(contentEl)
|
|
.setName('🔔 Reminder')
|
|
.setDesc('Remind me before the scheduled time')
|
|
.addDropdown(dropdown => {
|
|
dropdown.addOption('0', 'No reminder');
|
|
dropdown.addOption('5', '5 minutes before');
|
|
dropdown.addOption('10', '10 minutes before');
|
|
dropdown.addOption('15', '15 minutes before');
|
|
dropdown.addOption('30', '30 minutes before');
|
|
dropdown.addOption('60', '1 hour before');
|
|
dropdown.setValue(this.reminderMinutes.toString());
|
|
dropdown.onChange(value => this.reminderMinutes = parseInt(value));
|
|
});
|
|
}
|
|
|
|
// Buttons
|
|
const buttonContainer = contentEl.createEl('div', { cls: 'immerse-modal-buttons' });
|
|
|
|
const cancelBtn = buttonContainer.createEl('button', { text: 'Cancel', cls: 'immerse-btn' });
|
|
cancelBtn.addEventListener('click', () => this.close());
|
|
|
|
const addBtn = buttonContainer.createEl('button', { text: 'Add Task', cls: 'immerse-btn immerse-btn-primary' });
|
|
addBtn.addEventListener('click', () => this.submitTask());
|
|
}
|
|
|
|
submitTask() {
|
|
if (this.taskText.trim()) {
|
|
const task = this.plugin.createTask(this.taskText, this.estimatedMinutes, this.selectedList);
|
|
// Add scheduling data if provided
|
|
if (this.scheduledDate) {
|
|
task.scheduledDate = this.scheduledDate;
|
|
}
|
|
if (this.scheduledTime) {
|
|
task.scheduledTime = this.scheduledTime;
|
|
}
|
|
if (this.reminderMinutes > 0 && this.scheduledDate && this.scheduledTime) {
|
|
task.reminderMinutes = this.reminderMinutes;
|
|
}
|
|
this.plugin.addTask(task);
|
|
new Notice('✅ Task added!');
|
|
this.close();
|
|
} else {
|
|
new Notice('Please enter a task description');
|
|
}
|
|
}
|
|
|
|
onClose() {
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
}
|
|
}
|
|
|
|
// ============ Edit Task Modal ============
|
|
|
|
export class EditTaskModal extends Modal {
|
|
plugin: ImmersePlugin;
|
|
task: ImmerseTask;
|
|
|
|
constructor(app: App, plugin: ImmersePlugin, task: ImmerseTask) {
|
|
super(app);
|
|
this.plugin = plugin;
|
|
this.task = { ...task };
|
|
}
|
|
|
|
onOpen() {
|
|
const { contentEl } = this;
|
|
contentEl.addClass('immerse-modal');
|
|
|
|
contentEl.createEl('h2', { text: '✏️ Edit Task' });
|
|
|
|
new Setting(contentEl)
|
|
.setName('Task')
|
|
.addText(text => text
|
|
.setValue(this.task.text)
|
|
.onChange(value => this.task.text = value));
|
|
|
|
new Setting(contentEl)
|
|
.setName('Estimated Time')
|
|
.addDropdown(dropdown => {
|
|
const options: Record<string, string> = {
|
|
'5': '5 min',
|
|
'10': '10 min',
|
|
'15': '15 min',
|
|
'20': '20 min',
|
|
'25': '25 min',
|
|
'30': '30 min',
|
|
'45': '45 min',
|
|
'50': '50 min',
|
|
'60': '1 hour',
|
|
'90': '1.5 hours',
|
|
'120': '2 hours',
|
|
'180': '3 hours',
|
|
};
|
|
Object.entries(options).forEach(([value, label]) => {
|
|
dropdown.addOption(value, label);
|
|
});
|
|
dropdown.setValue(this.task.estimatedMinutes.toString());
|
|
dropdown.onChange(value => this.task.estimatedMinutes = parseInt(value));
|
|
});
|
|
|
|
new Setting(contentEl)
|
|
.setName('List')
|
|
.addDropdown(dropdown => {
|
|
this.plugin.settings.lists.forEach(list => {
|
|
dropdown.addOption(list.id, `${list.icon} ${list.name}`);
|
|
});
|
|
dropdown.setValue(this.task.list);
|
|
dropdown.onChange(value => this.task.list = value);
|
|
});
|
|
|
|
new Setting(contentEl)
|
|
.setName('Notes')
|
|
.setDesc('Add any additional details or links')
|
|
.addTextArea(textarea => {
|
|
textarea
|
|
.setValue(this.task.notes)
|
|
.onChange(value => this.task.notes = value);
|
|
textarea.inputEl.rows = 4;
|
|
});
|
|
|
|
// Scheduled date
|
|
new Setting(contentEl)
|
|
.setName('📅 Scheduled Date')
|
|
.setDesc('Optional: When do you plan to work on this?')
|
|
.addText(text => {
|
|
text.setPlaceholder('YYYY-MM-DD')
|
|
.setValue(this.task.scheduledDate || '')
|
|
.onChange(value => this.task.scheduledDate = value || undefined);
|
|
text.inputEl.type = 'date';
|
|
});
|
|
|
|
// Scheduled time
|
|
new Setting(contentEl)
|
|
.setName('⏰ Scheduled Time')
|
|
.setDesc('Optional: What time?')
|
|
.addText(text => {
|
|
text.setPlaceholder('HH:mm')
|
|
.setValue(this.task.scheduledTime || '')
|
|
.onChange(value => this.task.scheduledTime = value || undefined);
|
|
text.inputEl.type = 'time';
|
|
});
|
|
|
|
// Reminder
|
|
if (this.plugin.settings.enableReminders) {
|
|
new Setting(contentEl)
|
|
.setName('🔔 Reminder')
|
|
.setDesc('Remind me before the scheduled time')
|
|
.addDropdown(dropdown => {
|
|
dropdown.addOption('0', 'No reminder');
|
|
dropdown.addOption('5', '5 minutes before');
|
|
dropdown.addOption('10', '10 minutes before');
|
|
dropdown.addOption('15', '15 minutes before');
|
|
dropdown.addOption('30', '30 minutes before');
|
|
dropdown.addOption('60', '1 hour before');
|
|
dropdown.setValue((this.task.reminderMinutes || 0).toString());
|
|
dropdown.onChange(value => {
|
|
const minutes = parseInt(value);
|
|
this.task.reminderMinutes = minutes > 0 ? minutes : undefined;
|
|
});
|
|
});
|
|
}
|
|
|
|
// Show actual time if task has been worked on
|
|
if (this.task.actualMinutes > 0) {
|
|
new Setting(contentEl)
|
|
.setName('Time Tracked')
|
|
.setDesc(`You've worked on this task for ${this.plugin.formatTimeHuman(this.task.actualMinutes)}`);
|
|
}
|
|
|
|
const buttonContainer = contentEl.createEl('div', { cls: 'immerse-modal-buttons' });
|
|
|
|
const cancelBtn = buttonContainer.createEl('button', { text: 'Cancel', cls: 'immerse-btn' });
|
|
cancelBtn.addEventListener('click', () => this.close());
|
|
|
|
const saveBtn = buttonContainer.createEl('button', { text: 'Save', cls: 'immerse-btn immerse-btn-primary' });
|
|
saveBtn.addEventListener('click', () => {
|
|
this.plugin.updateTask(this.task.id, this.task);
|
|
new Notice('✅ Task updated!');
|
|
this.close();
|
|
});
|
|
}
|
|
|
|
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();
|
|
}
|
|
} |