diff --git a/.gitignore b/.gitignore index 6953e01..95c6beb 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ data.json RELEASE-GUIDE.md ROADMAP.md deploy-test.bat -.claude/ \ No newline at end of file +.claude/ +RELEASE_NOTES_v1.1.0md diff --git a/README.md b/README.md index 8eea3af..f18ddc3 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A powerful task management and focus timer plugin for [Obsidian](https://obsidia ![Immerse Banner](https://img.shields.io/badge/Obsidian-Plugin-7c3aed?style=for-the-badge&logo=obsidian&logoColor=white) ![License](https://img.shields.io/badge/License-MIT-green?style=for-the-badge) -![Version](https://img.shields.io/badge/Version-1.0.9-blue?style=for-the-badge) +![Version](https://img.shields.io/badge/Version-1.1.0-blue?style=for-the-badge) ## 🎯 Overview @@ -48,6 +48,29 @@ Immerse brings the power of time-boxed task management directly into your Obsidi - **Pomodoro Count**: Track total pomodoros completed - **Daily Note Logging**: Automatically log completed tasks to your daily notes with timestamps and performance metrics +### 📈 Reporting & Analytics (New in v1.1.0!) +- **Comprehensive Reports**: View detailed productivity reports with date range filtering +- **Key Metrics Dashboard**: Track tasks done, tasks per day, hours per day, minutes per task, and day streaks +- **Visual Analytics**: Pie charts showing task distribution and time allocation +- **Time by List**: See how much time you spend on different task categories +- **Productivity Insights**: Discover your most productive hour, day, and month +- **Daily Breakdown**: Visual bar charts showing last 10 days of activity +- **Quick Filters**: Today, Last 7/30/90 days for easy report generation + +### 🗓️ Task Scheduling & Reminders (New in v1.1.0!) +- **Schedule Tasks**: Set specific date and time for tasks +- **Smart Reminders**: Get notifications before task is due (5/10/15/30/60 minute options) +- **Overdue Detection**: Visual indicators (⚠️ red badge) for past-due tasks +- **Startup Checks**: Alerts when opening Obsidian if tasks are overdue +- **Background Monitoring**: Automatic 30-second checks for due tasks +- **Sound Alerts**: Optional audio notifications for reminders + +### 📱 Mobile Optimized (New in v1.1.0!) +- **Responsive Design**: Fully optimized for mobile screens (tablets and phones) +- **Touch-Friendly**: Larger buttons and tap targets (44px minimum) +- **Adaptive Layouts**: Charts and visualizations scale appropriately +- **Mobile Testing**: Works great on Obsidian mobile app + ### 🎨 User Experience - **Status Bar Timer**: Timer that stays visible while you work - **Celebration Messages**: Fun, randomized messages when you complete tasks diff --git a/RELEASE_NOTES_v1.1.0.md b/RELEASE_NOTES_v1.1.0.md new file mode 100644 index 0000000..6b6ca53 --- /dev/null +++ b/RELEASE_NOTES_v1.1.0.md @@ -0,0 +1,166 @@ +# Immerse v1.1.0 Release Notes + +Release Date: November 24, 2024 + +## 🎉 What's New + +### 📈 Reporting & Analytics System +A complete analytics dashboard to track your productivity over time! + +**Features:** +- **Key Metrics Dashboard**: View tasks completed, tasks per day, hours per day, minutes per task, and current day streak +- **Visual Charts**: Beautiful pie chart showing distribution of tasks, hours, and pomodoros +- **Time by List Analysis**: See exactly how much time you spend on different task categories +- **Productivity Insights**: Discover your most productive hour, day, and month +- **Daily Breakdown**: Visual bar charts showing the last 10 days of activity with tasks, hours, and pomodoros +- **Flexible Filtering**: Quick filters (Today, Last 7/30/90 days) plus custom date range selection +- **Report View**: Dedicated view accessible via "📊 Reports" button + +### 🗓️ Task Scheduling & Reminders +Never miss a deadline with the new scheduling system! + +**Features:** +- **Schedule Tasks**: Set specific date and time when tasks are due +- **Smart Reminders**: Choose notification timing (5, 10, 15, 30, or 60 minutes before due time) +- **Visual Indicators**: + - Blue 📅 badge shows scheduled date/time + - Red ⚠️ pulsing "OVERDUE" badge for past-due tasks + - Red left border highlight on overdue tasks +- **Background Monitoring**: Automatic checks every 30 seconds for upcoming/overdue tasks +- **Startup Alerts**: Get notified when opening Obsidian if tasks are overdue +- **Sound Notifications**: Optional audio alerts for reminders (respects sound settings) +- **Duplicate Prevention**: Smart tracking ensures you don't get reminded multiple times + +### 📱 Mobile Optimization +Full responsive design for excellent mobile experience! + +**Improvements:** +- **Responsive Layouts**: All UI elements adapt to mobile screen sizes +- **Touch-Friendly**: + - Minimum 44px tap targets (Apple's recommended size) + - Larger checkboxes (28px on mobile) + - Larger action buttons +- **Optimized Views**: + - Pie charts scale down appropriately (220px on tablets, 180px on phones) + - Stats grid adapts (2 columns on tablets, 1 column on small phones) + - Daily breakdown bars stack vertically on mobile + - Modal buttons become full-width and stack +- **Always Visible Actions**: Task action buttons always visible on mobile (no hover needed) +- **Adaptive Typography**: Font sizes scale appropriately for readability + +## 🔧 Technical Improvements + +### Performance +- Efficient report generation with on-demand calculation +- Optimized reminder checks with 30-second intervals +- Smart caching to prevent duplicate notifications + +### Code Structure +- New `reportView.ts` file for analytics view +- Enhanced type definitions for scheduling and reporting +- Improved data tracking for historical statistics +- Better separation of concerns + +### Compatibility +- Fully compatible with Obsidian desktop and mobile +- Works with both light and dark themes +- Respects user's sound and notification preferences + +## 📊 Statistics Tracked + +The plugin now tracks comprehensive statistics including: +- Total tasks completed +- Total time spent (all-time and daily) +- Total pomodoros completed +- Tasks per day average +- Hours per day average +- Minutes per task average +- Current day streak +- Most productive hour/day/month +- Time breakdown by task list + +## 🎨 UI/UX Enhancements + +- New "📊 Reports" button in main view header +- Modern, clean report interface with gradient accents +- Color-coded progress bars using list colors +- Hover effects and animations for better interactivity +- Responsive filter controls +- Improved date picker styling and alignment + +## 🐛 Bug Fixes + +- Fixed pie chart rendering issues (now uses proper color values) +- Improved gradient calculation for proper segment display +- Better normalization of metrics (using time-based calculations) +- Fixed date input alignment in report filters + +## ⚙️ Settings Updates + +### New Settings: +- **Enable Reminders**: Toggle reminder notifications on/off +- **Default Reminder Minutes**: Set default reminder time for new scheduled tasks (default: 30 minutes) + +### Updated Settings: +All existing settings remain compatible with no migration required. + +## 📱 Mobile Testing Recommendations + +To ensure the best experience on mobile: + +1. **Test on actual device**: Install Obsidian mobile and test the plugin +2. **Browser DevTools**: Use Chrome/Edge DevTools device emulation +3. **Responsive breakpoints**: + - Desktop: >768px + - Tablet: ≤768px + - Phone: ≤480px + +## 🔄 Upgrade Instructions + +### From v1.0.x: + +1. **Backup your data** (optional but recommended): + - Your tasks and settings are in `.obsidian/plugins/immerse/data.json` + - Make a copy before updating + +2. **Update files**: + - Copy `main.js`, `manifest.json`, and `styles.css` to your vault + - **DO NOT replace `data.json`** - this contains your tasks! + +3. **Reload Obsidian**: + - Press Ctrl/Cmd+R to reload + - Or restart Obsidian + +4. **Verify**: + - Check that your tasks are still there + - Click "📊 Reports" to see your new analytics + - Try scheduling a task with a reminder + +## 🚀 What's Next (v1.2.0) + +Future enhancements we're considering: +- Calendar view integration +- Export reports (PDF/CSV) +- More chart types and visualizations +- Task templates +- Recurring tasks +- Advanced filtering options + +## 💡 Feedback + +Found a bug or have a feature request? +- Open an issue: [https://git.cribdev.com/crib/immerse/issues](https://git.cribdev.com/crib/immerse/issues) +- Join the discussion in the repository + +## 🙏 Credits + +Special thanks to: +- [Blitzit](https://www.blitzit.app/) for continued inspiration +- The Obsidian community for feedback and support +- Claude.ai for development assistance + +--- + +**Enjoy v1.1.0!** ⚡ + +Made with ❤️ for the Obsidian community diff --git a/main.js b/main.js index aafc247..97d8dcb 100644 --- a/main.js +++ b/main.js @@ -27,7 +27,7 @@ __export(main_exports, { default: () => ImmersePlugin }); module.exports = __toCommonJS(main_exports); -var import_obsidian3 = require("obsidian"); +var import_obsidian4 = require("obsidian"); // src/types.ts var DEFAULT_SETTINGS = { @@ -57,7 +57,9 @@ var DEFAULT_DATA = { totalFocusMinutesToday: 0, streak: 0, lastActiveDate: "", - pomodorosCompleted: 0 + pomodorosCompleted: 0, + dailyStats: [], + completedTasksArchive: [] }; var VIEW_TYPE_IMMERSE = "immerse-view"; var CELEBRATION_MESSAGES = [ @@ -347,6 +349,11 @@ var ImmerseView = class extends import_obsidian2.ItemView { const dateStr = today.toLocaleDateString("en-US", { weekday: "long", month: "short", day: "numeric" }); 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 = "\u{1F4CA} 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", () => { @@ -359,7 +366,7 @@ var ImmerseView = class extends import_obsidian2.ItemView { const statItems = [ { label: "Pending", value: stats.pendingCount.toString(), icon: "\u{1F4CB}" }, { label: "Done Today", value: stats.completedToday.toString(), icon: "\u2705" }, - { label: "Focus Time", value: this.plugin.formatTimeHuman(stats.totalFocusMinutesToday), icon: "\u23F1\uFE0F" }, + { label: "Today's Focus", value: this.plugin.formatTimeHuman(stats.totalFocusMinutesToday), icon: "\u23F1\uFE0F" }, { label: "Streak", value: `${stats.streak} days`, icon: "\u{1F525}" } ]; statItems.forEach((stat) => { @@ -600,8 +607,281 @@ var ImmerseView = class extends import_obsidian2.ItemView { } }; +// src/reportView.ts +var import_obsidian3 = require("obsidian"); +var VIEW_TYPE_REPORT = "immerse-report-view"; +var ReportView = class extends import_obsidian3.ItemView { + constructor(leaf, plugin) { + super(leaf); + this.selectedListIds = []; + this.plugin = plugin; + 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() { + return VIEW_TYPE_REPORT; + } + getDisplayText() { + return "\u{1F4CA} Reports"; + } + getIcon() { + return "bar-chart-2"; + } + async onOpen() { + const container = this.containerEl.children[1]; + container.empty(); + container.addClass("immerse-report-view"); + this.renderContent(); + } + async onClose() { + } + renderContent() { + const container = this.containerEl.children[1]; + container.empty(); + const header = container.createEl("div", { cls: "immerse-report-header" }); + header.createEl("h2", { text: "\u{1F4CA} Reports", cls: "immerse-report-title" }); + this.renderFilters(container); + const generateBtn = container.createEl("button", { + text: "\u{1F504} Generate Report", + cls: "immerse-btn immerse-btn-primary immerse-report-generate-btn" + }); + generateBtn.addEventListener("click", () => this.renderReport(container)); + this.renderReport(container); + } + renderFilters(container) { + const filtersSection = container.createEl("div", { cls: "immerse-report-filters" }); + const dateRow = filtersSection.createEl("div", { cls: "immerse-report-filter-row" }); + new import_obsidian3.Setting(dateRow).setName("Start Date").addText((text) => { + text.setValue(this.startDate).onChange((value) => this.startDate = value); + text.inputEl.type = "date"; + }); + new import_obsidian3.Setting(dateRow).setName("End Date").addText((text) => { + text.setValue(this.endDate).onChange((value) => this.endDate = value); + text.inputEl.type = "date"; + }); + 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) { + const oldReport = container.querySelector(".immerse-report-content"); + if (oldReport) + oldReport.remove(); + const filters = { + startDate: this.startDate, + endDate: this.endDate, + listIds: this.selectedListIds.length > 0 ? this.selectedListIds : void 0 + }; + const reportData = this.plugin.generateReport(filters); + const reportContent = container.createEl("div", { cls: "immerse-report-content" }); + 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; + } + this.renderSummaryStats(reportContent, reportData); + if (reportData.timeByList.length > 0) { + this.renderTimeByList(reportContent, reportData); + } + this.renderInsights(reportContent, reportData); + if (reportData.dailyBreakdown.length > 0) { + this.renderDailyBreakdown(reportContent, reportData); + } + } + renderSummaryStats(container, data) { + const statsGrid = container.createEl("div", { cls: "immerse-stats-grid" }); + const stats = [ + { label: "TASKS DONE", value: data.totalTasks.toString(), icon: "\u2713" }, + { label: "TASKS PER DAY", value: data.tasksPerDay.toFixed(1), icon: "\u{1F4C5}" }, + { label: "HOURS PER DAY", value: data.hoursPerDay.toFixed(1), icon: "\u23F0" }, + { label: "MINS PER TASK", value: data.minsPerTask.toString(), icon: "\u23F1\uFE0F" }, + { label: "DAY STREAK", value: data.currentStreak.toString(), icon: "\u{1F525}" }, + { label: "TOTAL HOURS", value: (data.totalMinutes / 60).toFixed(1), icon: "\u231A" } + ]; + 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, data) { + 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" }); + 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" }); + 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; + 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, data) { + 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 !== void 0) { + const card = insightsGrid.createEl("div", { cls: "immerse-insight-card" }); + card.createEl("span", { text: "\u{1F550}", 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: "\u{1F4C5}", 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: "\u{1F5D3}\uFE0F", 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, data) { + const pieContainer = container.createEl("div", { cls: "immerse-daily-pie-container" }); + const totalTasks = data.totalTasks; + const totalMinutes = data.totalMinutes; + const totalPomodoros = data.totalPomodoros; + const taskMinutes = totalTasks * data.minsPerTask; + const pomodoroMinutes = totalPomodoros * 25; + const totalTime = taskMinutes + totalMinutes + pomodoroMinutes; + if (totalTime === 0) + return; + const tasksPercent = taskMinutes / totalTime * 100; + const hoursPercent = totalMinutes / totalTime * 100; + const pomodorosPercent = pomodoroMinutes / totalTime * 100; + const tasksDeg = tasksPercent / 100 * 360; + const hoursDeg = tasksDeg + hoursPercent / 100 * 360; + const pieChart = pieContainer.createEl("div", { cls: "immerse-daily-pie-chart" }); + const gradient = `conic-gradient(from 0deg, #6366f1 0deg ${tasksDeg}deg, #22c55e ${tasksDeg}deg ${hoursDeg}deg, #f59e0b ${hoursDeg}deg 360deg)`; + pieChart.style.background = gradient; + 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" }); + const legend = pieContainer.createEl("div", { cls: "immerse-daily-pie-legend" }); + 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: "\u2713" }); + 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" }); + 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: "\u23F1\uFE0F" }); + 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" }); + 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: "\u{1F345}" }); + 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, data) { + const section = container.createEl("div", { cls: "immerse-report-section" }); + section.createEl("h3", { text: "Daily Breakdown", cls: "immerse-report-section-title" }); + this.renderPieChart(section, data); + const breakdownContainer = section.createEl("div", { cls: "immerse-daily-breakdown-container" }); + const recentData = data.dailyBreakdown.slice(-10); + 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" }); + 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" }); + const barsContainer = row.createEl("div", { cls: "immerse-daily-bars" }); + const tasksRow = barsContainer.createEl("div", { cls: "immerse-daily-bar-row" }); + const tasksLabel = tasksRow.createEl("span", { cls: "immerse-daily-bar-label" }); + tasksLabel.createEl("span", { text: "\u2713", 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" }); + const hoursRow = barsContainer.createEl("div", { cls: "immerse-daily-bar-row" }); + const hoursLabel = hoursRow.createEl("span", { cls: "immerse-daily-bar-label" }); + hoursLabel.createEl("span", { text: "\u23F1\uFE0F", 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" }); + const pomodorosRow = barsContainer.createEl("div", { cls: "immerse-daily-bar-row" }); + const pomodorosLabel = pomodorosRow.createEl("span", { cls: "immerse-daily-bar-label" }); + pomodorosLabel.createEl("span", { text: "\u{1F345}", 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" }); + }); + } +}; + // src/main.ts -var ImmersePlugin = class extends import_obsidian3.Plugin { +var ImmersePlugin = class extends import_obsidian4.Plugin { constructor() { super(...arguments); // Timer state @@ -636,6 +916,10 @@ var ImmersePlugin = class extends import_obsidian3.Plugin { VIEW_TYPE_IMMERSE, (leaf) => new ImmerseView(leaf, this) ); + this.registerView( + VIEW_TYPE_REPORT, + (leaf) => new ReportView(leaf, this) + ); this.addRibbonIcon("zap", "Open Immerse", () => { this.activateView(); }); @@ -664,6 +948,11 @@ var ImmersePlugin = class extends import_obsidian3.Plugin { name: "Complete Current Task", callback: () => this.completeActiveTask() }); + this.addCommand({ + id: "view-reports", + name: "View Reports", + callback: () => this.activateReportView() + }); this.addSettingTab(new ImmerseSettingTab(this.app, this)); this.createStatusBar(); if (this.settings.enableReminders) { @@ -723,6 +1012,20 @@ var ImmersePlugin = class extends import_obsidian3.Plugin { workspace.revealLeaf(leaf); } } + async activateReportView() { + const { workspace } = this.app; + const existingLeaves = workspace.getLeavesOfType(VIEW_TYPE_REPORT); + if (existingLeaves.length > 0) { + workspace.revealLeaf(existingLeaves[0]); + } else { + const leaf = workspace.getLeaf("tab"); + await leaf.setViewState({ + type: VIEW_TYPE_REPORT, + active: true + }); + workspace.revealLeaf(leaf); + } + } // ============ Task Management ============ createTask(text, estimatedMinutes = this.settings.defaultEstimateMinutes, list = "work") { return { @@ -770,6 +1073,8 @@ var ImmersePlugin = class extends import_obsidian3.Plugin { task.isActive = false; this.data.completedToday++; this.data.lastActiveDate = new Date().toDateString(); + this.archiveCompletedTask(task); + this.updateDailyStats(task); if (this.settings.enableCelebrations) { this.showCelebration(task); } @@ -791,9 +1096,185 @@ var ImmersePlugin = class extends import_obsidian3.Plugin { if (this.activeTaskId) { this.completeTask(this.activeTaskId); } else { - new import_obsidian3.Notice("No active task to complete"); + new import_obsidian4.Notice("No active task to complete"); } } + // ============ Data Archiving & Statistics ============ + archiveCompletedTask(task) { + const wasOverdue = task.scheduledDate && task.scheduledTime && new Date(`${task.scheduledDate}T${task.scheduledTime}`).getTime() < (task.completedAt || Date.now()); + const record = { + 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) { + const today = new Date().toISOString().split("T")[0]; + 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); + } + todayStats.tasksCompleted++; + todayStats.totalMinutes += task.actualMinutes; + todayStats.pomodorosCompleted = this.data.pomodorosCompleted; + todayStats.tasksByList[task.list] = (todayStats.tasksByList[task.list] || 0) + 1; + todayStats.minutesByList[task.list] = (todayStats.minutesByList[task.list] || 0) + task.actualMinutes; + 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) { + const { startDate, endDate, listIds } = filters; + const filteredStats = this.data.dailyStats.filter((stat) => { + return stat.date >= startDate && stat.date <= endDate; + }); + 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; + }); + const totalTasks = filteredTasks.length; + const totalMinutes = filteredTasks.reduce((sum, task) => sum + task.actualMinutes, 0); + const totalPomodoros = filteredStats.reduce((sum, stat) => sum + stat.pomodorosCompleted, 0); + const daysWithData = filteredStats.length || 1; + const tasksPerDay = totalTasks / daysWithData; + const hoursPerDay = totalMinutes / 60 / daysWithData; + const minsPerTask = totalTasks > 0 ? totalMinutes / totalTasks : 0; + const timeByListMap = {}; + 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); + const dailyBreakdown = filteredStats.map((stat) => ({ + date: stat.date, + tasks: stat.tasksCompleted, + hours: Math.round(stat.totalMinutes / 60 * 10) / 10, + pomodoros: stat.pomodorosCompleted + })); + 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) { + if (tasks.length === 0) + return void 0; + const hourCounts = {}; + 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 : void 0; + } + calculateMostProductiveDay(stats) { + if (stats.length === 0) + return void 0; + const dayNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; + const dayCounts = {}; + 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 : void 0; + } + calculateMostProductiveMonth(stats) { + if (stats.length === 0) + return void 0; + const monthNames = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" + ]; + const monthCounts = {}; + 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 : void 0; + } // ============ Timer Management ============ // Sync timer based on timestamp when app returns from background syncTimerFromTimestamp() { @@ -835,7 +1316,7 @@ var ImmersePlugin = class extends import_obsidian3.Plugin { if (this.settings.enableSounds) { this.playAlertSound(); } - new import_obsidian3.Notice(`\u23F0 Time's up for: ${task.text}`); + new import_obsidian4.Notice(`\u23F0 Time's up for: ${task.text}`); } }, 1e3); this.saveAllData(); @@ -892,7 +1373,7 @@ var ImmersePlugin = class extends import_obsidian3.Plugin { if (this.settings.enableSounds) { this.playAlertSound(); } - new import_obsidian3.Notice("\u{1F345} Pomodoro complete! Time for a break."); + new import_obsidian4.Notice("\u{1F345} Pomodoro complete! Time for a break."); if (this.settings.autoStartBreak) { this.startBreak(); } else { @@ -902,7 +1383,7 @@ var ImmersePlugin = class extends import_obsidian3.Plugin { if (this.settings.enableSounds) { this.playAlertSound(); } - new import_obsidian3.Notice("\u26A1 Break over! Ready to focus?"); + new import_obsidian4.Notice("\u26A1 Break over! Ready to focus?"); this.refreshView(); } this.saveAllData(); @@ -914,7 +1395,7 @@ var ImmersePlugin = class extends import_obsidian3.Plugin { this.currentTimerSeconds = (isLongBreak ? this.settings.longBreakMinutes : this.settings.pomodoroBreakMinutes) * 60; this.timerStartTimestamp = Date.now(); this.pausedTimeRemaining = this.currentTimerSeconds; - new import_obsidian3.Notice(isLongBreak ? "\u2615 Long break time!" : "\u2615 Short break time!"); + new import_obsidian4.Notice(isLongBreak ? "\u2615 Long break time!" : "\u2615 Short break time!"); this.refreshView(); if (!this.timerInterval) { this.timerInterval = window.setInterval(() => { @@ -960,7 +1441,7 @@ var ImmersePlugin = class extends import_obsidian3.Plugin { } }, 1e3); } else { - new import_obsidian3.Notice("No active task. Select a task first."); + new import_obsidian4.Notice("No active task. Select a task first."); } this.updateStatusBar(); this.refreshView(); @@ -991,7 +1472,7 @@ var ImmersePlugin = class extends import_obsidian3.Plugin { if (pendingTasks.length > 0) { this.startPomodoro(pendingTasks[0].id); } else { - new import_obsidian3.Notice("No pending tasks. Add a task first!"); + new import_obsidian4.Notice("No pending tasks. Add a task first!"); } } // ============ Status Bar Timer ============ @@ -1058,7 +1539,7 @@ var ImmersePlugin = class extends import_obsidian3.Plugin { } showReminder(task) { const timeStr = task.scheduledTime; - new import_obsidian3.Notice(`\u{1F514} Reminder: "${task.text}" is scheduled for ${timeStr}`, 8e3); + new import_obsidian4.Notice(`\u{1F514} Reminder: "${task.text}" is scheduled for ${timeStr}`, 8e3); if (this.settings.enableSounds) { this.playAlertSound(); } @@ -1066,7 +1547,7 @@ var ImmersePlugin = class extends import_obsidian3.Plugin { showOverdueNotice(task) { const dateStr = task.scheduledDate; const timeStr = task.scheduledTime; - new import_obsidian3.Notice(`\u26A0\uFE0F Overdue: "${task.text}" was scheduled for ${dateStr} ${timeStr}`, 1e4); + new import_obsidian4.Notice(`\u26A0\uFE0F Overdue: "${task.text}" was scheduled for ${dateStr} ${timeStr}`, 1e4); if (this.settings.enableSounds) { this.playAlertSound(); } @@ -1083,7 +1564,7 @@ var ImmersePlugin = class extends import_obsidian3.Plugin { messages = OVERTIME_MESSAGES; } const celebration = messages[Math.floor(Math.random() * messages.length)]; - new import_obsidian3.Notice(`${celebration.emoji} ${celebration.message}${extraMessage}`); + new import_obsidian4.Notice(`${celebration.emoji} ${celebration.message}${extraMessage}`); } playCompletionSound() { try { @@ -1145,7 +1626,7 @@ var ImmersePlugin = class extends import_obsidian3.Plugin { await this.appendToDailyNote(taskEntry); } catch (e) { console.error("Failed to log task to daily note:", e); - new import_obsidian3.Notice("Failed to log task to daily note. Make sure Daily Notes core plugin is enabled."); + new import_obsidian4.Notice("Failed to log task to daily note. Make sure Daily Notes core plugin is enabled."); } } getDailyNoteSettings() { @@ -1183,14 +1664,14 @@ var ImmersePlugin = class extends import_obsidian3.Plugin { const { vault } = this.app; const dailySettings = this.getDailyNoteSettings(); if (!dailySettings) { - new import_obsidian3.Notice("Daily Notes core plugin is not enabled. Please enable it in Settings \u2192 Core plugins."); + new import_obsidian4.Notice("Daily Notes core plugin is not enabled. Please enable it in Settings \u2192 Core plugins."); return null; } const filename = this.formatDailyNoteDate(dailySettings.format); const folder = dailySettings.folder ? `${dailySettings.folder}/` : ""; const path = `${folder}${filename}.md`; let file = vault.getAbstractFileByPath(path); - if (file && file instanceof import_obsidian3.TFile) { + if (file && file instanceof import_obsidian4.TFile) { return file; } try { @@ -1204,17 +1685,17 @@ var ImmersePlugin = class extends import_obsidian3.Plugin { if (dailySettings.template) { const templatePath = dailySettings.template.endsWith(".md") ? dailySettings.template : `${dailySettings.template}.md`; const templateFile = vault.getAbstractFileByPath(templatePath); - if (templateFile && templateFile instanceof import_obsidian3.TFile) { + if (templateFile && templateFile instanceof import_obsidian4.TFile) { content = await vault.read(templateFile); content = content.replace(/{{date}}/g, filename).replace(/{{time}}/g, new Date().toLocaleTimeString()).replace(/{{title}}/g, filename); } } const newFile = await vault.create(path, content); - new import_obsidian3.Notice(`\u{1F4DD} Created daily note: ${filename}`); + new import_obsidian4.Notice(`\u{1F4DD} Created daily note: ${filename}`); return newFile; } catch (e) { console.error("Failed to create daily note:", e); - new import_obsidian3.Notice("Failed to create daily note"); + new import_obsidian4.Notice("Failed to create daily note"); return null; } } @@ -1227,7 +1708,7 @@ var ImmersePlugin = class extends import_obsidian3.Plugin { const existingContent = await vault.read(file); const newContent = existingContent.trimEnd() + "\n" + content + "\n"; await vault.modify(file, newContent); - new import_obsidian3.Notice("\u{1F4DD} Task logged to daily note"); + new import_obsidian4.Notice("\u{1F4DD} Task logged to daily note"); } // ============ Utilities ============ formatTime(seconds) { @@ -1293,7 +1774,7 @@ var ImmersePlugin = class extends import_obsidian3.Plugin { }; } }; -var ImmerseSettingTab = class extends import_obsidian3.PluginSettingTab { +var ImmerseSettingTab = class extends import_obsidian4.PluginSettingTab { constructor(app, plugin) { super(app, plugin); this.plugin = plugin; @@ -1303,41 +1784,41 @@ var ImmerseSettingTab = class extends import_obsidian3.PluginSettingTab { containerEl.empty(); containerEl.createEl("h1", { text: "\u26A1 Immerse Settings" }); containerEl.createEl("h2", { text: "\u{1F345} Pomodoro Timer" }); - new import_obsidian3.Setting(containerEl).setName("Work Duration").setDesc("Length of each work session in minutes").addSlider((slider) => slider.setLimits(5, 60, 5).setValue(this.plugin.settings.pomodoroWorkMinutes).setDynamicTooltip().onChange(async (value) => { + new import_obsidian4.Setting(containerEl).setName("Work Duration").setDesc("Length of each work session in minutes").addSlider((slider) => slider.setLimits(5, 60, 5).setValue(this.plugin.settings.pomodoroWorkMinutes).setDynamicTooltip().onChange(async (value) => { this.plugin.settings.pomodoroWorkMinutes = value; await this.plugin.saveAllData(); })); - new import_obsidian3.Setting(containerEl).setName("Short Break Duration").setDesc("Length of short breaks in minutes").addSlider((slider) => slider.setLimits(1, 15, 1).setValue(this.plugin.settings.pomodoroBreakMinutes).setDynamicTooltip().onChange(async (value) => { + new import_obsidian4.Setting(containerEl).setName("Short Break Duration").setDesc("Length of short breaks in minutes").addSlider((slider) => slider.setLimits(1, 15, 1).setValue(this.plugin.settings.pomodoroBreakMinutes).setDynamicTooltip().onChange(async (value) => { this.plugin.settings.pomodoroBreakMinutes = value; await this.plugin.saveAllData(); })); - new import_obsidian3.Setting(containerEl).setName("Long Break Duration").setDesc("Length of long breaks in minutes").addSlider((slider) => slider.setLimits(5, 30, 5).setValue(this.plugin.settings.longBreakMinutes).setDynamicTooltip().onChange(async (value) => { + new import_obsidian4.Setting(containerEl).setName("Long Break Duration").setDesc("Length of long breaks in minutes").addSlider((slider) => slider.setLimits(5, 30, 5).setValue(this.plugin.settings.longBreakMinutes).setDynamicTooltip().onChange(async (value) => { this.plugin.settings.longBreakMinutes = value; await this.plugin.saveAllData(); })); - new import_obsidian3.Setting(containerEl).setName("Long Break Interval").setDesc("Number of pomodoros before a long break").addSlider((slider) => slider.setLimits(2, 6, 1).setValue(this.plugin.settings.longBreakInterval).setDynamicTooltip().onChange(async (value) => { + new import_obsidian4.Setting(containerEl).setName("Long Break Interval").setDesc("Number of pomodoros before a long break").addSlider((slider) => slider.setLimits(2, 6, 1).setValue(this.plugin.settings.longBreakInterval).setDynamicTooltip().onChange(async (value) => { this.plugin.settings.longBreakInterval = value; await this.plugin.saveAllData(); })); - new import_obsidian3.Setting(containerEl).setName("Auto-start Breaks").setDesc("Automatically start break timer after work session").addToggle((toggle) => toggle.setValue(this.plugin.settings.autoStartBreak).onChange(async (value) => { + new import_obsidian4.Setting(containerEl).setName("Auto-start Breaks").setDesc("Automatically start break timer after work session").addToggle((toggle) => toggle.setValue(this.plugin.settings.autoStartBreak).onChange(async (value) => { this.plugin.settings.autoStartBreak = value; await this.plugin.saveAllData(); })); containerEl.createEl("h2", { text: "\u2699\uFE0F General" }); - new import_obsidian3.Setting(containerEl).setName("Default Time Estimate").setDesc("Default estimated time for new tasks in minutes").addSlider((slider) => slider.setLimits(5, 120, 5).setValue(this.plugin.settings.defaultEstimateMinutes).setDynamicTooltip().onChange(async (value) => { + new import_obsidian4.Setting(containerEl).setName("Default Time Estimate").setDesc("Default estimated time for new tasks in minutes").addSlider((slider) => slider.setLimits(5, 120, 5).setValue(this.plugin.settings.defaultEstimateMinutes).setDynamicTooltip().onChange(async (value) => { this.plugin.settings.defaultEstimateMinutes = value; await this.plugin.saveAllData(); })); - new import_obsidian3.Setting(containerEl).setName("Enable Sounds").setDesc("Play sounds for timer completion and task completion").addToggle((toggle) => toggle.setValue(this.plugin.settings.enableSounds).onChange(async (value) => { + new import_obsidian4.Setting(containerEl).setName("Enable Sounds").setDesc("Play sounds for timer completion and task completion").addToggle((toggle) => toggle.setValue(this.plugin.settings.enableSounds).onChange(async (value) => { this.plugin.settings.enableSounds = value; await this.plugin.saveAllData(); })); - new import_obsidian3.Setting(containerEl).setName("Enable Celebrations").setDesc("Show celebration messages when completing tasks").addToggle((toggle) => toggle.setValue(this.plugin.settings.enableCelebrations).onChange(async (value) => { + new import_obsidian4.Setting(containerEl).setName("Enable Celebrations").setDesc("Show celebration messages when completing tasks").addToggle((toggle) => toggle.setValue(this.plugin.settings.enableCelebrations).onChange(async (value) => { this.plugin.settings.enableCelebrations = value; await this.plugin.saveAllData(); })); containerEl.createEl("h2", { text: "\u{1F4DD} Daily Note Integration" }); - new import_obsidian3.Setting(containerEl).setName("Log completed tasks to daily note").setDesc("When you complete a task, add an entry to your daily note. Uses the core Daily Notes plugin settings.").addToggle((toggle) => toggle.setValue(this.plugin.settings.logToDaily).onChange(async (value) => { + new import_obsidian4.Setting(containerEl).setName("Log completed tasks to daily note").setDesc("When you complete a task, add an entry to your daily note. Uses the core Daily Notes plugin settings.").addToggle((toggle) => toggle.setValue(this.plugin.settings.logToDaily).onChange(async (value) => { this.plugin.settings.logToDaily = value; await this.plugin.saveAllData(); })); @@ -1353,7 +1834,7 @@ var ImmerseSettingTab = class extends import_obsidian3.PluginSettingTab { `; containerEl.createEl("h2", { text: "\u{1F4CB} Lists" }); this.plugin.settings.lists.forEach((list, index) => { - new import_obsidian3.Setting(containerEl).setName(`${list.icon} ${list.name}`).addText((text) => text.setValue(list.name).setPlaceholder("List name").onChange(async (value) => { + new import_obsidian4.Setting(containerEl).setName(`${list.icon} ${list.name}`).addText((text) => text.setValue(list.name).setPlaceholder("List name").onChange(async (value) => { this.plugin.settings.lists[index].name = value; await this.plugin.saveAllData(); })).addText((text) => text.setValue(list.icon).setPlaceholder("Emoji").onChange(async (value) => { @@ -1368,7 +1849,7 @@ var ImmerseSettingTab = class extends import_obsidian3.PluginSettingTab { this.display(); })); }); - new import_obsidian3.Setting(containerEl).addButton((btn) => btn.setButtonText("+ Add List").onClick(async () => { + new import_obsidian4.Setting(containerEl).addButton((btn) => btn.setButtonText("+ Add List").onClick(async () => { this.plugin.settings.lists.push({ id: this.plugin.generateId(), name: "New List", diff --git a/manifest.json b/manifest.json index ce06649..f6215ae 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "immerse", "name": "Immerse", - "version": "1.0.9", + "version": "1.1.0", "minAppVersion": "0.15.0", "description": "A Blitzit-inspired task management and focus timer plugin. Plan your day, track time with Pomodoro technique, and crush your tasks with satisfying checkoffs.", "author": "Crib", diff --git a/package.json b/package.json index 2e46df7..6faea98 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "immerse", - "version": "1.0.9", + "version": "1.1.0", "description": "A Blitzit-inspired task management and focus timer plugin for Obsidian", "main": "main.js", "scripts": { diff --git a/src/main.ts b/src/main.ts index 6be170f..e688c13 100644 --- a/src/main.ts +++ b/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 = {}; + 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 = {}; + 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 = {}; + + 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 = {}; + + 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 diff --git a/src/modals.ts b/src/modals.ts index 8a44c20..518ead6 100644 --- a/src/modals.ts +++ b/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(); diff --git a/src/reportView.ts b/src/reportView.ts new file mode 100644 index 0000000..279fccf --- /dev/null +++ b/src/reportView.ts @@ -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' }); + }); + } +} diff --git a/src/types.ts b/src/types.ts index ce0e0ae..2ab556e 100644 --- a/src/types.ts +++ b/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; // listId -> count + minutesByList: Record; // 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 = [ diff --git a/src/view.ts b/src/view.ts index a191a12..fed6ada 100644 --- a/src/view.ts +++ b/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: '🔥' }, ]; diff --git a/styles.css b/styles.css index 88f24cc..736530f 100644 --- a/styles.css +++ b/styles.css @@ -648,4 +648,798 @@ justify-content: flex-end; margin-top: 8px; } +} + +/* ============ Report View ============ */ +.immerse-report-view { + padding: 20px; + overflow-y: auto; + height: 100%; +} + +.immerse-report-header { + margin-bottom: 20px; + padding-bottom: 10px; + border-bottom: 2px solid var(--ft-border); +} + +.immerse-report-title { + font-size: 28px; + font-weight: 700; + margin: 0 0 10px 0; + color: var(--ft-text); +} + +.immerse-report-filters { + background: var(--ft-bg-secondary); + padding: 12px; + border-radius: var(--ft-radius); + margin-bottom: 16px; +} + +.immerse-report-filter-row { + display: flex; + gap: 12px; + margin-bottom: 12px; + flex-wrap: wrap; + align-items: center; +} + +.immerse-report-filter-row .setting-item { + flex: 1; + min-width: 200px; + margin: 0 !important; + padding: 0 !important; + border: none !important; + display: flex !important; + flex-direction: row !important; + align-items: center !important; + gap: 12px; +} + +.immerse-report-filter-row .setting-item > * { + margin: 0 !important; + padding: 0 !important; +} + +.immerse-report-filter-row .setting-item-info { + flex-shrink: 0 !important; + width: auto !important; + min-width: 80px !important; + max-width: 100px !important; + padding: 0 !important; + margin: 0 !important; +} + +.immerse-report-filter-row .setting-item-control { + flex-grow: 1 !important; + padding: 0 !important; + margin: 0 !important; +} + +.immerse-report-filter-row .setting-item-name { + margin: 0 !important; + padding: 0 !important; + line-height: 1.4 !important; +} + +.immerse-report-filter-row .setting-item-description { + display: none !important; +} + +.immerse-report-filter-row input[type="date"] { + width: 100% !important; + margin: 0 !important; +} + +.immerse-report-quick-filters { + display: flex; + gap: 8px; + align-items: center; + flex-wrap: wrap; +} + +.immerse-filter-label { + font-weight: 500; + color: var(--ft-text); + font-size: 14px; +} + +.immerse-quick-filter-btn { + padding: 6px 12px; + border-radius: 6px; + background: var(--ft-bg); + border: 1px solid var(--ft-border); + color: var(--ft-text); + cursor: pointer; + transition: all 0.2s ease; + font-size: 13px; + white-space: nowrap; +} + +.immerse-quick-filter-btn:hover { + background: var(--ft-primary); + color: white; + border-color: var(--ft-primary); +} + +.immerse-report-generate-btn { + width: 100%; + margin-bottom: 20px; +} + +.immerse-report-content { + margin-top: 20px; +} + +.immerse-no-data-message { + text-align: center; + padding: 60px 20px; + color: var(--ft-text-muted); + font-size: 16px; +} + +/* Stats Grid */ +.immerse-stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 16px; + margin-bottom: 30px; +} + +.immerse-stat-card { + background: var(--ft-bg-secondary); + padding: 16px; + border-radius: var(--ft-radius); + text-align: center; + border: 1px solid var(--ft-border); +} + +.immerse-stat-label { + font-size: 11px; + font-weight: 600; + color: var(--ft-text-muted); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 10px; +} + +.immerse-stat-value-row { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +.immerse-stat-icon { + font-size: 24px; +} + +.immerse-stat-value { + font-size: 28px; + font-weight: 700; + color: var(--ft-text); +} + +/* Report Sections */ +.immerse-report-section { + margin-bottom: 40px; +} + +.immerse-report-section-title { + font-size: 20px; + font-weight: 700; + margin-bottom: 20px; + color: var(--ft-text); + padding-bottom: 10px; + border-bottom: 2px solid var(--ft-border); + position: relative; +} + +.immerse-report-section-title::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + width: 60px; + height: 2px; + background: linear-gradient(90deg, var(--ft-primary), var(--ft-success)); +} + +/* Time by List */ +.immerse-time-by-list { + display: flex; + flex-direction: column; + gap: 16px; +} + +.immerse-list-stat-item { + background: var(--ft-bg-secondary); + padding: 16px; + border-radius: 10px; + border: 1px solid var(--ft-border); + transition: all 0.2s ease; +} + +.immerse-list-stat-item:hover { + transform: translateX(4px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); +} + +.immerse-list-info { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 10px; +} + +.immerse-list-icon { + font-size: 22px; + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1)); +} + +.immerse-list-name { + font-weight: 600; + color: var(--ft-text); + font-size: 16px; +} + +.immerse-list-progress { + width: 100%; + height: 10px; + background: var(--ft-bg); + border-radius: 5px; + overflow: hidden; + margin-bottom: 10px; + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.immerse-list-progress-fill { + height: 100%; + transition: width 0.6s ease; + border-radius: 5px; +} + +.immerse-list-stats { + display: flex; + justify-content: space-between; + font-size: 13px; + gap: 8px; + flex-wrap: wrap; +} + +.immerse-list-stat-text { + color: var(--ft-text-muted); +} + +.immerse-list-stat-percentage { + font-weight: 600; + color: var(--ft-text); +} + +/* Insights */ +.immerse-insights-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 20px; +} + +.immerse-insight-card { + background: var(--ft-bg-secondary); + padding: 28px; + border-radius: 16px; + text-align: center; + border: 2px solid var(--ft-border); + position: relative; + overflow: visible; + transition: all 0.3s ease; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.immerse-insight-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: var(--ft-primary); + border-radius: 16px 16px 0 0; +} + +.immerse-insight-card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12); + border-color: var(--ft-primary); +} + +.immerse-insight-icon { + font-size: 48px; + margin-bottom: 12px; + display: block; + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1)); +} + +.immerse-insight-label { + font-size: 10px; + font-weight: 700; + color: var(--ft-text-muted); + text-transform: uppercase; + letter-spacing: 1.5px; + margin-bottom: 16px; + display: block; +} + +.immerse-insight-value { + font-size: 32px; + font-weight: 800; + color: var(--ft-primary); + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.08); + line-height: 1.2; + display: block; +} + +/* Daily Breakdown - Enhanced Visual Chart Style */ +.immerse-daily-breakdown-container { + display: flex; + flex-direction: column; + gap: 16px; +} + +.immerse-daily-row { + display: grid; + grid-template-columns: 110px 1fr; + gap: 20px; + align-items: center; + padding: 20px; + background: linear-gradient(135deg, var(--ft-bg-secondary) 0%, var(--ft-bg) 100%); + border-radius: 12px; + border: 2px solid var(--ft-border); + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.immerse-daily-row::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 4px; + background: linear-gradient(180deg, var(--ft-primary), var(--ft-success)); + opacity: 0; + transition: opacity 0.3s ease; +} + +.immerse-daily-row:hover::before { + opacity: 1; +} + +.immerse-daily-row:hover { + transform: translateX(6px); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); + border-color: var(--ft-primary); +} + +.immerse-daily-date { + font-weight: 800; + color: var(--ft-text); + font-size: 14px; + text-align: center; + display: flex; + flex-direction: column; + gap: 4px; +} + +.immerse-daily-date-day { + font-size: 11px; + color: var(--ft-text-muted); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 1px; +} + +.immerse-daily-date-num { + font-size: 16px; + color: var(--ft-primary); +} + +.immerse-daily-bars { + display: flex; + flex-direction: column; + gap: 12px; + flex: 1; +} + +.immerse-daily-bar-row { + display: flex; + align-items: center; + gap: 12px; +} + +.immerse-daily-bar-label { + min-width: 90px; + font-size: 12px; + color: var(--ft-text-muted); + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; + display: flex; + align-items: center; + gap: 6px; +} + +.immerse-daily-bar-icon { + font-size: 16px; +} + +.immerse-daily-bar-track { + flex: 1; + height: 12px; + background: var(--ft-bg); + border-radius: 6px; + overflow: hidden; + position: relative; + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.immerse-daily-bar-fill { + height: 100%; + border-radius: 6px; + transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; +} + +/* Pie chart for daily breakdown */ +.immerse-daily-pie-container { + display: flex; + align-items: center; + justify-content: center; + gap: 40px; + margin-top: 30px; +} + +.immerse-daily-pie-chart { + position: relative; + width: 280px; + height: 280px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + /* Background set via inline style for proper gradient rendering */ + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); + transition: transform 0.3s ease; +} + +.immerse-daily-pie-chart:hover { + transform: scale(1.05); +} + +.immerse-daily-pie-center { + position: absolute; + width: 160px; + height: 160px; + background: var(--ft-bg); + border-radius: 50%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); +} + +.immerse-daily-pie-center-value { + font-size: 36px; + font-weight: 800; + color: var(--ft-text-primary); + line-height: 1; +} + +.immerse-daily-pie-center-label { + font-size: 12px; + font-weight: 500; + color: var(--ft-text-secondary); + text-transform: uppercase; + letter-spacing: 1px; + margin-top: 4px; +} + +.immerse-daily-pie-legend { + display: flex; + flex-direction: column; + gap: 20px; +} + +.immerse-daily-pie-legend-item { + display: flex; + align-items: center; + gap: 12px; +} + +.immerse-daily-pie-legend-color { + width: 24px; + height: 24px; + border-radius: 6px; + flex-shrink: 0; +} + +.immerse-daily-pie-legend-info { + display: flex; + flex-direction: column; + gap: 2px; +} + +.immerse-daily-pie-legend-label { + font-size: 13px; + font-weight: 600; + color: var(--ft-text-primary); + display: flex; + align-items: center; + gap: 6px; +} + +.immerse-daily-pie-legend-value { + font-size: 20px; + font-weight: 700; + color: var(--ft-text-primary); +} + +.immerse-daily-pie-legend-percentage { + font-size: 12px; + color: var(--ft-text-secondary); + font-weight: 500; +} + +.immerse-daily-bar-fill.tasks { + background: linear-gradient(90deg, var(--ft-primary), #7c3aed); +} + +.immerse-daily-bar-fill.hours { + background: linear-gradient(90deg, var(--ft-success), #059669); +} + +.immerse-daily-bar-fill.pomodoros { + background: linear-gradient(90deg, var(--ft-warning), #dc2626); +} + +.immerse-daily-bar-value { + min-width: 50px; + text-align: right; + font-weight: 800; + font-size: 14px; + color: var(--ft-text); + font-feature-settings: 'tnum'; + font-variant-numeric: tabular-nums; +} + +/* Responsive adjustments for smaller screens */ +@media (max-width: 768px) { + .immerse-report-view { + padding: 15px; + } + + .immerse-stats-grid { + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 12px; + } + + .immerse-insights-grid { + grid-template-columns: 1fr; + } + + .immerse-report-filter-row { + flex-direction: column; + } + + .immerse-report-filter-row .setting-item { + min-width: 100%; + } + + /* Pie chart mobile optimization */ + .immerse-daily-pie-container { + flex-direction: column; + gap: 30px; + } + + .immerse-daily-pie-chart { + width: 220px; + height: 220px; + } + + .immerse-daily-pie-center { + width: 130px; + height: 130px; + } + + .immerse-daily-pie-center-value { + font-size: 28px; + } + + /* Daily breakdown mobile optimization */ + .immerse-daily-row { + flex-direction: column; + gap: 12px; + } + + .immerse-daily-date { + flex-direction: row; + gap: 8px; + align-items: center; + margin-bottom: 8px; + } + + .immerse-daily-bars { + width: 100%; + } + + .immerse-daily-bar-label { + min-width: 70px; + font-size: 11px; + } + + .immerse-daily-bar-track { + flex: 1; + } + + .immerse-daily-bar-value { + min-width: 40px; + font-size: 12px; + } + + /* Task list mobile optimization */ + .immerse-task-item { + padding: 10px 12px; + font-size: 14px; + } + + .immerse-task-actions { + opacity: 1; + } + + .immerse-task-btn { + width: 36px; + height: 36px; + font-size: 16px; + } + + /* Active card mobile optimization */ + .immerse-active-card { + padding: 20px 16px; + } + + .immerse-timer-time { + font-size: 40px; + } + + /* Stats bar mobile optimization */ + .immerse-stats-bar { + grid-template-columns: repeat(2, 1fr); + gap: 10px; + } + + .immerse-stat-item { + padding: 10px; + } + + .immerse-stat-value { + font-size: 16px; + } + + .immerse-stat-label { + font-size: 10px; + } + + /* Modal optimization for mobile */ + .immerse-modal { + padding: 16px; + max-height: 80vh; + overflow-y: auto; + } + + .immerse-modal-buttons { + flex-direction: column; + gap: 10px; + } + + .immerse-modal-buttons .immerse-btn { + width: 100%; + padding: 12px 16px; + font-size: 14px; + min-height: 44px; + } + + /* Increase touch target sizes */ + .immerse-checkbox { + width: 28px; + height: 28px; + min-width: 28px; + } + + .immerse-filter-btn { + padding: 8px 14px; + font-size: 14px; + min-height: 40px; + } + + /* Better spacing for buttons */ + .immerse-header-actions { + width: 100%; + justify-content: stretch; + } + + .immerse-header-actions .immerse-btn { + flex: 1; + min-height: 44px; + } +} + +/* Extra small screens (phones in portrait) */ +@media (max-width: 480px) { + .immerse-container { + padding: 12px; + } + + .immerse-report-view { + padding: 12px; + } + + /* Make pie chart smaller on very small screens */ + .immerse-daily-pie-chart { + width: 180px; + height: 180px; + } + + .immerse-daily-pie-center { + width: 110px; + height: 110px; + } + + .immerse-daily-pie-center-value { + font-size: 24px; + } + + .immerse-daily-pie-center-label { + font-size: 10px; + } + + .immerse-daily-pie-legend-value { + font-size: 18px; + } + + /* Stack stats grid in single column on very small screens */ + .immerse-stats-grid { + grid-template-columns: 1fr; + } + + /* Smaller buttons and text */ + .immerse-btn { + padding: 6px 12px; + font-size: 13px; + } + + .immerse-timer-time { + font-size: 36px; + } + + .immerse-active-task-name { + font-size: 18px; + } + + /* Quick filters wrap better */ + .immerse-quick-filter-btn { + padding: 5px 10px; + font-size: 12px; + } + + /* Report title smaller */ + .immerse-report-title { + font-size: 24px; + } + + .immerse-report-section-title { + font-size: 18px; + } } \ No newline at end of file