Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,92 @@ print(summary.totals.totalOvertime) -- Total overtime
print(summary.weekdays.Monday.workedHours) -- Monday's hours
```

## Daily Overview (Tagesübersicht)

The daily overview feature allows you to dive deeper into individual days from the weekly overview. It provides detailed information about a specific day's work patterns, goal achievement, and project/file breakdowns.

### Navigation from Weekly Overview

From the weekly overview window, you can navigate to daily overviews using numbered keys:

- **1** = Monday (Montag)
- **2** = Tuesday (Dienstag)
- **3** = Wednesday (Mittwoch)
- **4** = Thursday (Donnerstag)
- **5** = Friday (Freitag)
- **6** = Saturday (Samstag)
- **7** = Sunday (Sonntag)

### Daily Overview Features

Each daily overview shows:

#### Work Time Summary
- **Worked hours** vs. **daily target** with visual status indicators
- **Overtime calculation** (positive/negative)
- **Goal achievement status**: 🟢 Erreicht (reached) or 🔴 Nicht erreicht (not reached)

#### Work Periods
- **Start and end times** for the day
- **Break/pause time** calculation
- **Work period format**: Shows actual work sessions separated by breaks (e.g., "8-12 Uhr Pause: 1.0h 13-17 Uhr")

#### Project and File Breakdown
- **Time spent per project** in minutes
- **Time spent per file** within each project in minutes
- **Sorted by most worked time** (descending order)

### Sample Daily Overview

```
═══ Tagesübersicht - Montag, KW 11/2023 ═══

┌─ Arbeitszeit-Übersicht ──────────────────────────────────────┐
│ Gearbeitet: 8.00 Stunden │
│ Tagesziel: 8.00 Stunden │
│ Überstunden: +0.00 Stunden │
│ Status: 🟢 Erreicht │
└──────────────────────────────────────────────────────────────┘

┌─ Arbeitszeiten ──────────────────────────────────────────────┐
│ Von: 08:00 bis 17:00 │
│ Pause: 1.0h │
│ Format: 8-12 Uhr Pause: 1.0h 13-17 Uhr │
└──────────────────────────────────────────────────────────────┘

┌─ Projekte/Dateien (in Minuten) ─────────────────────────────┐
│ 📁 WorkProject 480 min │
│ 📄 main.lua 240 min │
│ 📄 test.lua 240 min │
│ 📁 PersonalProject 120 min │
│ 📄 hobby.lua 120 min │
└──────────────────────────────────────────────────────────────┘
```

### Navigation Controls

In the daily overview:
- **q** or **Esc**: Close the daily overview
- **b**: Return to the weekly overview

### Programmatic Access

You can also access daily summary data programmatically:

```lua
-- Get detailed daily summary
local dailySummary = require('maorun.time').getDailySummary({
year = '2023',
week = '11',
weekday = 'Monday'
})

-- Show daily overview window
require('maorun.time').showDailyOverview({
weekday = 'Monday'
})
```

## Time Export

The plugin supports exporting time tracking data in CSV and Markdown formats for weekly or monthly periods. This is useful for billing, reporting, or personal analysis.
Expand Down
180 changes: 180 additions & 0 deletions lua/maorun/time/core.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1577,6 +1577,186 @@ function M._calculatePauseTime(year_str, week_str, weekday)
return 0
end

---Get detailed daily summary data
---@param opts { year?: string, week?: string, weekday: string }
---@return table Daily summary with projects, files, time periods, and goal status
function M.getDailySummary(opts)
opts = opts or {}

-- Get current week if not specified
local current_time = os.time()
local year_str = tostring(opts.year or os.date('%Y', current_time))
local week_str = tostring(opts.week or os.date('%W', current_time))
local weekday = opts.weekday

if not weekday then
error('weekday parameter is required')
end

-- Initialize daily summary structure
local summary = {
year = year_str,
week = week_str,
weekday = weekday,
workedHours = 0,
expectedHours = config_module.obj.content.hoursPerWeekday[weekday] or 0,
overtime = 0,
goalAchieved = false,
pauseTime = 0,
workPeriods = {},
projects = {},
earliestStart = nil,
latestEnd = nil,
}

-- Calculate overtime
summary.overtime = summary.workedHours - summary.expectedHours
summary.goalAchieved = summary.workedHours >= summary.expectedHours

-- Get weekday data
local weekday_data = utils.getWeekdayData(year_str, week_str, weekday)
if not weekday_data then
return summary
end

-- Collect all time entries and calculate totals
local all_entries = {}

for project_name, project_data in pairs(weekday_data) do
if project_name ~= 'summary' and type(project_data) == 'table' then
summary.projects[project_name] = {
totalHours = 0,
files = {},
}

for file_name, file_data in pairs(project_data) do
if file_name ~= 'summary' and type(file_data) == 'table' then
local file_hours = 0
local file_entries = {}

if file_data.items then
for _, entry in ipairs(file_data.items) do
if entry.startTime and entry.endTime and entry.diffInHours then
table.insert(all_entries, {
startTime = entry.startTime,
endTime = entry.endTime,
startReadable = entry.startReadable,
endReadable = entry.endReadable,
diffInHours = entry.diffInHours,
project = project_name,
file = file_name,
})

table.insert(file_entries, {
startTime = entry.startTime,
endTime = entry.endTime,
startReadable = entry.startReadable,
endReadable = entry.endReadable,
diffInHours = entry.diffInHours,
})

file_hours = file_hours + entry.diffInHours
summary.workedHours = summary.workedHours + entry.diffInHours
end
end
end

if file_hours > 0 then
summary.projects[project_name].files[file_name] = {
hours = file_hours,
entries = file_entries,
}
summary.projects[project_name].totalHours = summary.projects[project_name].totalHours
+ file_hours
end
end
end

-- Remove projects with no worked hours
if summary.projects[project_name].totalHours == 0 then
summary.projects[project_name] = nil
end
end
end

-- Update calculations with actual worked hours
summary.overtime = summary.workedHours - summary.expectedHours
summary.goalAchieved = summary.workedHours >= summary.expectedHours

-- Calculate pause time and work periods
if #all_entries > 0 then
-- Sort entries by start time
table.sort(all_entries, function(a, b)
return a.startTime < b.startTime
end)

summary.earliestStart = all_entries[1].startTime
summary.latestEnd = all_entries[#all_entries].endTime

-- Find latest end time (might not be the last entry if they overlap)
for _, entry in ipairs(all_entries) do
if not summary.latestEnd or entry.endTime > summary.latestEnd then
summary.latestEnd = entry.endTime
end
end

-- Calculate pause time using existing function
summary.pauseTime = M._calculatePauseTime(year_str, week_str, weekday)

-- Create work periods by detecting gaps in work
summary.workPeriods = M._calculateWorkPeriods(all_entries)
end

return summary
end

---Calculate work periods based on time entries, detecting breaks between work sessions
---@param entries table Array of time entries sorted by start time
---@return table Array of work periods with start/end times
function M._calculateWorkPeriods(entries)
if #entries == 0 then
return {}
end

local work_periods = {}
local current_period = {
start = entries[1].startTime,
startReadable = entries[1].startReadable,
end_time = entries[1].endTime,
endReadable = entries[1].endReadable,
}

-- Minimum gap in minutes to consider a break (30 minutes)
local min_break_gap = 30 * 60

for i = 2, #entries do
local curr_entry = entries[i]

-- Calculate gap between current period end and current entry start
local gap = curr_entry.startTime - current_period.end_time

if gap >= min_break_gap then
-- Found a significant break, end current period and start new one
table.insert(work_periods, current_period)
current_period = {
start = curr_entry.startTime,
startReadable = curr_entry.startReadable,
end_time = curr_entry.endTime,
endReadable = curr_entry.endReadable,
}
else
-- Continue current period, extend end time
current_period.end_time = curr_entry.endTime
current_period.endReadable = curr_entry.endReadable
end
end

-- Don't forget to add the last period
table.insert(work_periods, current_period)

return work_periods
end

-- Zeit-Validierung & Korrekturmodus (Time Validation & Correction Mode)

---Detect overlapping time entries within the same day/project/file
Expand Down
13 changes: 13 additions & 0 deletions lua/maorun/time/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,15 @@ function M.setup(user_config)
weeklyOverview = function(opts)
ui.showWeeklyOverview(opts or {})
end,
getWeeklySummary = function(opts)
return core.getWeeklySummary(opts or {})
end,
getDailySummary = function(opts)
return core.getDailySummary(opts or {})
end,
dailyOverview = function(opts)
ui.showDailyOverview(opts or {})
end,
validate = function(opts)
return core.validateTimeData(opts or {})
end,
Expand Down Expand Up @@ -191,9 +200,13 @@ M.calculate = function(opts) -- Match the public Time.calculate behavior
end
M.exportTimeData = core.exportTimeData -- Expose the export function
M.getWeeklySummary = core.getWeeklySummary -- Expose the weekly summary function
M.getDailySummary = core.getDailySummary -- Expose the daily summary function
M.showWeeklyOverview = function(opts)
ui.showWeeklyOverview(opts or {})
end
M.showDailyOverview = function(opts)
ui.showDailyOverview(opts or {})
end
M.weekdays = config_module.weekdayNumberMap -- Expose weekday map
M.get_config = core.get_config -- Expose the get_config function
M.validateTimeData = core.validateTimeData -- Expose the validation function
Expand Down
Loading
Loading