diff --git a/README.md b/README.md index 9e2388b..ccf4817 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/lua/maorun/time/core.lua b/lua/maorun/time/core.lua index 009bcb3..24bc376 100644 --- a/lua/maorun/time/core.lua +++ b/lua/maorun/time/core.lua @@ -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 diff --git a/lua/maorun/time/init.lua b/lua/maorun/time/init.lua index 133f065..68762a1 100644 --- a/lua/maorun/time/init.lua +++ b/lua/maorun/time/init.lua @@ -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, @@ -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 diff --git a/lua/maorun/time/ui.lua b/lua/maorun/time/ui.lua index 46521e8..8fa83a9 100644 --- a/lua/maorun/time/ui.lua +++ b/lua/maorun/time/ui.lua @@ -777,6 +777,10 @@ function M._formatWeeklySummaryContent(summary, opts) table.insert(content, '') table.insert(content, 'DrΓΌcke q zum Schließen, f fΓΌr Filter-Optionen, d fΓΌr Datei-Details') + table.insert( + content, + 'DrΓΌcke 1-7 fΓΌr TagesΓΌbersicht (Mo-So): 1=Mo, 2=Di, 3=Mi, 4=Do, 5=Fr, 6=Sa, 7=So' + ) return content end @@ -982,6 +986,24 @@ function M._showFloatingWindowWithDetails(content, title, summary, opts) callback = toggle_details, }) + -- Daily overview navigation - numbered keys 1-7 for Monday to Sunday + local weekday_order = + { 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday' } + for i, weekday in ipairs(weekday_order) do + vim.api.nvim_buf_set_keymap(buf, 'n', tostring(i), '', { + noremap = true, + silent = true, + callback = function() + close_window() + M.showDailyOverview({ + year = summary.year, + week = summary.week, + weekday = weekday, + }) + end, + }) + end + -- Set window options vim.api.nvim_win_set_option(win, 'wrap', false) vim.api.nvim_win_set_option(win, 'cursorline', true) @@ -1601,4 +1623,346 @@ function M._performValidationAction(action, issue, callback) end end +---Show daily overview for a specific day +---@param opts { year?: string, week?: string, weekday: string } +function M.showDailyOverview(opts) + opts = opts or {} + + if not opts.weekday then + notify( + 'Weekday parameter is required for daily overview', + 'error', + { title = 'TimeTracking' } + ) + return + end + + -- Get daily summary data + local summary = core.getDailySummary(opts) + + -- Format the content + local content = M._formatDailySummaryContent(summary) + + -- Show in floating window + M._showDailyFloatingWindow( + content, + 'TagesΓΌbersicht - ' + .. M._getGermanWeekdayName(summary.weekday) + .. ' (KW ' + .. summary.week + .. '/' + .. summary.year + .. ')', + summary + ) +end + +---Format daily summary content for display +---@param summary table Daily summary data +---@return table Array of content lines +function M._formatDailySummaryContent(summary) + local content = {} + + local weekday_name_de = M._getGermanWeekdayName(summary.weekday) + + -- Header + table.insert( + content, + string.format( + '═══ TagesΓΌbersicht - %s, KW %s/%s ═══', + weekday_name_de, + summary.week, + summary.year + ) + ) + table.insert(content, '') + + -- Goal achievement status + local goal_status = summary.goalAchieved and '🟒 Erreicht' or 'πŸ”΄ Nicht erreicht' + local overtime_sign = summary.overtime >= 0 and '+' or '' + + table.insert( + content, + 'β”Œβ”€ Arbeitszeit-Übersicht ──────────────────────────────────────┐' + ) + table.insert( + content, + string.format( + 'β”‚ Gearbeitet: %8.2f Stunden β”‚', + summary.workedHours + ) + ) + table.insert( + content, + string.format( + 'β”‚ Tagesziel: %8.2f Stunden β”‚', + summary.expectedHours + ) + ) + table.insert( + content, + string.format( + 'β”‚ Überstunden: %s%7.2f Stunden β”‚', + overtime_sign, + summary.overtime + ) + ) + table.insert( + content, + string.format('β”‚ Status: %-8s β”‚', goal_status) + ) + table.insert( + content, + 'β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜' + ) + table.insert(content, '') + + -- Work periods (if any work was done) + if summary.workedHours > 0 and summary.earliestStart and summary.latestEnd then + table.insert( + content, + 'β”Œβ”€ Arbeitszeiten ──────────────────────────────────────────────┐' + ) + + local total_span_hours = (summary.latestEnd - summary.earliestStart) / 3600 + + table.insert( + content, + string.format( + 'β”‚ Von: %s bis %s β”‚', + os.date('%H:%M', summary.earliestStart), + os.date('%H:%M', summary.latestEnd) + ) + ) + + if summary.pauseTime > 0 then + table.insert( + content, + string.format( + 'β”‚ Pause: %.1fh β”‚', + summary.pauseTime + ) + ) + + -- Show actual work periods instead of hardcoded format + if summary.workPeriods and #summary.workPeriods > 0 then + local period_strings = {} + for _, period in ipairs(summary.workPeriods) do + table.insert( + period_strings, + string.format( + '%s-%s Uhr', + os.date('%H', period.start), + os.date('%H', period.end_time) + ) + ) + end + + -- Calculate average pause time between periods (if multiple periods exist) + local pause_display = '' + if #summary.workPeriods > 1 and summary.pauseTime > 0 then + local avg_pause = summary.pauseTime / (#summary.workPeriods - 1) -- gaps between periods + pause_display = string.format(' Pause: %.1fh ', avg_pause) + elseif #summary.workPeriods == 1 and summary.pauseTime > 0 then + pause_display = string.format(' Pause: %.1fh ', summary.pauseTime) + else + pause_display = ' ' + end + + local periods_text = table.concat(period_strings, pause_display) + table.insert( + content, + string.format('β”‚ Format: %s β”‚', periods_text) + ) + end + else + table.insert( + content, + string.format( + 'β”‚ Durchgehend: %s-%s Uhr (%.1fh) β”‚', + os.date('%H', summary.earliestStart), + os.date('%H', summary.latestEnd), + total_span_hours + ) + ) + end + + table.insert( + content, + 'β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜' + ) + table.insert(content, '') + end + + -- Project breakdown + if next(summary.projects) then + table.insert( + content, + 'β”Œβ”€ Projekte/Dateien (in Minuten) ─────────────────────────────┐' + ) + + -- Sort projects by total hours (descending) + local sorted_projects = {} + for project_name, project_data in pairs(summary.projects) do + table.insert(sorted_projects, { + name = project_name, + hours = project_data.totalHours, + files = project_data.files, + }) + end + table.sort(sorted_projects, function(a, b) + return a.hours > b.hours + end) + + for _, project in ipairs(sorted_projects) do + local project_minutes = math.floor(project.hours * 60) + table.insert( + content, + string.format('β”‚ πŸ“ %-20s %25d min β”‚', project.name, project_minutes) + ) + + -- Sort files by hours (descending) + local sorted_files = {} + for file_name, file_data in pairs(project.files) do + table.insert(sorted_files, { + name = file_name, + hours = file_data.hours, + }) + end + table.sort(sorted_files, function(a, b) + return a.hours > b.hours + end) + + for _, file in ipairs(sorted_files) do + local file_minutes = math.floor(file.hours * 60) + table.insert( + content, + string.format('β”‚ πŸ“„ %-18s %25d min β”‚', file.name, file_minutes) + ) + end + + -- Add separator between projects (except for last one) + if project ~= sorted_projects[#sorted_projects] then + table.insert( + content, + 'β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€' + ) + end + end + + table.insert( + content, + 'β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜' + ) + else + table.insert( + content, + 'β”Œβ”€ Projekte/Dateien ──────────────────────────────────────────┐' + ) + table.insert(content, 'β”‚ Keine Arbeitszeit an diesem Tag erfasst. β”‚') + table.insert( + content, + 'β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜' + ) + end + + table.insert(content, '') + table.insert(content, 'DrΓΌcke q zum Schließen, b um zur WochenΓΌbersicht zurΓΌckzukehren') + + return content +end + +---Show daily overview in floating window +---@param content table Array of content lines +---@param title string Window title +---@param summary table Daily summary data +function M._showDailyFloatingWindow(content, title, summary) + -- Calculate window size + local max_width = 0 + for _, line in ipairs(content) do + max_width = math.max(max_width, vim.fn.strdisplaywidth(line)) + end + + local width = math.min(max_width + 4, vim.o.columns - 10) + local height = math.min(#content + 2, vim.o.lines - 10) + + -- Calculate position (centered) + local row = math.floor((vim.o.lines - height) / 2) + local col = math.floor((vim.o.columns - width) / 2) + + -- Create buffer + local buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_lines(buf, 0, -1, false, content) + vim.api.nvim_buf_set_option(buf, 'modifiable', false) + vim.api.nvim_buf_set_option(buf, 'bufhidden', 'wipe') + + -- Create window + local win = vim.api.nvim_open_win(buf, true, { + relative = 'editor', + width = width, + height = height, + row = row, + col = col, + style = 'minimal', + border = 'rounded', + title = title, + title_pos = 'center', + }) + + -- Set key mappings for the floating window + local function close_window() + if vim.api.nvim_win_is_valid(win) then + vim.api.nvim_win_close(win, true) + end + end + + local function back_to_weekly() + close_window() + -- Return to weekly overview + M.showWeeklyOverview({ + year = summary.year, + week = summary.week, + }) + end + + -- Key mappings + vim.api.nvim_buf_set_keymap(buf, 'n', 'q', '', { + noremap = true, + silent = true, + callback = close_window, + }) + + vim.api.nvim_buf_set_keymap(buf, 'n', '', '', { + noremap = true, + silent = true, + callback = close_window, + }) + + vim.api.nvim_buf_set_keymap(buf, 'n', 'b', '', { + noremap = true, + silent = true, + callback = back_to_weekly, + }) + + -- Set window options + vim.api.nvim_win_set_option(win, 'wrap', false) + vim.api.nvim_win_set_option(win, 'cursorline', true) +end + +---Get German weekday name +---@param weekday string English weekday name +---@return string German weekday name +function M._getGermanWeekdayName(weekday) + local weekday_names_de = { + Monday = 'Montag', + Tuesday = 'Dienstag', + Wednesday = 'Mittwoch', + Thursday = 'Donnerstag', + Friday = 'Freitag', + Saturday = 'Samstag', + Sunday = 'Sonntag', + } + return weekday_names_de[weekday] or weekday +end + return M diff --git a/test/weekly_overview_spec.lua b/test/weekly_overview_spec.lua index 7a1d2be..2a2883b 100644 --- a/test/weekly_overview_spec.lua +++ b/test/weekly_overview_spec.lua @@ -485,3 +485,173 @@ describe('Weekly Overview Functionality', function() end) end) end) + +describe('Daily Overview Functionality', function() + describe('getDailySummary', function() + it('should return empty summary for day with no data', function() + local mock_time = 1678704000 -- March 13, 2023, week 11 + os_module.time = function() + return mock_time + end + os_module.date = function(format, time_val) + time_val = time_val or mock_time + return original_os_date(format, time_val) + end + + maorunTime.setup({ path = tempPath }) + + local summary = maorunTime.getDailySummary({ weekday = 'Monday' }) + + -- Should return valid structure with zero values + assert.is_table(summary) + assert.are.equal('Monday', summary.weekday) + assert.are.equal(0, summary.workedHours) + assert.are.equal(8, summary.expectedHours) -- Default config + assert.are.equal(-8, summary.overtime) + assert.is_false(summary.goalAchieved) + assert.are.equal(0, summary.pauseTime) + assert.is_table(summary.projects) + assert.is_nil(summary.earliestStart) + assert.is_nil(summary.latestEnd) + end) + + it('should calculate daily summary correctly with actual time data', function() + local mock_time = 1678704000 -- March 13, 2023, week 11 + os_module.time = function() + return mock_time + end + os_module.date = function(format, time_val) + time_val = time_val or mock_time + return original_os_date(format, time_val) + end + + maorunTime.setup({ path = tempPath }) + + -- Add some time entries for Monday + maorunTime.addManualTimeEntry({ + startTime = mock_time + 8 * 3600, -- 8:00 AM + endTime = mock_time + 12 * 3600, -- 12:00 PM + weekday = 'Monday', + project = 'WorkProject', + file = 'main.lua', + }) + + maorunTime.addManualTimeEntry({ + startTime = mock_time + 13 * 3600, -- 1:00 PM (after lunch) + endTime = mock_time + 17 * 3600, -- 5:00 PM + weekday = 'Monday', + project = 'WorkProject', + file = 'test.lua', + }) + + local summary = maorunTime.getDailySummary({ weekday = 'Monday' }) + + -- Should have calculated summary correctly + assert.are.equal('Monday', summary.weekday) + assert.are.equal(8, summary.workedHours) -- 4h + 4h + assert.are.equal(8, summary.expectedHours) + assert.are.equal(0, summary.overtime) + assert.is_true(summary.goalAchieved) + assert.are.equal(1, summary.pauseTime) -- 1 hour lunch break + + -- Should have project data + assert.is_table(summary.projects) + assert.is_not_nil(summary.projects.WorkProject) + assert.are.equal(8, summary.projects.WorkProject.totalHours) + + -- Should have file data + assert.is_not_nil(summary.projects.WorkProject.files['main.lua']) + assert.are.equal(4, summary.projects.WorkProject.files['main.lua'].hours) + assert.is_not_nil(summary.projects.WorkProject.files['test.lua']) + assert.are.equal(4, summary.projects.WorkProject.files['test.lua'].hours) + + -- Should have time period info + assert.are.equal(mock_time + 8 * 3600, summary.earliestStart) + assert.are.equal(mock_time + 17 * 3600, summary.latestEnd) + end) + + it('should handle multiple projects correctly', function() + local mock_time = 1678704000 + os_module.time = function() + return mock_time + end + os_module.date = function(format, time_val) + time_val = time_val or mock_time + return original_os_date(format, time_val) + end + + maorunTime.setup({ path = tempPath }) + + -- Add time entries for different projects + maorunTime.addManualTimeEntry({ + startTime = mock_time + 8 * 3600, + endTime = mock_time + 12 * 3600, + weekday = 'Tuesday', + project = 'ProjectA', + file = 'file1.lua', + }) + + maorunTime.addManualTimeEntry({ + startTime = mock_time + 13 * 3600, + endTime = mock_time + 15 * 3600, + weekday = 'Tuesday', + project = 'ProjectB', + file = 'file2.lua', + }) + + local summary = maorunTime.getDailySummary({ weekday = 'Tuesday' }) + + -- Should have both projects + assert.is_not_nil(summary.projects.ProjectA) + assert.is_not_nil(summary.projects.ProjectB) + assert.are.equal(4, summary.projects.ProjectA.totalHours) + assert.are.equal(2, summary.projects.ProjectB.totalHours) + assert.are.equal(6, summary.workedHours) + end) + end) + + describe('showDailyOverview integration', function() + it('should expose showDailyOverview function in UI module', function() + local ui = require('maorun.time.ui') + assert.is_function(ui.showDailyOverview) + end) + + it('should format daily summary content correctly', function() + local mock_time = 1678704000 + os_module.time = function() + return mock_time + end + os_module.date = function(format, time_val) + time_val = time_val or mock_time + return original_os_date(format, time_val) + end + + maorunTime.setup({ path = tempPath }) + + -- Add some test data + maorunTime.addManualTimeEntry({ + startTime = mock_time + 8 * 3600, + endTime = mock_time + 12 * 3600, + weekday = 'Wednesday', + project = 'TestProject', + file = 'test.lua', + }) + + local ui = require('maorun.time.ui') + local summary = maorunTime.getDailySummary({ weekday = 'Wednesday' }) + local content = ui._formatDailySummaryContent(summary) + + -- Should return formatted content without error + assert.is_table(content) + assert.is_true(#content > 0) + + -- Check for key sections + local content_str = table.concat(content, '\n') + -- Debug: print the content to see what's actually there + -- print("Content:", content_str) + assert.is_true(content_str:find('TagesΓΌbersicht') ~= nil) + assert.is_true(content_str:find('Arbeitszeit%-Übersicht') ~= nil) + assert.is_true(content_str:find('TestProject') ~= nil) + end) + end) +end)